import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { stub } from "@std/testing/mock"; import { createSession, Identity } from "@commonfabric/identity"; import { getPatternIdentityRef, Runtime, type RuntimeProgram, } from "@commonfabric/runner"; import { StorageManager } from "@commonfabric/runner/storage/cache.deno"; import { readStoredCfcMetadata } from "@commonfabric/runner/cfc"; import { loadCompiledClosure, loadVerifiedSourceClosure, setCompileCacheRuntimeVersionForTesting, sourceDocKey, } from "../../runner/src/compilation-cache/cell-cache.ts"; import { PiecesController } from "../src/ops/pieces-controller.ts"; const signer = await Identity.fromPassphrase("setsrc module delegation"); function authorizedWriterProgram(version: string): RuntimeProgram { return { main: "/app/main.tsx", files: [ { name: "/app/main.tsx", contents: `/// import { handler, pattern, Writable, WriteAuthorizedBy, } from "commonfabric"; import { revision } from "../shared/revision.ts"; const setName = handler< { name: string }, { name: Writable } >((event, state) => { state.name.set(revision + ":" + event.name); }); export default pattern<{ seed?: string }>(() => { const name = new Writable< WriteAuthorizedBy >("initial").for("name"); return { name, setName: setName({ name }) }; }); `, }, { name: "/shared/revision.ts", contents: `/// export const revision = ${JSON.stringify(version)}; `, }, ], }; } /** * The same contract in two revisions, with a confidential argument. The label * is what makes the piece's argument document CFC-relevant, so a transaction * reading it carries that clause in its flow join; the two revisions declare * the same label, so nothing about the swap itself weakens anything. */ function confidentialArgumentProgram(version: string): RuntimeProgram { return { main: "/main.tsx", files: [{ name: "/main.tsx", contents: `/// import { Confidential, NAME, pattern } from "commonfabric"; const ATOM = { type: "https://commonfabric.org/cfc/atom/Resource", class: "SetsrcDelegationCheck", subject: "did:example:owner", } as const; type Label = readonly [typeof ATOM]; interface Args { seed: Confidential; } export default pattern(({ seed }) => ({ [NAME]: "Delegation check", label: ${JSON.stringify(version)} + ":" + seed, })); `, }], }; } describe("setsrc module delegation", () => { let storageManager: ReturnType; let runtime: Runtime; let pieces: PiecesController; beforeEach(async () => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager, }); const session = createSession({ identity: signer, spaceDid: await runtime.createSpace(), }); pieces = new PiecesController(session, runtime); await pieces.synced(); }); afterEach(async () => { await runtime?.dispose(); await storageManager?.close(); }); it("keeps a preview and a rejected setup from publishing successor authority", async () => { const piece = await pieces.create(authorizedWriterProgram("v1"), { input: {}, }); const previous = getPatternIdentityRef(piece.getCell())!; const next = authorizedWriterProgram("v2"); const report = await piece.checkPattern(next); expect(report.compatible).toBe(true); const delegated = () => { const tx = runtime.edit(); try { return tx.getCfcState().moduleDelegations.get(pieces.getSpace()) ?.get(report.candidate.identity) ?? []; } finally { tx.abort(); } }; expect(delegated()).not.toContain(previous.identity); const prepare = runtime.prepareTxForCommit.bind(runtime); let refusedSetup = false; { using _rejectSetup = stub(runtime, "prepareTxForCommit", (tx) => { const staged = getPatternIdentityRef(piece.getCell().withTx(tx)); if (staged?.identity === report.candidate.identity) { refusedSetup = true; expect( tx.getCfcState().moduleDelegations.get(pieces.getSpace()) ?.get(report.candidate.identity), ).toContain(previous.identity); expect(delegated()).not.toContain(previous.identity); tx.abort("injected setup refusal"); throw new Error("injected setup refusal"); } prepare(tx); }); await expect(piece.setPattern(next)).rejects.toThrow( "injected setup refusal", ); } expect(refusedSetup).toBe(true); expect(getPatternIdentityRef(piece.getCell())).toEqual(previous); expect(delegated()).not.toContain(previous.identity); const tx = runtime.edit(); try { const closure = await loadVerifiedSourceClosure( runtime, pieces.getSpace(), report.candidate.identity, tx, ); expect( closure?.get(report.candidate.identity)?.delegatedModuleIdentities ?? [], ) .not.toContain(previous.identity); } finally { tx.abort(); } expect(delegated()).not.toContain(previous.identity); await piece.setPattern(next); expect(delegated()).toContain(previous.identity); const result = await piece.result.getCell(); result.key("setName").send({ name: "accepted" }); await result.pull(); expect(await piece.result.get(["name"])).toBe("v2:accepted"); }); it("publishes authority only after an incompatible source change is confirmed", async () => { const program = (version: string, seedType: string): RuntimeProgram => ({ main: "/api/patterns/confirm-authority.tsx", files: [{ name: "/api/patterns/confirm-authority.tsx", contents: `/// import { handler, pattern, Writable, WriteAuthorizedBy } from "commonfabric"; const setName = handler<{name: string}, {name: Writable}>( (event, state) => state.name.set(${ JSON.stringify(version) } + ":" + event.name) ); export default pattern<{seed?: ${seedType}}>(() => { const name = new Writable>("initial").for("name"); return {name, setName: setName({name})}; });`, }], }); const piece = await pieces.create(program("old", "string"), { input: {} }); const previous = getPatternIdentityRef(piece.getCell())!; const candidate = program("confirmed", "number"); using _fetch = stub(globalThis, "fetch", () => Promise.resolve( new Response(candidate.files[0].contents, { headers: { "content-type": "text/typescript-jsx" }, }), )); const action = { kind: "repoint" as const, url: "system:confirm-authority.tsx", }; const warning = await piece.changeSource(action); expect(warning.status).toBe("incompatible"); if (warning.status !== "incompatible") { throw new Error("expected compatibility confirmation"); } const delegated = () => { const tx = runtime.edit(); try { return tx.getCfcState().moduleDelegations.get(pieces.getSpace()) ?.get(warning.prepared.candidate.identity) ?? []; } finally { tx.abort(); } }; expect(delegated()).not.toContain(previous.identity); expect( (await piece.changeSource(action, { confirmedChange: warning.prepared })) .status, ) .toBe("applied"); expect(delegated()).toContain(previous.identity); const result = await piece.result.getCell(); result.key("setName").send({ name: "accepted" }); await result.pull(); expect(await piece.result.get(["name"])).toBe("confirmed:accepted"); }); it("preserves both predecessor chains when concurrent updates share a successor", async () => { const first = await pieces.create(authorizedWriterProgram("a"), { input: {}, }); const second = await pieces.create(authorizedWriterProgram("b"), { input: {}, }); const predecessors = [first, second].map((piece) => getPatternIdentityRef(piece.getCell())!.identity ); await Promise.all([ first.setPattern(authorizedWriterProgram("shared")), second.setPattern(authorizedWriterProgram("shared")), ]); const successor = getPatternIdentityRef(first.getCell())!; expect(getPatternIdentityRef(second.getCell())).toEqual(successor); const tx = runtime.edit(); try { const source = await loadVerifiedSourceClosure( runtime, pieces.getSpace(), successor.identity, tx, ); for (const predecessor of predecessors) { expect(source?.get(successor.identity)?.delegatedModuleIdentities) .toContain(predecessor); } } finally { tx.abort(); } for (const piece of [first, second]) { const result = await piece.result.getCell(); result.key("setName").send({ name: "accepted" }); await result.pull(); expect(await piece.result.get(["name"])).toBe("shared:accepted"); } }); describe("in a runtime that evaluated the successor before another runtime updated the source", () => { // `runtime` performs the update. `observer` shares its store but // evaluated the successor for a piece of its own first, so it resolves // the updated piece's pattern from memory rather than from storage. let observer: Runtime; let observerPieces: PiecesController; beforeEach(async () => { observer = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager, }); observerPieces = new PiecesController( createSession({ identity: signer, spaceDid: pieces.getSpace(), }), observer, ); await observerPieces.synced(); await observerPieces.create(authorizedWriterProgram("v2"), { input: {}, }); }); afterEach(async () => { // The storage manager belongs to `runtime`, which the outer `afterEach` // disposes after this one. await observer.dispose({ closeStorage: false }); }); const invokeSetName = async ( piece: Awaited>, name: string, ): Promise => { const result = await piece.result.getCell(); result.key("setName").send({ name }); await result.pull(); return await piece.result.get(["name"]); }; it("authorizes the successor's writes to a piece it swaps over", async () => { const piece = await pieces.create(authorizedWriterProgram("v1"), { input: {}, }); const observed = await observerPieces.get(piece.id, true); expect(await invokeSetName(observed, "before")).toBe("v1:before"); await piece.setPattern(authorizedWriterProgram("v2")); await observer.idle(); // The swap waits on the read of the successor's authority, which the // watcher tracks apart from the scheduler. await observer.runner.idlePointerMaintenance(); expect(await invokeSetName(observed, "after")).toBe("v2:after"); }); it("authorizes the successor's writes to a piece it starts afterwards", async () => { const piece = await pieces.create(authorizedWriterProgram("v1"), { input: {}, }); const updater = await pieces.get(piece.id, true); expect(await invokeSetName(updater, "before")).toBe("v1:before"); await piece.setPattern(authorizedWriterProgram("v2")); await pieces.synced(); const observed = await observerPieces.get(piece.id, true); expect(await invokeSetName(observed, "after")).toBe("v2:after"); }); it("authorizes the successor's writes for every earlier pattern in the piece's history, not just the latest", async () => { // `observer` first registers v2's grant from v1 through one piece. A // second piece then goes v0 → v1 → v2, which extends v2's stored // grants to v0 as well; its field is still bound to v0. const first = await pieces.create(authorizedWriterProgram("v1"), { input: {}, }); await first.setPattern(authorizedWriterProgram("v2")); await pieces.synced(); const firstObserved = await observerPieces.get(first.id, true); expect(await invokeSetName(firstObserved, "first")).toBe("v2:first"); const second = await pieces.create(authorizedWriterProgram("v0"), { input: {}, }); const updater = await pieces.get(second.id, true); expect(await invokeSetName(updater, "before")).toBe("v0:before"); await second.setPattern(authorizedWriterProgram("v1")); await second.setPattern(authorizedWriterProgram("v2")); await pieces.synced(); const observed = await observerPieces.get(second.id, true); expect(await invokeSetName(observed, "after")).toBe("v2:after"); }); }); it("merges predecessor chains into an already-stored successor closure", async () => { const first = await pieces.create(authorizedWriterProgram("v1"), { input: {}, }); const second = await pieces.create(authorizedWriterProgram("v4"), { input: {}, }); const firstRef = getPatternIdentityRef(first.getCell())!; const secondRef = getPatternIdentityRef(second.getCell())!; const invokeSetName = async ( piece: typeof first, name: string, ): Promise => { const result = await piece.result.getCell(); result.key("setName").send({ name }); await result.pull(); return await piece.result.get(["name"]) as string; }; expect(await invokeSetName(first, "before")).toBe("v1:before"); await first.setPattern(authorizedWriterProgram("v2")); const intermediateRef = getPatternIdentityRef(first.getCell())!; expect(await invokeSetName(first, "middle")).toBe("v2:middle"); await first.setPattern(authorizedWriterProgram("v3")); expect(await invokeSetName(first, "after")).toBe("v3:after"); const successorRef = getPatternIdentityRef(first.getCell())!; await runtime.patternManager.flushCompileCacheWrites(); await pieces.synced(); const patternSpace = pieces.getSpace(); const loadClosure = async (targetRuntime: Runtime, identity: string) => { const tx = targetRuntime.edit(); try { return await loadVerifiedSourceClosure( targetRuntime, patternSpace, identity, tx, ); } finally { tx.abort(); } }; const freshRuntimes: Runtime[] = []; try { const createFreshRuntime = () => { const freshRuntime = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager, }); // Runtime.dispose() closes the shared emulated storage manager, so all // cold-start runtimes stay alive until the assertions are complete. freshRuntimes.push(freshRuntime); return freshRuntime; }; const createFreshPieces = async (freshRuntime: Runtime) => { const freshSession = createSession({ identity: signer, spaceDid: patternSpace, }); const freshPieces = new PiecesController(freshSession, freshRuntime); await freshPieces.synced(); return freshPieces; }; // Restart before the second pattern converges on the already-stored // successor. Use a fresh compiled-cache variant so the source document // contributes the first pattern's chain while the second update // contributes the new chain. Both sets must persist their authenticated // union, and that committed union must be installed in this runtime. const bumpedRuntimeVersion = "setsrc-delegation-shared-successor"; const restoreRuntimeVersion = setCompileCacheRuntimeVersionForTesting( bumpedRuntimeVersion, ); try { const mergeRuntime = createFreshRuntime(); const mergePieces = await createFreshPieces(mergeRuntime); const mergeSecond = await mergePieces.get(second.id, true); await mergeSecond.setPattern(authorizedWriterProgram("v3")); expect(getPatternIdentityRef(mergeSecond.getCell())).toEqual( successorRef, ); const registeredTx = mergeRuntime.edit(); const registeredDelegations = registeredTx.getCfcState() .moduleDelegations.get(patternSpace)?.get(successorRef.identity) ?? []; registeredTx.abort(); expect(registeredDelegations).toContain(firstRef.identity); expect(registeredDelegations).toContain(intermediateRef.identity); expect(registeredDelegations).toContain(secondRef.identity); // This resolves through the successor module already evaluated for // mergeSecond. It therefore depends on save-time registration of the // complete A+B union; reloading a closure cannot rescue a partial map. const mergeFirst = await mergePieces.get(first.id, true); expect(await invokeSetName(mergeFirst, "merged")).toBe("v3:merged"); await mergeRuntime.patternManager.flushCompileCacheWrites(); await mergePieces.synced(); const firstClosure = await loadClosure(mergeRuntime, firstRef.identity); const intermediateClosure = await loadClosure( mergeRuntime, intermediateRef.identity, ); const secondClosure = await loadClosure( mergeRuntime, secondRef.identity, ); const successorClosure = await loadClosure( mergeRuntime, successorRef.identity, ); const byName = (closure: NonNullable) => new Map( [...closure].map(([identity, doc]) => [doc.filename, identity]), ); const firstByName = byName(firstClosure!); const intermediateByName = byName(intermediateClosure!); const secondByName = byName(secondClosure!); for (const [identity, doc] of successorClosure!) { if (!doc.filename.startsWith("/")) continue; const delegated = doc.delegatedModuleIdentities ?? []; expect(delegated).toContain(firstByName.get(doc.filename)); expect(delegated).toContain(intermediateByName.get(doc.filename)); expect(delegated).toContain(secondByName.get(doc.filename)); expect(delegated).not.toContain(identity); } const compiledTx = mergeRuntime.edit(); try { const compiledClosure = await loadCompiledClosure( mergeRuntime, patternSpace, successorRef.identity, { runtimeVersion: bumpedRuntimeVersion }, compiledTx, ); for (const [identity, sourceDoc] of successorClosure!) { expect(compiledClosure.get(identity)?.delegatedModuleIdentities) .toEqual(sourceDoc.delegatedModuleIdentities); } } finally { compiledTx.abort(); } // A later cold runtime can warm-hit only the repaired compiled set; // both predecessor chains still have to authorize the first pattern. const coldRuntime = createFreshRuntime(); const coldPieces = await createFreshPieces(coldRuntime); const coldPiece = await coldPieces.get( first.id, true, ); expect(await invokeSetName(coldPiece, "cold")).toBe("v3:cold"); const recoveredVersion = "setsrc-delegation-source-recovery"; const restoreRecoveredVersion = setCompileCacheRuntimeVersionForTesting( recoveredVersion, ); try { const recoveredRuntime = createFreshRuntime(); const recoveredPieces = await createFreshPieces(recoveredRuntime); const recoveredPiece = await recoveredPieces.get(first.id, true); expect(await invokeSetName(recoveredPiece, "recovered")) .toBe("v3:recovered"); await recoveredRuntime.patternManager.flushCompileCacheWrites(); const recoveredTx = recoveredRuntime.edit(); try { const compiled = await loadCompiledClosure( recoveredRuntime, patternSpace, successorRef.identity, { runtimeVersion: recoveredVersion }, recoveredTx, ); for (const [identity, sourceDoc] of successorClosure!) { expect(compiled.get(identity)?.delegatedModuleIdentities) .toEqual(sourceDoc.delegatedModuleIdentities); } } finally { recoveredTx.abort(); } } finally { restoreRecoveredVersion(); } } finally { restoreRuntimeVersion(); } } finally { for (const freshRuntime of freshRuntimes.reverse()) { await freshRuntime.dispose(); } } }); it("updates a piece whose argument carries a confidentiality label", async () => { // The source transition stages the successor's delegation union on the // same transaction that moves the piece's source pointer, so that the // authority and the pointer land together. That transaction also reads the // piece's argument. Where the argument carries a label, the join reaches // the cache document the union is staged on, and measuring that write // against the ceiling a content-addressed cache document declares — none — // refuses the commit. At the strict rung that is a refused pattern update // for exactly the pieces a confidentiality label is written for. const strictStorage = StorageManager.emulate({ as: signer }); const strictRuntime = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager: strictStorage, cfcEnforcementMode: "enforce-strict", cfcFlowLabels: "persist", }); try { const strictPieces = new PiecesController( createSession({ identity: signer, spaceDid: await strictRuntime.createSpace(), }), strictRuntime, ); await strictPieces.synced(); const piece = await strictPieces.create( confidentialArgumentProgram("v1"), { input: { seed: "hello" } }, ); await strictRuntime.idle(); const previous = getPatternIdentityRef(piece.getCell())!; await piece.setPattern(confidentialArgumentProgram("v2")); await strictRuntime.idle(); // The swap happened, the argument survived it, and the successor // inherited the predecessor's writer authority. expect(await piece.result.get(["label"])).toBe("v2:hello"); const successor = getPatternIdentityRef(piece.getCell())!; expect(successor.identity).not.toBe(previous.identity); const tx = strictRuntime.edit(); try { const closure = await loadVerifiedSourceClosure( strictRuntime, strictPieces.getSpace(), successor.identity, tx, ); expect(closure?.get(successor.identity)?.delegatedModuleIdentities) .toContain(previous.identity); // The skip decides which store the value may land in, not whether the // join follows it: the successor's record carries the argument's // clause. Without this the case would pass equally if the label had // stopped reaching the write at all, which is what the refusal was // about. const recordId = strictRuntime.getCell( strictPieces.getSpace(), sourceDocKey(successor.identity), undefined, tx, ).getAsNormalizedFullLink().id; const stamped = readStoredCfcMetadata(tx, { space: strictPieces.getSpace(), id: recordId, })?.labelMap.entries ?? []; expect( stamped.flatMap((entry) => entry.label.confidentiality ?? []), ).toContainEqual({ type: "https://commonfabric.org/cfc/atom/Resource", class: "SetsrcDelegationCheck", subject: "did:example:owner", }); } finally { tx.abort(); } } finally { await strictRuntime.dispose(); await strictStorage.close(); } }); });