From bf7acdcc18e4dff6442ac3381f57f6d79cb66e7a Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:32:25 +0200 Subject: [PATCH] test(sqlite): finalize repo and storage conformance --- .../testing/conformance/session-repo.ts | 6 + .../session/testing/conformance/storage.ts | 90 +++++++++- .../sqlite-node/src/sqlite/repo.ts | 2 +- .../sqlite-node/src/sqlite/storage.ts | 4 +- .../sqlite-node/test/repo-conformance.test.ts | 42 +++++ .../sqlite-node/test/repo.test.ts | 170 +----------------- .../test/storage-conformance.test.ts | 6 +- 7 files changed, 141 insertions(+), 179 deletions(-) create mode 100644 packages/session-backends/sqlite-node/test/repo-conformance.test.ts diff --git a/packages/agent/src/harness/session/testing/conformance/session-repo.ts b/packages/agent/src/harness/session/testing/conformance/session-repo.ts index f39d87162..e973f1c13 100644 --- a/packages/agent/src/harness/session/testing/conformance/session-repo.ts +++ b/packages/agent/src/harness/session/testing/conformance/session-repo.ts @@ -572,6 +572,8 @@ export function createSessionRepoForkSourceSnapshotConformance id), [ROOT_ID], ); + strictEqual(await forked.getName(BACKGROUND_CONTEXT), "first name"); + strictEqual(await forked.getLabel(ROOT_ID, BACKGROUND_CONTEXT), "first label"); await Promise.all([source.close(BACKGROUND_CONTEXT), forked.close(BACKGROUND_CONTEXT)]); }, ), diff --git a/packages/agent/src/harness/session/testing/conformance/storage.ts b/packages/agent/src/harness/session/testing/conformance/storage.ts index f92915696..e833753f2 100644 --- a/packages/agent/src/harness/session/testing/conformance/storage.ts +++ b/packages/agent/src/harness/session/testing/conformance/storage.ts @@ -2,7 +2,16 @@ import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict"; import type { Usage } from "@earendil-works/pi-ai"; import { BACKGROUND_CONTEXT } from "../../../context.ts"; import { insertEntry, insertUsage } from "../../commit.ts"; -import type { CompactionEntry, CustomEntry, Entry, JsonValue, MessageEntry, NewEntry, Write } from "../../types.ts"; +import type { + CompactionEntry, + CustomEntry, + Entry, + EntryStructure, + JsonValue, + MessageEntry, + NewEntry, + Write, +} from "../../types.ts"; import { appendList, deleteList, @@ -104,7 +113,8 @@ function compactionEntry(id: string, parentId: string | null): NewEntry entry.id); } @@ -371,6 +381,43 @@ export function createStorageConformance(factory: () => Promise) }, ), + createCase( + factory, + "values", + "applies same-transaction value and list operations in write order", + async ({ storage }) => { + const keptValue = testValue("write-order/kept"); + const deletedValue = testValue("write-order/deleted"); + const keptList = testList("write-order/kept"); + const deletedList = testList("write-order/deleted"); + const result = await storage.commit( + [ + setValue(deletedValue, "transient"), + deleteValue(deletedValue), + setValue(keptValue, "transient"), + setValue(keptValue, "kept"), + appendList(keptList, "transient"), + deleteList(keptList), + appendList(keptList, "kept"), + appendList(deletedList, "transient"), + deleteList(deletedList), + ], + BACKGROUND_CONTEXT, + ); + + strictEqual(await storage.getValue(deletedValue, BACKGROUND_CONTEXT), undefined); + deepStrictEqual(await storage.getValue(keptValue, BACKGROUND_CONTEXT), { + address: keptValue, + value: "kept", + seq: result.seqs[3], + }); + deepStrictEqual(await storage.readList(keptList, undefined, BACKGROUND_CONTEXT), [ + { seq: result.seqs[6], value: "kept" }, + ]); + deepStrictEqual(await storage.readList(deletedList, undefined, BACKGROUND_CONTEXT), []); + }, + ), + createCase( factory, "values", @@ -693,6 +740,45 @@ export function createStorageConformance(factory: () => Promise) ); }), + createCase( + factory, + "branch queries", + "applies branch query semantics to structure scans", + async ({ storage }) => { + const result = await storage.commit( + [ + insertEntry(userEntry("root")), + insertEntry(customEntry("marker", "root", "marker")), + insertEntry(userEntry("middle", "marker")), + insertEntry(compactionEntry("compact", "middle")), + insertEntry(customEntry("note", "compact", "note")), + insertEntry(userEntry("leaf", "note")), + ], + BACKGROUND_CONTEXT, + ); + + deepStrictEqual( + ids( + await storage.scanBranchStructure( + { start: "leaf", stopAtType: "compaction", type: "message" }, + BACKGROUND_CONTEXT, + ), + ), + ["leaf"], + ); + deepStrictEqual( + ids( + await storage.scanBranchStructure( + { start: "leaf", order: "oldestFirst", cursor: { seq: result.seqs[1] }, limit: 2 }, + BACKGROUND_CONTEXT, + ), + ), + ["middle", "compact"], + ); + await rejects(storage.scanBranchStructure({ start: "missing" }, BACKGROUND_CONTEXT)); + }, + ), + createCase( factory, "usage and stats", diff --git a/packages/session-backends/sqlite-node/src/sqlite/repo.ts b/packages/session-backends/sqlite-node/src/sqlite/repo.ts index a23b15cc9..81ab4c699 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/repo.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/repo.ts @@ -152,7 +152,6 @@ export class SqliteSessionRepo { async create(options: SqliteSessionCreateOptions | undefined, _context: Context): Promise { options ??= {}; - await mkdir(this.directory, { recursive: true }); const createdAt = this.now(); const id = options.id ?? uuidv7(createdAt); this.reserveId(id); @@ -163,6 +162,7 @@ export class SqliteSessionRepo { let initialized = false; let session: SqliteOpenSession | undefined; try { + await mkdir(this.directory, { recursive: true }); const file = await openFile(path, "wx"); await file.close(); reservedFile = true; diff --git a/packages/session-backends/sqlite-node/src/sqlite/storage.ts b/packages/session-backends/sqlite-node/src/sqlite/storage.ts index 740520979..1f0df84c8 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/storage.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/storage.ts @@ -106,12 +106,12 @@ export class SqliteStorage implements Storage { scanBranch(query: StorageBranchScan, _context: Context): Promise { if (this.state !== "open") return Promise.reject(new Error("SqliteStorage is closed")); - return Promise.resolve(scanBranchEntries(this.db, query)); + return Promise.resolve().then(() => scanBranchEntries(this.db, query)); } scanBranchStructure(query: StorageBranchScan, _context: Context): Promise { if (this.state !== "open") return Promise.reject(new Error("SqliteStorage is closed")); - return Promise.resolve(scanBranchEntryStructures(this.db, query)); + return Promise.resolve().then(() => scanBranchEntryStructures(this.db, query)); } scanEntries(query: EntryScan, _context: Context): Promise { diff --git a/packages/session-backends/sqlite-node/test/repo-conformance.test.ts b/packages/session-backends/sqlite-node/test/repo-conformance.test.ts new file mode 100644 index 000000000..9db8d32df --- /dev/null +++ b/packages/session-backends/sqlite-node/test/repo-conformance.test.ts @@ -0,0 +1,42 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { type ConformanceCase, createSessionRepoConformance } from "@earendil-works/pi-agent-core/session/testing"; +import { describe, it } from "vitest"; +import { createNodeSqliteFactory, SqliteSessionRepo } from "../src/index.ts"; + +const NOW = 1_700_000_000_000; + +function registerConformance(name: string, cases: readonly ConformanceCase[]): void { + describe(name, () => { + for (const group of new Set(cases.map((testCase) => testCase.group))) { + describe(group, () => { + for (const testCase of cases.filter((candidate) => candidate.group === group)) { + it(testCase.name, () => testCase.run()); + } + }); + } + }); +} + +let currentDirectory: string | undefined; + +async function createConformanceRepo() { + currentDirectory = await mkdtemp(join(tmpdir(), "pi-sqlite-session-repo-conformance-")); + return new SqliteSessionRepo({ + directory: currentDirectory, + databaseFactory: createNodeSqliteFactory(), + now: () => NOW, + }); +} + +async function cleanupConformanceRepo() { + if (currentDirectory === undefined) return; + await rm(currentDirectory, { recursive: true, force: true }); + currentDirectory = undefined; +} + +registerConformance( + "SqliteSessionRepo conformance", + createSessionRepoConformance(createConformanceRepo, cleanupConformanceRepo), +); diff --git a/packages/session-backends/sqlite-node/test/repo.test.ts b/packages/session-backends/sqlite-node/test/repo.test.ts index f5e17a48c..4e6c4edc8 100644 --- a/packages/session-backends/sqlite-node/test/repo.test.ts +++ b/packages/session-backends/sqlite-node/test/repo.test.ts @@ -236,7 +236,7 @@ describe("SqliteSessionRepo", () => { }); }); - it("forks at a serialized boundary for an open source session", async () => { + it("does not copy usage ledger rows when forking", async () => { await withTempDir(async (directory) => { const repo = new SqliteSessionRepo({ directory, @@ -244,125 +244,6 @@ describe("SqliteSessionRepo", () => { now: () => 1_700_000_000_000, }); const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT); - const applicationValue = storedValues.value("test.application.value"); - const applicationList = storedValues.list("test.application.list"); - const firstCommit = source.mutate( - "main", - (mutator) => - mutator.commit( - [ - sessionWrites.insertEntry({ id: "root", parentId: null, type: "custom", customType: "root" }), - storedValues.setValue(storedValues.laneLeaf("main"), "root"), - storedValues.setValue(applicationValue, "excluded"), - storedValues.appendList(applicationList, "excluded"), - ], - BACKGROUND_CONTEXT, - ), - BACKGROUND_CONTEXT, - ); - const forkPromise = repo.fork(source.metadata, { id: "fork" }, BACKGROUND_CONTEXT); - const secondCommit = source.mutate( - "main", - (mutator) => - mutator.commit( - [ - sessionWrites.insertEntry({ id: "child", parentId: "root", type: "custom", customType: "child" }), - storedValues.setValue(storedValues.laneLeaf("main"), "child"), - ], - BACKGROUND_CONTEXT, - ), - BACKGROUND_CONTEXT, - ); - - const [, fork] = await Promise.all([firstCommit, forkPromise]); - await secondCommit; - - expect(await fork.getLeafId(BACKGROUND_CONTEXT)).toBe("root"); - expect((await fork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map((entry) => entry.id)).toEqual([ - "root", - ]); - expect(await fork.getValue(applicationValue, BACKGROUND_CONTEXT)).toBeUndefined(); - expect(await fork.readList(applicationList, undefined, BACKGROUND_CONTEXT)).toEqual([]); - await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]); - }); - }); - - it("forks the whole open tree with every configured lane", async () => { - await withTempDir(async (directory) => { - const repo = new SqliteSessionRepo({ - directory, - databaseFactory: createNodeSqliteFactory(), - now: () => 1_700_000_000_000, - }); - const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT); - const configuration = { - model: { provider: "test", modelId: "model" }, - thinkingLevel: "off" as const, - activeToolNames: [], - }; - await source.mutate( - "main", - (mutator) => - mutator.commit( - [ - sessionWrites.insertEntry({ id: "root", parentId: null, type: "custom", customType: "root" }), - sessionWrites.insertEntry({ id: "child", parentId: "root", type: "custom", customType: "child" }), - sessionWrites.insertEntry({ - id: "sibling", - parentId: "root", - type: "custom", - customType: "sibling", - }), - storedValues.setValue(storedValues.laneLeaf("main"), "child"), - storedValues.setValue(storedValues.laneLeaf("review"), "sibling"), - storedValues.setValue(storedValues.laneConfig("review"), configuration), - storedValues.setValue(storedValues.laneState("review"), { - currentOperationId: null, - pendingNextRun: [], - }), - ], - BACKGROUND_CONTEXT, - ), - BACKGROUND_CONTEXT, - ); - - const fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT); - - expect((await fork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id)).toEqual([ - "root", - "child", - "sibling", - ]); - expect(await fork.getLeafId(BACKGROUND_CONTEXT)).toBe("child"); - expect(await fork.view("review").getLeafId(BACKGROUND_CONTEXT)).toBe("sibling"); - expect((await fork.getValue(storedValues.laneState("review"), BACKGROUND_CONTEXT))?.value).toEqual({ - currentOperationId: null, - pendingNextRun: [], - }); - - const branchFork = await repo.fork(source.metadata, { id: "branch", entryId: "root" }, BACKGROUND_CONTEXT); - expect((await branchFork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id)).toEqual([ - "root", - ]); - expect(await branchFork.getLeafId(BACKGROUND_CONTEXT)).toBe("root"); - await Promise.all([ - source.close(BACKGROUND_CONTEXT), - fork.close(BACKGROUND_CONTEXT), - branchFork.close(BACKGROUND_CONTEXT), - ]); - }); - }); - - it("forks one branch with scoped values and a zero ledger", async () => { - await withTempDir(async (directory) => { - const repo = new SqliteSessionRepo({ - directory, - databaseFactory: createNodeSqliteFactory(), - now: () => 1_700_000_000_000, - }); - const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT); - const applicationValue = storedValues.value("test.application.value"); - const applicationList = storedValues.list("test.application.list"); await source.mutate( "main", (mutator) => @@ -375,30 +256,7 @@ describe("SqliteSessionRepo", () => { type: "message", message: { role: "user", content: "child", timestamp: 1 }, }), - sessionWrites.insertEntry({ - id: "sibling", - parentId: "root", - type: "custom", - customType: "sibling", - }), storedValues.setValue(storedValues.laneLeaf("main"), "child"), - storedValues.setValue(storedValues.sessionName, "source name"), - storedValues.setValue(storedValues.entryLabel("root"), "root label"), - storedValues.setValue(storedValues.entryLabel("sibling"), "sibling label"), - storedValues.setValue(applicationValue, "excluded"), - storedValues.appendList(applicationList, "excluded"), - storedValues.setValue(storedValues.laneLastResult("main"), { - operationId: "previous", - kind: "navigation", - leafId: "child", - oldLeafId: "root", - outcome: "completed", - }), - storedValues.setValue(storedValues.pendingEntry("pending"), { - type: "custom", - customType: "pending", - }), - storedValues.setValue(storedValues.operationToolMemo("operation", "invocation", "memo"), true), sessionWrites.insertUsage({ id: "usage", adjustment: false, @@ -420,35 +278,9 @@ describe("SqliteSessionRepo", () => { await source.close(BACKGROUND_CONTEXT); const fork = await repo.fork(source.metadata, { id: "fork", entryId: "child" }, BACKGROUND_CONTEXT); - expect((await fork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map((entry) => entry.id)).toEqual([ - "root", - "child", - ]); - expect(await fork.getLeafId(BACKGROUND_CONTEXT)).toBe("child"); - expect(await fork.getName(BACKGROUND_CONTEXT)).toBe("source name"); - expect(await fork.getLabel("root", BACKGROUND_CONTEXT)).toBe("root label"); - expect(await fork.getLabel("sibling", BACKGROUND_CONTEXT)).toBeUndefined(); - expect(await fork.getValue(applicationValue, BACKGROUND_CONTEXT)).toBeUndefined(); - expect(await fork.readList(applicationList, undefined, BACKGROUND_CONTEXT)).toEqual([]); - expect(await fork.getValue(storedValues.laneLastResult("main"), BACKGROUND_CONTEXT)).toBeUndefined(); - expect(await fork.getValue(storedValues.pendingEntry("pending"), BACKGROUND_CONTEXT)).toBeUndefined(); - expect( - await fork.getValue(storedValues.operationToolMemo("operation", "invocation", "memo"), BACKGROUND_CONTEXT), - ).toBeUndefined(); await withDb(fork.metadata.path, (db) => { expect(sql`SELECT COUNT(*) AS count FROM usage_ledger`.get<{ count: number }>(db)).toEqual({ count: 0 }); }); - expect(await fork.getStats(BACKGROUND_CONTEXT)).toEqual({ - messageCount: 1, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - }); await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]); }); }); diff --git a/packages/session-backends/sqlite-node/test/storage-conformance.test.ts b/packages/session-backends/sqlite-node/test/storage-conformance.test.ts index 2d9e132cb..dc43445a9 100644 --- a/packages/session-backends/sqlite-node/test/storage-conformance.test.ts +++ b/packages/session-backends/sqlite-node/test/storage-conformance.test.ts @@ -18,16 +18,12 @@ const EMPTY_USAGE = { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }; -// Re-enable each case as the work-in-progress backend implements the missing contract behavior. -const DISABLED_CASES = new Set(["branch queries/applies stops before filters and cursors before limits"]); - function registerConformance(name: string, cases: readonly ConformanceCase[]): void { describe(name, () => { for (const group of new Set(cases.map((testCase) => testCase.group))) { describe(group, () => { for (const testCase of cases.filter((candidate) => candidate.group === group)) { - const register = DISABLED_CASES.has(`${group}/${testCase.name}`) ? it.skip : it; - register(testCase.name, () => testCase.run()); + it(testCase.name, () => testCase.run()); } }); }