UNPKG

@tanstack/ai-persistence

Version:

Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks.

1,073 lines (1,072 loc) 34.8 kB
import { beforeAll, describe, expect, it } from "vitest"; //#region src/testkit/conformance.ts /** * Shared conformance suite for the `AIPersistence` store contract. * * Every backend runs this identical suite — the in-memory reference store and * every adapter you write against your own database — so that schema drift or * an implementation gap fails immediately. It exercises every method of every * store the persistence exposes and is the authoritative compatibility gate for * the store interfaces in `../types.ts`. * * Covers all seven stores: the four chat state stores (`messages`, `runs`, * `interrupts`, `metadata`) and the three generation stores (`generationRuns`, * `artifacts`, `blobs`). Locks are not part of this suite — they are a separate * coordination concern (`LockStore` + `withLocks`), not a store. * * SKIPPING (declare or fail): a backend that deliberately omits a store must * declare it in `options.skip`, and one that omits an OPTIONAL store method * must declare it in `options.skipMethods`. Anything absent and not declared * fails the suite loudly, and anything declared absent is reported by vitest as * a SKIPPED case, never as a pass. Silent gaps are not allowed: a case that did * not run must never be indistinguishable from one that did. A chat-only * adapter therefore passes `skip: ['generationRuns', 'artifacts', 'blobs']`, * and a generation-only one skips the four state stores. * * NOT COVERED HERE: the four durable-run fields on `RunRecord` (`sandboxKey`, * `detachedSince`, `cancelRequested`, `driverEpoch`). They exist for durable * sandboxed runs, a chat app never writes them, and requiring them here made every * backend implement four columns and an omitted-vs-explicit-undefined rule it had no * use for. They are proven by `runDurableRunFieldsConformance` from * `@tanstack/ai-sandbox/testkit`, next to the takeover and reaper suites that consume * them. A backend that never runs sandboxes can leave the columns out. * * RESERVED RUN-ID PREFIX: `rc-` belongs to the `listReclaimable` case, which * filters the method's (not thread-scoped) result down to `rc-` ids before an * exact-set assertion. A new case in `describe('runs')` must NOT seed a run id * starting with `rc-`, or it silently changes that expected set. */ /** * Unwrap a value the store contract says must be present. Fails the test with a * readable message instead of a non-null assertion (banned in this package) or * an early `return` that would pass silently. */ function required(value, what) { if (value == null) throw new Error(`AIPersistence conformance: expected ${what} to exist`); return value; } /** Read a `BlobObject.body` to completion as one contiguous buffer. */ async function drainStream(stream) { const chunks = []; let total = 0; const reader = stream.getReader(); for (;;) { const { value, done } = await reader.read(); if (done) break; chunks.push(value); total += value.byteLength; } const bytes = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } return bytes; } /** * Register a Vitest suite that validates `makePersistence()` against the full * `AIPersistence` contract — every store it provides, and none it declares * skipped. */ function runPersistenceConformance(name, makePersistence, options) { const skip = new Set(options?.skip ?? []); const skipMethods = new Set(options?.skipMethods ?? []); describe(`AIPersistence conformance: ${name}`, () => { let persistence; beforeAll(async () => { persistence = await makePersistence(); }); /** * Return the store for `key`, or `null` when the backend intentionally * skips it. Throws (failing the test) when a store is missing but was not * declared in `options.skip`. */ function resolveStore(key) { const store = persistence.stores[key]; if (store) return store; if (skip.has(key)) return null; throw new Error(`AIPersistence conformance: store '${key}' is missing. Provide it, or pass { skip: ['${key}'] } if the omission is intentional.`); } /** * Narrow `runs` to a store that definitely implements the optional method * `methodName`, so the case can call it without a non-null assertion. * * Returns `false` only when the omission was declared in * `options.skipMethods` (the caller then reports a skip). An undeclared * omission throws, mirroring `resolveStore`: a case that cannot run must * never be reported as a pass. */ function hasRunsMethod(runs, methodName) { if (runs[methodName]) return true; const key = `runs.${methodName}`; if (skipMethods.has(key)) return false; throw new Error(`AIPersistence conformance: optional method '${key}' is not implemented. Implement it, or pass { skipMethods: ['${key}'] } if the omission is intentional.`); } describe("messages", () => { it("round-trips a thread and returns [] for unknown threads", async (ctx) => { const store = resolveStore("messages"); if (!store) return ctx.skip("store not provided"); expect(await store.loadThread("thread-unknown")).toEqual([]); await store.saveThread("thread-msg", [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }]); expect(await store.loadThread("thread-msg")).toEqual([{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }]); await store.saveThread("thread-msg", [{ role: "user", content: "redo" }]); expect(await store.loadThread("thread-msg")).toEqual([{ role: "user", content: "redo" }]); }); it("round-trips rich message shapes with deep equality", async (ctx) => { const store = resolveStore("messages"); if (!store) return ctx.skip("store not provided"); const rich = [ { role: "user", content: "plain string" }, { role: "assistant", content: "", toolCalls: [{ id: "call-1", type: "function", function: { name: "search", arguments: "{\"query\":\"weather in Paris\"}" } }] }, { role: "tool", content: "{\"temperature\":21,\"unit\":\"C\"}", toolCallId: "call-1" }, { role: "user", content: [{ type: "text", content: "What is in this image?" }, { type: "image", source: { type: "url", value: "https://example.com/cat.png", mimeType: "image/png" } }] }, { role: "assistant", content: "Here is my answer.", thinking: [{ content: "The user is asking about the image.", signature: "sig-1" }] } ]; await store.saveThread("thread-rich", rich); expect(await store.loadThread("thread-rich")).toEqual(rich); }); }); describe("runs", () => { it("creates, resumes idempotently, updates, and gets", async (ctx) => { const store = resolveStore("runs"); if (!store) return ctx.skip("store not provided"); expect(await store.get("run-missing")).toBeNull(); const created = await store.createOrResume({ runId: "run-1", threadId: "thread-1", startedAt: 1e3 }); expect(created).toMatchObject({ runId: "run-1", threadId: "thread-1", status: "running", startedAt: 1e3 }); const resumed = await store.createOrResume({ runId: "run-1", threadId: "thread-different", startedAt: 9999 }); expect(resumed).toMatchObject({ runId: "run-1", threadId: "thread-1", startedAt: 1e3 }); await store.update("run-1", { status: "completed", finishedAt: 2e3, usage: { promptTokens: 3, completionTokens: 4, totalTokens: 7 } }); const done = await store.get("run-1"); expect(done).toMatchObject({ runId: "run-1", status: "completed", finishedAt: 2e3, usage: { promptTokens: 3, completionTokens: 4, totalTokens: 7 } }); const resumedAfterUpdate = await store.createOrResume({ runId: "run-1", threadId: "thread-different", startedAt: 9999 }); expect(resumedAfterUpdate).toEqual(done); await store.update("run-1", { status: "failed", error: { message: "boom", code: "provider_overloaded" } }); const failed = await store.get("run-1"); expect(failed?.status).toBe("failed"); expect(failed?.error).toEqual({ message: "boom", code: "provider_overloaded" }); await store.update("run-absent", { status: "completed" }); expect(await store.get("run-absent")).toBeNull(); }); it("createOrResume never resurrects a finished run", async (ctx) => { const store = resolveStore("runs"); if (!store) return ctx.skip("store not provided"); await store.createOrResume({ runId: "nc-1", threadId: "nc-t", startedAt: 10 }); await store.update("nc-1", { status: "completed", finishedAt: 20 }); const resumed = await store.createOrResume({ runId: "nc-1", threadId: "nc-t", startedAt: 999, status: "running" }); expect(resumed).toMatchObject({ runId: "nc-1", status: "completed", startedAt: 10, finishedAt: 20 }); expect(await store.get("nc-1")).toMatchObject({ status: "completed", startedAt: 10, finishedAt: 20 }); }); it("findActiveRun returns the most recent running run for a thread", async (ctx) => { const store = resolveStore("runs"); if (!store) return ctx.skip("store not provided"); const thread = "thread-active"; expect(await store.findActiveRun(thread)).toBeNull(); await store.createOrResume({ runId: "active-1", threadId: thread, startedAt: 1e3 }); await store.createOrResume({ runId: "active-2", threadId: thread, startedAt: 2e3 }); expect(await store.findActiveRun(thread)).toMatchObject({ runId: "active-2", status: "running" }); await store.createOrResume({ runId: "other-1", threadId: "thread-other", startedAt: 3e3 }); expect(await store.findActiveRun(thread)).toMatchObject({ runId: "active-2" }); await store.update("active-2", { status: "completed", finishedAt: 2500 }); expect(await store.findActiveRun(thread)).toMatchObject({ runId: "active-1", status: "running" }); await store.update("active-1", { status: "completed", finishedAt: 1500 }); expect(await store.findActiveRun(thread)).toBeNull(); }); it("lists runs by thread when supported", async (ctx) => { const runs = resolveStore("runs"); if (!runs) return ctx.skip("store not provided"); if (!hasRunsMethod(runs, "listByThread")) return ctx.skip("runs.listByThread not implemented"); await runs.createOrResume({ runId: "lt-b", threadId: "lt", startedAt: 2 }); await runs.createOrResume({ runId: "lt-a", threadId: "lt", startedAt: 1 }); const listed = await runs.listByThread("lt"); expect(listed.map((r) => r.runId)).toEqual(["lt-a", "lt-b"]); }); it("lists reclaimable detached runs when supported", async (ctx) => { const runs = resolveStore("runs"); if (!runs) return ctx.skip("store not provided"); if (!hasRunsMethod(runs, "listReclaimable")) return ctx.skip("runs.listReclaimable not implemented"); const now = 1e4; const ttlMs = 5e3; const cutoff = 5e3; await runs.createOrResume({ runId: "rc-included", threadId: "rc-t", startedAt: 1 }); await runs.update("rc-included", { detachedSince: 1e3 }); await runs.createOrResume({ runId: "rc-boundary", threadId: "rc-t", startedAt: 1 }); await runs.update("rc-boundary", { detachedSince: cutoff }); await runs.createOrResume({ runId: "rc-too-recent", threadId: "rc-t", startedAt: 1 }); await runs.update("rc-too-recent", { detachedSince: 5001 }); await runs.createOrResume({ runId: "rc-completed", threadId: "rc-t", startedAt: 1 }); await runs.update("rc-completed", { detachedSince: 1e3, status: "completed", finishedAt: 2e3 }); await runs.createOrResume({ runId: "rc-never-detached", threadId: "rc-t", startedAt: 1 }); const ourIds = (await runs.listReclaimable({ now, ttlMs })).map((r) => r.runId).filter((id) => id.startsWith("rc-")).sort(); expect(ourIds).toEqual(["rc-boundary", "rc-included"]); const zeroTtlRunId = `rc-${crypto.randomUUID()}`; await runs.createOrResume({ runId: zeroTtlRunId, threadId: "rc-t", startedAt: 1 }); await runs.update(zeroTtlRunId, { detachedSince: now }); const zeroTtlReclaimable = await runs.listReclaimable({ now, ttlMs: 0 }); expect(zeroTtlReclaimable.find((r) => r.runId === zeroTtlRunId)?.detachedSince).toBe(now); const reattachedRunId = `rc-${crypto.randomUUID()}`; await runs.createOrResume({ runId: reattachedRunId, threadId: "rc-t", startedAt: 1 }); await runs.update(reattachedRunId, { detachedSince: 1e3 }); await runs.update(reattachedRunId, { detachedSince: void 0 }); const afterReattach = await runs.listReclaimable({ now, ttlMs }); expect(afterReattach.some((r) => r.runId === reattachedRunId)).toBe(false); const terminalRunIds = await Promise.all([ "completed", "failed", "aborted" ].map(async (status) => { const runId = `rc-${crypto.randomUUID()}`; await runs.createOrResume({ runId, threadId: "rc-t", startedAt: 1 }); await runs.update(runId, { status, detachedSince: 1e3, finishedAt: 2e3 }); return runId; })); const afterTerminal = await runs.listReclaimable({ now, ttlMs }); expect(afterTerminal.some((r) => terminalRunIds.includes(r.runId))).toBe(false); const interruptedRunId = `rc-${crypto.randomUUID()}`; await runs.createOrResume({ runId: interruptedRunId, threadId: "rc-t", startedAt: 1 }); await runs.update(interruptedRunId, { status: "interrupted", detachedSince: 1e3 }); const afterInterrupted = await runs.listReclaimable({ now, ttlMs }); expect(afterInterrupted.some((r) => r.runId === interruptedRunId)).toBe(false); }); }); describe("interrupts", () => { it("creates, resolves, cancels, and lists by thread and run", async (ctx) => { const store = resolveStore("interrupts"); if (!store) return ctx.skip("store not provided"); expect(await store.get("int-missing")).toBeNull(); await store.create({ interruptId: "int-1", runId: "run-i", threadId: "thread-i", requestedAt: 10, payload: { tool: "search", args: { q: "x" } } }); await store.create({ interruptId: "int-2", runId: "run-i", threadId: "thread-i", requestedAt: 20, payload: { tool: "write" } }); await store.create({ interruptId: "int-3", runId: "run-other", threadId: "thread-i", requestedAt: 30, payload: {} }); const one = await store.get("int-1"); expect(one).toMatchObject({ interruptId: "int-1", runId: "run-i", threadId: "thread-i", status: "pending", requestedAt: 10, payload: { tool: "search", args: { q: "x" } } }); expect((await store.list("thread-i")).map((r) => r.interruptId)).toEqual([ "int-1", "int-2", "int-3" ]); expect((await store.listByRun("run-i")).map((r) => r.interruptId)).toEqual(["int-1", "int-2"]); expect((await store.listPending("thread-i")).map((r) => r.interruptId)).toEqual([ "int-1", "int-2", "int-3" ]); await store.resolve("int-1", { ok: true }); const resolved = await store.get("int-1"); expect(resolved?.status).toBe("resolved"); expect(resolved?.response).toEqual({ ok: true }); expect(typeof resolved?.resolvedAt).toBe("number"); await store.cancel("int-2"); const cancelled = await store.get("int-2"); expect(cancelled?.status).toBe("cancelled"); expect(typeof cancelled?.resolvedAt).toBe("number"); expect((await store.listPending("thread-i")).map((r) => r.interruptId)).toEqual(["int-3"]); expect((await store.listPendingByRun("run-i")).map((r) => r.interruptId)).toEqual([]); }); it("create is insert-if-absent: a duplicate id never clobbers a resolved interrupt", async (ctx) => { const store = resolveStore("interrupts"); if (!store) return ctx.skip("store not provided"); await store.create({ interruptId: "int-dup", runId: "run-dup", threadId: "thread-dup", requestedAt: 100, payload: { attempt: 1 } }); await store.resolve("int-dup", { answer: 42 }); await store.create({ interruptId: "int-dup", runId: "run-dup", threadId: "thread-dup", requestedAt: 200, payload: { attempt: 2 } }); const after = await store.get("int-dup"); expect(after?.status).toBe("resolved"); expect(after?.response).toEqual({ answer: 42 }); expect(after?.payload).toEqual({ attempt: 1 }); expect(after?.requestedAt).toBe(100); }); it("lists ordered by requestedAt ascending even when inserts are out of order", async (ctx) => { const store = resolveStore("interrupts"); if (!store) return ctx.skip("store not provided"); await store.create({ interruptId: "int-late", runId: "run-order", threadId: "thread-order", requestedAt: 300, payload: {} }); await store.create({ interruptId: "int-early", runId: "run-order", threadId: "thread-order", requestedAt: 100, payload: {} }); await store.create({ interruptId: "int-mid", runId: "run-order", threadId: "thread-order", requestedAt: 200, payload: {} }); expect((await store.list("thread-order")).map((r) => r.interruptId)).toEqual([ "int-early", "int-mid", "int-late" ]); expect((await store.listPending("thread-order")).map((r) => r.interruptId)).toEqual([ "int-early", "int-mid", "int-late" ]); expect((await store.listByRun("run-order")).map((r) => r.interruptId)).toEqual([ "int-early", "int-mid", "int-late" ]); }); }); describe("generationRuns", () => { it("creates, resumes idempotently, updates, and gets", async () => { const store = resolveStore("generationRuns"); if (!store) return; expect(await store.get("gen-missing")).toBeNull(); const created = await store.createOrResume({ runId: "gen-1", threadId: "gen-thread-1", activity: "image", provider: "openai", model: "gpt-image-1", startedAt: 1e3 }); expect(created).toMatchObject({ runId: "gen-1", activity: "image", provider: "openai", model: "gpt-image-1", status: "running", startedAt: 1e3 }); expect(created.threadId).toBe("gen-thread-1"); const resumed = await store.createOrResume({ runId: "gen-1", activity: "video", provider: "google", model: "veo-3", startedAt: 9999, threadId: "thread-late" }); expect(resumed).toMatchObject({ runId: "gen-1", activity: "image", provider: "openai", model: "gpt-image-1", startedAt: 1e3 }); expect(resumed.threadId).toBe("gen-thread-1"); await store.update("gen-1", { status: "completed", finishedAt: 2e3, result: { images: [{ url: "https://example.com/a.png" }] }, artifacts: [{ role: "output", artifactId: "art-1", runId: "gen-1", threadId: "thread-gen", name: "a.png", mimeType: "image/png", size: 3, createdAt: "2024-01-01T00:00:00.000Z", source: { activity: "image", path: "images.0", provider: "openai", model: "gpt-image-1" } }], usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 } }); const done = await store.get("gen-1"); expect(done).toMatchObject({ status: "completed", finishedAt: 2e3, result: { images: [{ url: "https://example.com/a.png" }] }, usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 } }); expect(done?.artifacts).toHaveLength(1); expect(done?.artifacts?.[0]).toMatchObject({ artifactId: "art-1", mimeType: "image/png" }); await store.update("gen-1", { status: "failed", error: { message: "boom", code: "provider_error" } }); const failed = await store.get("gen-1"); expect(failed?.status).toBe("failed"); expect(failed?.error).toEqual({ message: "boom", code: "provider_error" }); await store.update("gen-absent", { status: "completed" }); expect(await store.get("gen-absent")).toBeNull(); }); it("findLatestForThread returns the most recently started linked run", async () => { const store = resolveStore("generationRuns"); if (!store) return; const thread = "thread-gen-latest"; expect(await store.findLatestForThread(thread)).toBeNull(); await store.createOrResume({ runId: "gen-late", activity: "image", provider: "openai", model: "gpt-image-1", startedAt: 3e3, threadId: thread }); await store.createOrResume({ runId: "gen-early", activity: "image", provider: "openai", model: "gpt-image-1", startedAt: 1e3, threadId: thread }); expect(await store.findLatestForThread(thread)).toMatchObject({ runId: "gen-late" }); await store.createOrResume({ runId: "gen-other", activity: "image", provider: "openai", model: "gpt-image-1", startedAt: 9e3, threadId: "thread-gen-other" }); await store.update("gen-late", { status: "completed", finishedAt: 3500 }); expect(await store.findLatestForThread(thread)).toMatchObject({ runId: "gen-late", status: "completed" }); expect(await store.findLatestForThread("thread-unlinked")).toBeNull(); }); }); describe("artifacts", () => { const artifact = (overrides) => ({ runId: "run-art", threadId: "thread-art", name: "image.png", mimeType: "image/png", size: 3, createdAt: 100, ...overrides }); it("saves as an upsert, gets, and lists by run", async () => { const store = resolveStore("artifacts"); if (!store) return; expect(await store.get("art-missing")).toBeNull(); expect(await store.list("run-unknown")).toEqual([]); await store.save(artifact({ artifactId: "𐀀", blobKey: "artifacts/run-art/art-a", createdAt: 100 })); await store.save(artifact({ artifactId: "", sourceUrl: "https://provider.example/expiring.png", createdAt: 100 })); await store.save(artifact({ artifactId: "art-c", runId: "run-art-other" })); expect(await store.get("𐀀")).toMatchObject({ artifactId: "𐀀", runId: "run-art", threadId: "thread-art", blobKey: "artifacts/run-art/art-a", name: "image.png", mimeType: "image/png", size: 3, createdAt: 100 }); expect(await store.get("")).toMatchObject({ sourceUrl: "https://provider.example/expiring.png" }); expect((await store.list("run-art")).map((r) => r.artifactId)).toEqual(["", "𐀀"]); await store.save(artifact({ artifactId: "𐀀", name: "renamed.png", size: 9 })); const updated = await store.get("𐀀"); expect(updated).toMatchObject({ name: "renamed.png", size: 9 }); expect(updated?.blobKey).toBeUndefined(); expect((await store.list("run-art")).map((r) => r.artifactId)).toEqual(["", "𐀀"]); }); it("lists a thread in deterministic createdAt and artifactId order", async () => { const store = resolveStore("artifacts"); if (!store) return; await store.save(artifact({ artifactId: "thread-b", threadId: "thread-order", createdAt: 2 })); await store.save(artifact({ artifactId: "thread-a", threadId: "thread-order", createdAt: 2 })); await store.save(artifact({ artifactId: "thread-early", threadId: "thread-order", createdAt: 1 })); await store.save(artifact({ artifactId: "other", threadId: "thread-other", createdAt: 0 })); expect((await store.listForThread("thread-order")).map((r) => r.artifactId)).toEqual([ "thread-early", "thread-a", "thread-b" ]); }); it("orders artifact IDs by UTF-8 bytes after createdAt", async () => { const store = resolveStore("artifacts"); if (!store) return; await store.save(artifact({ artifactId: "𐀀", threadId: "thread-utf8", createdAt: 1 })); await store.save(artifact({ artifactId: "", threadId: "thread-utf8", createdAt: 1 })); await store.save(artifact({ artifactId: "a", threadId: "thread-utf8", createdAt: 1 })); expect((await store.listForThread("thread-utf8")).map((r) => r.artifactId)).toEqual([ "a", "", "𐀀" ]); }); it("deletes one artifact and every artifact for a run", async () => { const store = resolveStore("artifacts"); if (!store) return; await store.save(artifact({ artifactId: "art-d1", runId: "run-del" })); await store.save(artifact({ artifactId: "art-d2", runId: "run-del" })); await store.save(artifact({ artifactId: "art-keep", runId: "run-keep" })); await store.delete("art-d1"); expect(await store.get("art-d1")).toBeNull(); expect((await store.list("run-del")).map((r) => r.artifactId)).toEqual(["art-d2"]); await store.delete("art-d1"); await store.deleteForRun("run-del"); expect(await store.list("run-del")).toEqual([]); expect(await store.get("art-d2")).toBeNull(); expect((await store.list("run-keep")).map((r) => r.artifactId)).toEqual(["art-keep"]); await store.deleteForRun("run-del"); }); }); describe("blobs", () => { it("round-trips bytes and metadata through put/get/head", async () => { const store = resolveStore("blobs"); if (!store) return; expect(await store.get("blob-missing")).toBeNull(); expect(await store.head("blob-missing")).toBeNull(); const bytes = new Uint8Array([ 1, 2, 3, 4 ]); const put = await store.put("blob/a", bytes, { contentType: "image/png", customMetadata: { runId: "run-blob" } }); expect(put).toMatchObject({ key: "blob/a", size: 4, contentType: "image/png", customMetadata: { runId: "run-blob" } }); const object = required(await store.get("blob/a"), "blob/a"); expect(new Uint8Array(await object.arrayBuffer())).toEqual(bytes); expect(object).toMatchObject({ key: "blob/a", size: 4, contentType: "image/png", customMetadata: { runId: "run-blob" } }); const head = await store.head("blob/a"); expect(head).toMatchObject({ key: "blob/a", size: 4 }); await store.put("blob/text", "héllo"); const text = required(await store.get("blob/text"), "blob/text"); expect(await text.text()).toBe("héllo"); await store.put("blob/buffer", new Uint8Array([9, 9]).buffer); const buffered = required(await store.get("blob/buffer"), "blob/buffer"); expect(new Uint8Array(await buffered.arrayBuffer())).toEqual(new Uint8Array([9, 9])); }); it("accepts a stream body with no declared length", async () => { const store = resolveStore("blobs"); if (!store) return; const bytes = (/* @__PURE__ */ new Uint8Array(65536)).map((_, i) => i % 251); const lengthless = required(new Response(bytes).body, "response body").pipeThrough(new TransformStream()); const put = await store.put("blob/stream", lengthless, { contentType: "application/octet-stream" }); expect(put.size).toBe(bytes.byteLength); const object = required(await store.get("blob/stream"), "blob/stream"); expect(new Uint8Array(await object.arrayBuffer())).toEqual(bytes); expect(object.size).toBe(bytes.byteLength); }); it("accepts expectedLength as an advisory hint for a stream body", async () => { const store = resolveStore("blobs"); if (!store) return; const bytes = (/* @__PURE__ */ new Uint8Array(9216)).map((_, i) => i % 251); const lengthless = required(new Response(bytes).body, "response body").pipeThrough(new TransformStream()); const put = await store.put("blob/stream-hint", lengthless, { expectedLength: bytes.byteLength }); expect(put.size).toBe(bytes.byteLength); const object = required(await store.get("blob/stream-hint"), "blob/stream-hint"); expect(new Uint8Array(await object.arrayBuffer())).toEqual(bytes); expect(object.size).toBe(bytes.byteLength); }); it("serves a byte range and reports the slice it served", async () => { const store = resolveStore("blobs"); if (!store) return; const bytes = (/* @__PURE__ */ new Uint8Array(1024)).map((_, i) => i % 251); await store.put("blob/range", bytes); const middle = required(await store.get("blob/range", { range: { offset: 100, length: 50 } }), "blob/range"); expect(new Uint8Array(await middle.arrayBuffer())).toEqual(bytes.slice(100, 150)); expect(middle.size).toBe(bytes.byteLength); expect(middle.range).toEqual({ offset: 100, length: 50 }); const tail = required(await store.get("blob/range", { range: { offset: 1e3 } }), "blob/range"); expect(new Uint8Array(await tail.arrayBuffer())).toEqual(bytes.slice(1e3)); expect(tail.range).toEqual({ offset: 1e3, length: 24 }); const clamped = required(await store.get("blob/range", { range: { offset: 1e3, length: 1e3 } }), "blob/range"); expect(clamped.range).toEqual({ offset: 1e3, length: 24 }); expect((await clamped.arrayBuffer()).byteLength).toBe(24); const body = required(await store.get("blob/range", { range: { offset: 10, length: 5 } }), "blob/range").body; if (body) expect(await drainStream(body)).toEqual(bytes.slice(10, 15)); const whole = required(await store.get("blob/range"), "blob/range"); expect(whole.range).toBeUndefined(); }); it("overwrites an existing key and deletes silently", async () => { const store = resolveStore("blobs"); if (!store) return; const first = await store.put("blob/over", new Uint8Array([1]), { contentType: "text/plain", customMetadata: { v: "1" } }); const second = await store.put("blob/over", new Uint8Array([ 2, 2, 2 ]), { contentType: "application/octet-stream", customMetadata: { v: "2" } }); expect(second).toMatchObject({ size: 3, contentType: "application/octet-stream", customMetadata: { v: "2" } }); if (first.etag !== void 0 && second.etag !== void 0) expect(second.etag).not.toBe(first.etag); const after = required(await store.get("blob/over"), "blob/over"); expect(new Uint8Array(await after.arrayBuffer())).toEqual(new Uint8Array([ 2, 2, 2 ])); await store.delete("blob/over"); expect(await store.get("blob/over")).toBeNull(); expect(await store.head("blob/over")).toBeNull(); await store.delete("blob/over"); }); it("lists by literal prefix in ascending key order", async () => { const store = resolveStore("blobs"); if (!store) return; await store.put("list_/b", new Uint8Array([2])); await store.put("list_/a", new Uint8Array([1])); await store.put("list_/c", new Uint8Array([3])); await store.put("list-x/d", new Uint8Array([4])); await store.put("LIST_/e", new Uint8Array([5])); const page = await store.list({ prefix: "list_/" }); expect(page.objects.map((o) => o.key)).toEqual([ "list_/a", "list_/b", "list_/c" ]); expect(page.truncated).toBeFalsy(); expect((await store.list({ prefix: "list_/nothing-here" })).objects).toEqual([]); }); it("pages with a cursor and returns an empty page for limit 0", async () => { const store = resolveStore("blobs"); if (!store) return; for (const key of [ "page/a", "page/b", "page/c", "page/d", "page/e" ]) await store.put(key, new Uint8Array([1])); const empty = await store.list({ prefix: "page/", limit: 0 }); expect(empty.objects).toEqual([]); expect(empty.truncated).toBeFalsy(); expect(empty.cursor).toBeUndefined(); const seen = []; let cursor; for (let guard = 0; guard < 10; guard++) { const result = await store.list({ prefix: "page/", limit: 2, ...cursor !== void 0 ? { cursor } : {} }); seen.push(...result.objects.map((o) => o.key)); if (!result.truncated) break; expect(result.cursor).toBe(result.objects.at(-1)?.key); cursor = result.cursor; } expect(seen).toEqual([ "page/a", "page/b", "page/c", "page/d", "page/e" ]); const exact = await store.list({ prefix: "page/", limit: 5 }); expect(exact.objects.map((o) => o.key)).toEqual(seen); expect(exact.truncated).toBeFalsy(); }); }); describe("metadata", () => { it("sets, gets, namespaces, and deletes without composite-key collisions", async (ctx) => { const store = resolveStore("metadata"); if (!store) return ctx.skip("store not provided"); expect(await store.get("scope-a", "k")).toBeNull(); await store.set("scope-a", "k", { n: 1 }); await store.set("scope-b", "k", { n: 2 }); expect(await store.get("scope-a", "k")).toEqual({ n: 1 }); expect(await store.get("scope-b", "k")).toEqual({ n: 2 }); await store.set("scope-a", "k", { n: 3 }); expect(await store.get("scope-a", "k")).toEqual({ n: 3 }); await store.delete("scope-a", "k"); expect(await store.get("scope-a", "k")).toBeNull(); expect(await store.get("scope-b", "k")).toEqual({ n: 2 }); await store.set("a:b", "c", "left"); await store.set("a", "b:c", "right"); expect(await store.get("a:b", "c")).toBe("left"); expect(await store.get("a", "b:c")).toBe("right"); await store.delete("a:b", "c"); expect(await store.get("a:b", "c")).toBeNull(); expect(await store.get("a", "b:c")).toBe("right"); }); }); }); } //#endregion export { runPersistenceConformance }; //# sourceMappingURL=conformance.js.map