/// /** Exercises dialog refusal and cancellation at transaction boundaries. */ import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import type { BuiltInLLMMessage, CellScope } from "@commonfabric/api"; import { Identity } from "@commonfabric/identity"; import { LLMClient, type LLMResponse } from "@commonfabric/llm"; import { llmDialog } from "../../src/builtins/llm-dialog.ts"; import { LLMMessageSchema } from "../../src/builtins/llm-schemas.ts"; import type { Cell } from "../../src/cell.ts"; import { stampWaveRunContext, WaveAccumulator, waveSettlementOf, } from "../../src/executor/wave.ts"; import { Runtime } from "../../src/runtime.ts"; import { StorageManager } from "../../src/storage/cache.deno.ts"; import type { IExtendedStorageTransaction } from "../../src/storage/interface.ts"; const signer = await Identity.fromPassphrase("dialog transaction lifecycle"); /** Constructs one raw node with real cells and controllable model work. */ async function fixture( served = true, inputScope: CellScope = "space", bindingScope: CellScope = "space", ) { const storage = StorageManager.emulate({ as: signer }); const runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: storage, servingPosture: served, experimental: { serverExecution: served }, }); const input = runtime.getCell( signer.did(), "dialog-input", undefined, undefined, inputScope, ); const parent = runtime.getCell( signer.did(), "dialog-parent", undefined, undefined, bindingScope, ); const handlers = new Map< string, (tx: IExtendedStorageTransaction, event: any) => void >(); const cancellations: (() => void)[] = []; const requests: { response: ReturnType>; signal?: AbortSignal; }[] = []; const works: Promise[] = []; const held: ((tx?: IExtendedStorageTransaction) => unknown)[] = []; const publications: { identity: unknown; scope: CellScope }[] = []; const cells = new Map>(); let output: Cell; let changed = Promise.withResolvers(); const originalSend = LLMClient.prototype.sendRequest; const originalHandler = runtime.scheduler.addEventHandler; try { await input.sync(); await parent.sync(); runtime.scheduler.addEventHandler = (handler, ref) => { handlers.set(`${ref.scope ?? "space"}/${ref.path.at(-1)}`, handler); return () => {}; }; LLMClient.prototype.sendRequest = (_request, _partial, signal) => { const response = Promise.withResolvers(); requests.push({ response, signal }); const prior = changed; changed = Promise.withResolvers(); prior.resolve(); return response.promise; }; runtime.asyncWorkObserver = (work) => works.push(work); const { action } = llmDialog( input, (tx, result) => { output = result; publications.push({ identity: tx.tx.scopeKeyIdentity, scope: result.getAsNormalizedFullLink().scope ?? "space", }); parent.withTx(tx).set({ output: result }); }, (cancel) => cancellations.push(cancel), "dialog-test", parent, runtime, undefined, undefined, parent.key("output").getAsNormalizedFullLink(), ); return { runtime, input, parent, requests, works, held, publications, action, cancellations, get output() { return output; }, async select(scope: CellScope, identity = runtime.scopeKeyIdentity) { const tx = runtime.edit(); tx.tx.scopeKeyIdentity = identity; let messages = cells.get(scope); if (!messages) { messages = runtime.getCell( signer.did(), "messages", { type: "array", items: LLMMessageSchema }, tx, scope, ); messages.withTx(tx).set([]); cells.set(scope, messages.withTx()); } input.withTx(tx).set({ messages, builtinTools: false }); action(tx); expect((await tx.commit().settled).error).toBeUndefined(); await runtime.idle(); }, stage( scope: CellScope, content: string, identity = runtime.scopeKeyIdentity, hold = false, ) { const tx = runtime.edit(); tx.tx.scopeKeyIdentity = identity; if (hold) { const enqueue = tx.enqueuePostCommitEffect.bind(tx); tx.enqueuePostCommitEffect = (effect) => enqueue({ ...effect, flush: (committed) => { held.push((override) => effect.flush(override ?? committed)); }, }); } const handler = handlers.get(`${scope}/addMessage`); expect(handler).toBeDefined(); handler!(tx, { role: "user", content }); return tx; }, cancel(tx: IExtendedStorageTransaction, scope: CellScope) { handlers.get(`${scope}/cancelGeneration`)!(tx, undefined); action(tx); }, async issued(count: number) { while (requests.length < count) await changed.promise; }, async close() { runtime.clearSealDestination(); for (const request of requests) { request.response.resolve({ role: "assistant", content: "cleanup", id: "cleanup", }); } try { await runtime.settled(); } finally { try { await runtime.dispose({ closeStorage: false }); } finally { try { await storage.close(); } finally { LLMClient.prototype.sendRequest = originalSend; runtime.scheduler.addEventHandler = originalHandler; } } } }, }; } catch (error) { try { await runtime.dispose({ closeStorage: false }); } finally { try { await storage.close(); } finally { LLMClient.prototype.sendRequest = originalSend; runtime.scheduler.addEventHandler = originalHandler; } } throw error; } } /** Abandons a request after its transaction rolls back. */ function refuse(tx: IExtendedStorageTransaction) { const reason = { name: "StorageTransactionAborted", message: "Test refusal", reason: "Test refusal", } as const; tx.abort(reason); tx.abandonStagedWork(reason); } describe("llm-dialog-served", () => { it("settles an initially refused turn without adding a transcript entry", async () => { const f = await fixture(); try { await f.select("user"); const tx = f.stage("user", "refused"); refuse(tx); await f.runtime.settled(); expect(f.requests).toHaveLength(0); expect(f.output.key("pending").get()).toBe(false); expect( f.input.key("messages").asSchema({ type: "array", items: LLMMessageSchema, }).get(), ).toEqual([]); } finally { await f.close(); } }); it("keeps a newer scope selected when an older turn is refused", async () => { const f = await fixture(); try { await f.select("space"); const tx = f.stage("space", "refused"); await f.select("user"); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink().scope, ).toBe("user"); refuse(tx); await f.runtime.settled(); expect(f.requests).toHaveLength(0); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink().scope, ).toBe("user"); } finally { await f.close(); } }); it("retains a live turn when its cancellation wave is withdrawn", async () => { const f = await fixture(); const wave = new WaveAccumulator({ space: signer.did(), basisSeq: 0, scopeKeyIdentity: f.runtime.scopeKeyIdentity, replicaFor: (space) => f.runtime.storageManager.open(space).replica, }); try { await f.select("user"); const start = f.stage("user", "accepted"); expect((await start.commit().settled).error).toBeUndefined(); await start.postCommitEffectsSettled(); await f.issued(1); f.runtime.installSealDestination({ seal: (tx) => wave.seal(tx), deferSealedEffects: () => true, }); const cancel = f.runtime.edit(); stampWaveRunContext(cancel, { actionId: "withdrawn-cancel", kind: "derivation", }); f.cancel(cancel, "user"); expect((await cancel.commit().settled).error).toBeUndefined(); f.runtime.clearSealDestination(); wave.abandon("Withdraw cancellation"); await waveSettlementOf(cancel); expect(f.requests[0].signal?.aborted).toBe(false); f.requests[0].response.resolve({ role: "assistant", content: "accepted reply", id: "accepted", }); await f.runtime.settled(); expect(f.output.key("pending").get()).toBe(false); expect( f.input.key("messages").asSchema({ type: "array", items: LLMMessageSchema, }).get()?.at(-1)?.content, ).toBe("accepted reply"); } finally { wave.abandon("Fixture complete"); await wave.settled(); await f.close(); } }); for (const served of [false, true]) { it(`returns to the existing output scope with execution ${served ? "on" : "off"}`, async () => { const f = await fixture(served); try { await f.select("space"); const first = f.stage("space", "first"); expect((await first.commit().settled).error).toBeUndefined(); await first.postCommitEffectsSettled(); await f.issued(1); await f.select("user"); const second = f.stage("user", "second"); expect((await second.commit().settled).error).toBeUndefined(); await second.postCommitEffectsSettled(); await f.issued(2); await f.select("space"); expect(f.requests.map((request) => request.signal?.aborted)).toEqual([ false, false, ]); f.requests[1].response.resolve({ role: "assistant", content: "second reply", id: "second", }); f.requests[0].response.resolve({ role: "assistant", content: "first reply", id: "first", }); await f.runtime.settled(); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink() .scope, ).toBe("space"); expect(f.output.key("pending").get()).toBe(false); expect( f.input.key("messages").asSchema({ type: "array", items: LLMMessageSchema, }).get()?.at(-1)?.content, ).toBe("first reply"); } finally { await f.close(); } }); } it("aborts every live turn when one teardown transaction throws", async () => { const f = await fixture(); const edit = f.runtime.edit; try { for (const scope of ["space", "user"] as const) { await f.select(scope); const tx = f.stage(scope, scope); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); } await f.issued(2); let attempts = 0; f.runtime.edit = (...args) => { if (++attempts === 1) throw new Error("teardown failure"); return edit.apply(f.runtime, args); }; f.cancellations[0](); expect(attempts).toBe(2); expect(f.requests.map((request) => request.signal?.aborted)).toEqual([ true, true, ]); } finally { f.runtime.edit = edit; await f.close(); } }); it("retains async ownership until a failed model turn finishes its error write", async () => { const f = await fixture(); const originalEdit = f.runtime.editWithRetry; const edit = originalEdit.bind(f.runtime); const entered = Promise.withResolvers(); const release = Promise.withResolvers(); try { await f.select("user"); const tx = f.stage("user", "fails"); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); await f.issued(1); f.runtime.editWithRetry = (fn, maxRetries, options) => { entered.resolve(); return release.promise.then(() => edit(fn, maxRetries, options)); }; let settled = false; f.works[0].then(() => { settled = true; }); f.requests[0].response.reject(new Error("model failed")); await entered.promise; await clock.settle(); expect(settled).toBe(false); release.resolve(); await f.works[0]; expect(f.output.key("pending").get()).toBe(false); expect( f.input.key("messages").asSchema({ type: "array", items: LLMMessageSchema, }).get()?.at(-1)?.content, ).toContain("model failed"); } finally { release.resolve(); f.runtime.editWithRetry = originalEdit; await f.close(); } }); it("preserves another actor's accepted selection of a shared physical binding", async () => { const f = await fixture(true, "user"); const alice = { principal: (await Identity.fromPassphrase("dialog-alice")).did(), sessionId: "alice-one", }; const bob = { principal: (await Identity.fromPassphrase("dialog-bob")).did(), sessionId: "bob-one", }; try { await f.select("user", alice); const old = f.stage("user", "refused", alice); await f.select("session", bob); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink().scope, ).toBe("session"); refuse(old); await f.runtime.settled(); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink().scope, ).toBe("session"); } finally { await f.close(); } }); it("restores a refused actor's distinct binding to a shared result", async () => { const f = await fixture(true, "space", "user"); const alice = { principal: (await Identity.fromPassphrase("dialog-alice")).did(), sessionId: "alice-one", }; const bob = { principal: (await Identity.fromPassphrase("dialog-bob")).did(), sessionId: "bob-one", }; try { await f.select("space", alice); const clear = f.runtime.edit(); clear.tx.scopeKeyIdentity = alice; f.parent.withTx(clear).set(undefined); expect((await clear.commit().settled).error).toBeUndefined(); const old = f.stage("space", "refused", alice); f.action(old); await f.select("space", bob); refuse(old); await f.runtime.settled(); expect(f.publications.at(-1)).toEqual({ identity: alice, scope: "space", }); } finally { await f.close(); } }); it("does not dispatch an accepted turn after graph teardown", async () => { const f = await fixture(); try { await f.select("user"); const tx = f.stage("user", "held", undefined, true); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); expect(f.requests).toHaveLength(0); f.cancellations[0](); await f.runtime.settled(); expect(f.output.key("pending").get()).toBe(false); await f.held[0](); await clock.settle(); expect(f.requests).toHaveLength(0); expect(f.output.key("pending").get()).toBe(false); } finally { await f.close(); } }); for (const activePredecessor of [false, true]) { it(`retains an accepted local turn beyond the heartbeat age${activePredecessor ? " while its predecessor settles" : ""}`, async () => { const f = await fixture(); try { await f.select("user"); if (activePredecessor) { const predecessor = f.stage("user", "predecessor"); expect((await predecessor.commit().settled).error).toBeUndefined(); await predecessor.postCommitEffectsSettled(); await f.issued(1); const cancel = f.runtime.edit(); f.cancel(cancel, "user"); expect((await cancel.commit().settled).error).toBeUndefined(); } const first = f.stage("user", "held", undefined, true); expect((await first.commit().settled).error).toBeUndefined(); await first.postCommitEffectsSettled(); await clock.tick(6 * 60 * 1000); const second = f.stage("user", "ignored", undefined, true); expect(second.getCfcState().outbox).toHaveLength(0); expect((await second.commit().settled).error).toBeUndefined(); await f.held[0](); await f.issued(activePredecessor ? 2 : 1); } finally { await f.close(); } }); } it("clears an accepted claim when its provider release check rejects", async () => { const f = await fixture(); try { await f.select("user"); const tx = f.stage("user", "rejected release", undefined, true); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); expect(f.output.key("pending").get()).toBe(true); let rejected = 0; await f.held[0]({ getCfcState: () => ({ writePolicyInputs: [], prepare: { status: "prepared", digest: "test", input: { writePolicyInputs: [] }, }, }), noteCfcSinkReleaseReject: () => rejected++, } as unknown as IExtendedStorageTransaction); await f.runtime.settled(); expect(rejected).toBe(1); expect(f.requests).toHaveLength(0); expect(f.output.key("pending").get()).toBe(false); const edit = f.runtime.edit; let cleanup = 0; f.runtime.edit = (...args) => { cleanup++; return edit.apply(f.runtime, args); }; try { f.cancellations[0](); expect(cleanup).toBe(0); } finally { f.runtime.edit = edit; } } finally { await f.close(); } }); it("clears a handler claim accepted after the graph stops", async () => { const f = await fixture(); try { await f.select("user"); const tx = f.stage("user", "late acceptance", undefined, true); f.cancellations[0](); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); await f.runtime.settled(); expect(f.output.key("pending").get()).toBe(false); await f.held[0](); expect(f.requests).toHaveLength(0); } finally { await f.close(); } }); it("aborts a teardown transaction whose stamping fails", async () => { const f = await fixture(); const edit = f.runtime.edit; const stamp = f.runtime.stampServerRun; let cleanupTx: IExtendedStorageTransaction | undefined; let aborted = false; try { await f.select("user"); const tx = f.stage("user", "active"); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); await f.issued(1); f.runtime.edit = (...args) => { cleanupTx = edit.apply(f.runtime, args); const abort = cleanupTx.abort.bind(cleanupTx); cleanupTx.abort = (reason) => { aborted = true; return abort(reason); }; return cleanupTx; }; f.runtime.stampServerRun = () => { throw new Error("stamp failed"); }; f.cancellations[0](); expect(cleanupTx).toBeDefined(); expect(aborted).toBe(true); expect(f.requests[0].signal?.aborted).toBe(true); } finally { f.runtime.edit = edit; f.runtime.stampServerRun = stamp; await f.close(); } }); it("preserves refusal ownership when a different-target publication is withdrawn", async () => { const f = await fixture(); const wave = new WaveAccumulator({ space: signer.did(), basisSeq: 0, scopeKeyIdentity: f.runtime.scopeKeyIdentity, replicaFor: (space) => f.runtime.storageManager.open(space).replica, }); try { await f.select("user"); const userInputs = f.input.get(); await f.select("space"); const old = f.stage("space", "old refusal"); f.runtime.installSealDestination({ seal: (tx) => wave.seal(tx), deferSealedEffects: () => true, }); const publish = f.runtime.edit(); stampWaveRunContext(publish, { actionId: "withdrawn-publication", kind: "derivation", }); f.input.withTx(publish).set(userInputs); f.action(publish); expect((await publish.commit().settled).error).toBeUndefined(); f.runtime.clearSealDestination(); wave.abandon("Withdraw publication"); await waveSettlementOf(publish); const before = f.publications.length; refuse(old); await f.runtime.settled(); expect(f.publications).toHaveLength(before + 1); expect(f.publications.at(-1)?.scope).toBe("space"); } finally { wave.abandon("Fixture complete"); await wave.settled(); await f.close(); } }); it("lets an accepted refusal announcement supersede an older target", async () => { const f = await fixture(true, "user"); const alice = { principal: (await Identity.fromPassphrase("dialog-alice")).did(), sessionId: "alice-one", }; const bob = { principal: (await Identity.fromPassphrase("dialog-bob")).did(), sessionId: "bob-one", }; try { await f.select("session", bob); await f.select("user", alice); const older = f.stage("user", "older", alice); const newer = f.stage("session", "newer", bob); f.action(newer); refuse(newer); await f.runtime.settled(); const before = f.publications.length; refuse(older); await f.runtime.settled(); expect(f.publications).toHaveLength(before); expect( f.parent.key("output").resolveAsCell().getAsNormalizedFullLink().scope, ).toBe("session"); } finally { await f.close(); } }); it("keeps a dispatched turn when a duplicate release check rejects", async () => { const f = await fixture(); try { await f.select("user"); const tx = f.stage("user", "held", undefined, true); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); await f.held[0](); await f.issued(1); let rejected = 0; await f.held[0]({ getCfcState: () => ({ writePolicyInputs: [], prepare: { status: "prepared", digest: "test", input: { writePolicyInputs: [] }, }, }), noteCfcSinkReleaseReject: () => rejected++, } as unknown as IExtendedStorageTransaction); await clock.settle(); expect(rejected).toBe(1); expect(f.output.key("pending").get()).toBe(true); expect(f.requests[0].signal?.aborted).toBe(false); f.requests[0].response.resolve({ role: "assistant", content: "accepted reply", id: "accepted", }); await f.runtime.settled(); expect(f.output.key("pending").get()).toBe(false); expect( f.input.key("messages").asSchema({ type: "array", items: LLMMessageSchema, }).get()?.at(-1)?.content, ).toBe("accepted reply"); } finally { await f.close(); } }); it("recovers a pending dialog with no durable claim", async () => { const f = await fixture(); try { await f.select("user"); const pending = f.runtime.edit(); f.output.withTx(pending).key("pending").set(true); expect((await pending.commit().settled).error).toBeUndefined(); const tx = f.stage("user", "recovered"); expect(tx.getCfcState().outbox).toHaveLength(1); expect((await tx.commit().settled).error).toBeUndefined(); await tx.postCommitEffectsSettled(); await f.issued(1); } finally { await f.close(); } }); });