diff options
| author | Adam Malczewski <[email protected]> | 2026-06-04 23:22:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-04 23:22:03 +0900 |
| commit | 9b611d614d123462e50492d78202dae696b99aa2 (patch) | |
| tree | 7e0c85b60dd0becddbb04c29fdf14855c10c090f /packages/storage-sqlite/src/storage.test.ts | |
| parent | 974ce6f46c25a522a42c6bd04fd62ce2d031aad5 (diff) | |
| download | dispatch-9b611d614d123462e50492d78202dae696b99aa2.tar.gz dispatch-9b611d614d123462e50492d78202dae696b99aa2.zip | |
feat(core-ext): storage-sqlite, auth-apikey, provider-openai-compat
- storage-sqlite: bun:sqlite StorageNamespace backend + migrations (21 bun tests)
- auth-apikey: pure resolver from env → ApiKeyCredentials (4 tests)
- provider-openai-compat: OpenAI-compatible SSE stream → ProviderEvents
- orchestrator fixes: provider imports (@dispatch/kernel), missing dep,
exactOptionalPropertyTypes (omit-when-undefined), root tsconfig refs
- vitest excludes storage-sqlite (bun:sqlite); test:bun runs it under bun
Diffstat (limited to 'packages/storage-sqlite/src/storage.test.ts')
| -rw-r--r-- | packages/storage-sqlite/src/storage.test.ts | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/packages/storage-sqlite/src/storage.test.ts b/packages/storage-sqlite/src/storage.test.ts new file mode 100644 index 0000000..fc14498 --- /dev/null +++ b/packages/storage-sqlite/src/storage.test.ts @@ -0,0 +1,218 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { Migration } from "./migrate.js"; +import { computePending } from "./migrate.js"; +import type { SqliteStorageBackend } from "./storage.js"; +import { createSqliteStorage } from "./storage.js"; + +describe("computePending (pure)", () => { + it("returns all migrations when none applied", () => { + const migrations: Migration[] = [ + { version: 2, name: "b", up: "" }, + { version: 1, name: "a", up: "" }, + ]; + const result = computePending(new Set(), migrations); + expect(result.map((m) => m.version)).toEqual([1, 2]); + }); + + it("skips already-applied versions", () => { + const migrations: Migration[] = [ + { version: 1, name: "a", up: "" }, + { version: 2, name: "b", up: "" }, + { version: 3, name: "c", up: "" }, + ]; + const result = computePending(new Set([1, 3]), migrations); + expect(result.map((m) => m.version)).toEqual([2]); + }); + + it("returns empty when all applied", () => { + const migrations: Migration[] = [{ version: 1, name: "a", up: "" }]; + const result = computePending(new Set([1]), migrations); + expect(result).toEqual([]); + }); + + it("sorts by version ascending", () => { + const migrations: Migration[] = [ + { version: 3, name: "c", up: "" }, + { version: 1, name: "a", up: "" }, + { version: 2, name: "b", up: "" }, + ]; + const result = computePending(new Set(), migrations); + expect(result.map((m) => m.version)).toEqual([1, 2, 3]); + }); +}); + +describe("createSqliteStorage", () => { + let backend: SqliteStorageBackend; + + beforeEach(() => { + backend = createSqliteStorage({ path: ":memory:" }); + }); + + afterEach(() => { + backend.close(); + }); + + describe("StorageNamespace get/set/delete/has", () => { + it("returns null for missing key", async () => { + const ns = backend.storage("test"); + expect(await ns.get("missing")).toBeNull(); + }); + + it("roundtrips set then get", async () => { + const ns = backend.storage("test"); + await ns.set("key1", "value1"); + expect(await ns.get("key1")).toBe("value1"); + }); + + it("overwrites existing value", async () => { + const ns = backend.storage("test"); + await ns.set("key1", "v1"); + await ns.set("key1", "v2"); + expect(await ns.get("key1")).toBe("v2"); + }); + + it("has returns false for missing, true for existing", async () => { + const ns = backend.storage("test"); + expect(await ns.has("key1")).toBe(false); + await ns.set("key1", "val"); + expect(await ns.has("key1")).toBe(true); + }); + + it("delete removes the key", async () => { + const ns = backend.storage("test"); + await ns.set("key1", "val"); + await ns.delete("key1"); + expect(await ns.get("key1")).toBeNull(); + expect(await ns.has("key1")).toBe(false); + }); + + it("delete on missing key is a no-op", async () => { + const ns = backend.storage("test"); + await ns.delete("nonexistent"); + }); + }); + + describe("keys", () => { + it("returns all keys in namespace", async () => { + const ns = backend.storage("test"); + await ns.set("a", "1"); + await ns.set("b", "2"); + await ns.set("c", "3"); + const keys = await ns.keys(); + expect(keys.toSorted()).toEqual(["a", "b", "c"]); + }); + + it("filters by prefix", async () => { + const ns = backend.storage("test"); + await ns.set("foo:1", "a"); + await ns.set("foo:2", "b"); + await ns.set("bar:1", "c"); + const keys = await ns.keys("foo"); + expect(keys.toSorted()).toEqual(["foo:1", "foo:2"]); + }); + + it("returns empty for no matches", async () => { + const ns = backend.storage("test"); + await ns.set("a", "1"); + expect(await ns.keys("zzz")).toEqual([]); + }); + }); + + describe("namespace isolation", () => { + it("same key in different namespaces does not collide", async () => { + const ns1 = backend.storage("ns1"); + const ns2 = backend.storage("ns2"); + await ns1.set("key", "value1"); + await ns2.set("key", "value2"); + expect(await ns1.get("key")).toBe("value1"); + expect(await ns2.get("key")).toBe("value2"); + }); + + it("delete in one namespace does not affect another", async () => { + const ns1 = backend.storage("ns1"); + const ns2 = backend.storage("ns2"); + await ns1.set("key", "v1"); + await ns2.set("key", "v2"); + await ns1.delete("key"); + expect(await ns1.has("key")).toBe(false); + expect(await ns2.get("key")).toBe("v2"); + }); + + it("keys are scoped to namespace", async () => { + const ns1 = backend.storage("ns1"); + const ns2 = backend.storage("ns2"); + await ns1.set("a", "1"); + await ns1.set("b", "2"); + await ns2.set("c", "3"); + expect((await ns1.keys()).toSorted()).toEqual(["a", "b"]); + expect(await ns2.keys()).toEqual(["c"]); + }); + }); + + describe("migrate", () => { + it("runs pending migrations in order", async () => { + const migrations: Migration[] = [ + { version: 1, name: "create_foo", up: "CREATE TABLE foo (id INTEGER PRIMARY KEY);" }, + { version: 2, name: "create_bar", up: "CREATE TABLE bar (id INTEGER PRIMARY KEY);" }, + ]; + await backend.migrate("ext1", migrations); + + const ns = backend.storage("ext1"); + await ns.set("test", "val"); + expect(await ns.get("test")).toBe("val"); + }); + + it("skips already-applied migrations", async () => { + const migrations: Migration[] = [ + { version: 1, name: "create_foo", up: "CREATE TABLE foo (id INTEGER PRIMARY KEY);" }, + ]; + await backend.migrate("ext1", migrations); + await backend.migrate("ext1", [ + ...migrations, + { version: 2, name: "create_bar", up: "CREATE TABLE bar (id INTEGER PRIMARY KEY);" }, + ]); + }); + + it("is idempotent — calling migrate twice with same migrations is safe", async () => { + const migrations: Migration[] = [ + { version: 1, name: "create_foo", up: "CREATE TABLE foo (id INTEGER PRIMARY KEY);" }, + ]; + await backend.migrate("ext1", migrations); + await backend.migrate("ext1", migrations); + }); + + it("migrations are per-namespace", async () => { + const m1: Migration[] = [ + { version: 1, name: "create_t1", up: "CREATE TABLE t1 (id INTEGER PRIMARY KEY);" }, + ]; + const m2: Migration[] = [ + { version: 1, name: "create_t2", up: "CREATE TABLE t2 (id INTEGER PRIMARY KEY);" }, + ]; + await backend.migrate("ext1", m1); + await backend.migrate("ext2", m2); + }); + }); +}); + +describe("createSqliteStorage — persistence across reopen", () => { + it("migrations survive close and reopen", async () => { + const tmpPath = `/tmp/storage-sqlite-test-${Date.now()}.db`; + const migrations: Migration[] = [ + { version: 1, name: "create_foo", up: "CREATE TABLE foo (id INTEGER PRIMARY KEY);" }, + ]; + + const first = createSqliteStorage({ path: tmpPath }); + await first.migrate("ext1", migrations); + first.close(); + + const second = createSqliteStorage({ path: tmpPath }); + await second.migrate("ext1", [ + ...migrations, + { version: 2, name: "create_bar", up: "CREATE TABLE bar (id INTEGER PRIMARY KEY);" }, + ]); + second.close(); + + const { unlinkSync } = await import("node:fs"); + unlinkSync(tmpPath); + }); +}); |
