// The served lifecycle verbs, end to end: a serving host over a real // in-process memory server runs each verb on the space's serving runtime, // and a client runtime opened afterwards reads what the verb left in the // store. What is pinned is the store's state, never the serving runtime's // own view of it. import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { createSession, Identity, type Session } from "@commonfabric/identity"; import { streamEntriesDocId } from "@commonfabric/memory/v2"; import type { DID, MemorySpace } from "@commonfabric/memory/interface"; import type * as MemoryV2Server from "@commonfabric/memory/v2/server"; import { type Cell, entityIdFrom, getPatternIdentityRef, getPieceSourceRevisions, type NormalizedFullLink, Runtime, type RuntimeProgram, } from "@commonfabric/runner"; import type { ExecutorHost } from "@commonfabric/runner/executor/host"; import { type ServingMemoryServer, startServingMemoryServer, } from "@commonfabric/runner/executor/serving-memory-server.deno"; import { EmulatedStorageManager } from "@commonfabric/runner/storage/cache.deno"; import { loadVerifiedSourceClosure } from "../../runner/src/compilation-cache/cell-cache.ts"; import { PiecesController } from "../src/ops/pieces-controller.ts"; import { pieceId } from "../src/piece-id.ts"; import { resolveSlugTargetCell } from "../src/slugs.ts"; import { completeServedRegistration, confirmServedInstantiate, confirmServedRegistration, confirmServedSetSource, finishServedRegistration, prepareServedRegistration, servedInstantiatePiece, ServedLifecycleRefusal, type ServedPatternSource, servedSetPieceSource, type ServedSetSourceRequest, servedUploadPattern, } from "../src/ops/served-lifecycle.ts"; const spaceSigner = await Identity.fromPassphrase("served lifecycle space"); const space = spaceSigner.did() as MemorySpace; const serviceSigner = await Identity.fromPassphrase( "served lifecycle service", ); const aliceSigner = await Identity.fromPassphrase("served lifecycle alice"); function programOf(contents: string): RuntimeProgram { return { main: "/main.tsx", files: [{ name: "/main.tsx", contents }] }; } /** One optional input, one output. */ const BASE_PROGRAM = programOf([ "import { NAME, pattern } from 'commonfabric';", "export default pattern<{ seed?: string }, { label: string }>(", " ({ seed }) => ({", " [NAME]: 'Served lifecycle',", " label: seed ?? 'unset',", " }),", ");", "", ].join("\n")); const BROKEN_PROGRAM = programOf( "import { pattern } from 'commonfabric';\nexport default pattern<{}, {}>(() => ({ label: undefinedName }));\n", ); /** `BASE_PROGRAM` with its `seed` argument narrowed to a number. */ const NUMERIC_SEED_PROGRAM = programOf([ "import { NAME, pattern } from 'commonfabric';", "export default pattern<{ seed?: number }, { label: string }>(", " ({ seed }) => ({", " [NAME]: 'Served lifecycle',", " label: seed === undefined ? 'unset' : String(seed),", " }),", ");", "", ].join("\n")); /** * A handler whose write is authorized by its own module, over a field it * binds — the shape a source update must carry writer authority across. */ 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)}; `, }, ], }; } describe("served lifecycle verbs", () => { let serving: ServingMemoryServer; let server: MemoryV2Server.Server; let host: ExecutorHost; let session: Session; let cleanups: Array<() => Promise>; beforeEach(async () => { serving = await startServingMemoryServer({ apiUrl: new URL(import.meta.url), serviceIdentity: serviceSigner, policy: { flushDeadlineMs: 5_000, idleParkMs: 600_000 }, }); ({ server, host } = serving); session = createSession({ identity: serviceSigner, spaceDid: space as DID, }); cleanups = []; }); afterEach(async () => { await host.close(); for (const cleanup of cleanups.reverse()) await cleanup(); await serving.close(); }); /** A client's view of the space, opened as alice after the verb ran. */ const clientPieces = async (): Promise => { const manager = EmulatedStorageManager.connectTo(server, { as: aliceSigner, }); const runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: manager, experimental: { serverExecution: true }, }); cleanups.push(async () => { await runtime.dispose(); await manager.close(); }); const pieces = new PiecesController(session, runtime, { deferSpaceCellSync: true, }); await pieces.ready; return pieces; }; const served = ( name: string, run: (pieces: PiecesController) => Promise, confirm?: (runtime: Runtime, receipt: T) => Promise, ): Promise => host.runLifecycleVerb(space, { name, run: (runtime) => run( new PiecesController(session, runtime, { deferSpaceCellSync: true }), ), ...(confirm === undefined ? {} : { confirm }), }); const instantiate = ( source: ServedPatternSource, argument?: object, naming: { slug?: string; force?: boolean; register?: boolean; requestKey?: string; } = {}, ) => served( "instantiate", (pieces) => servedInstantiatePiece(pieces, { source, ...(argument === undefined ? {} : { argument }), ...naming, actingUser: aliceSigner.did(), }), (runtime, receipt) => confirmServedInstantiate(runtime, space, receipt, aliceSigner.did()), ); const refusalOf = async (work: Promise) => { try { await work; } catch (error) { expect(error).toBeInstanceOf(ServedLifecycleRefusal); return error as ServedLifecycleRefusal; } throw new Error("expected a refusal"); }; describe("instantiate", () => { it("refuses registration without a matching durable creation record", async () => { const created = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); const pieces = await clientPieces(); const missing = { ...created, requestKey: "absent-creation" }; for (const phase of ["prepare", "finish"] as const) { await expect( phase === "prepare" ? prepareServedRegistration(pieces, missing, aliceSigner.did()) : finishServedRegistration( pieces, { receipt: missing }, aliceSigner.did(), {}, ), ) .rejects.toThrow("Could not retain the registration"); } const record = pieces.runtime.getCell(pieces.getSpace(), { purpose: "piece-instantiation-receipt", principal: aliceSigner.did(), requestKey: missing.requestKey, }); expect(await record.pull()).toBeUndefined(); const other = await instantiate({ program: BASE_PROGRAM }); await expect( prepareServedRegistration(pieces, { ...created, pieceId: other.pieceId, }, aliceSigner.did()), ) .rejects.toThrow("Could not retain the registration"); expect(await pieces.getDefaultPattern(false)).toBeUndefined(); }); it("retains a recoverable failure when the root has no registration handler", async () => { const root = await instantiate({ program: BASE_PROGRAM }); const pending = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); const pieces = await clientPieces(); await pieces.linkDefaultPattern( (await pieces.get(root.pieceId)).getCell(), ); const prepared = await prepareServedRegistration( pieces, pending, aliceSigner.did(), ); expect(prepared.delivery).toBeUndefined(); expect(prepared.error).toBe( "The default pattern has no addPiece handler", ); const failed = await finishServedRegistration( pieces, prepared, aliceSigner.did(), {}, ); expect(failed.registration.status).toBe("failed"); expect(failed.registration.terminal).toBeUndefined(); await confirmServedRegistration( pieces.runtime, space, failed, aliceSigner.did(), ); await expect(confirmServedRegistration(pieces.runtime, space, { ...failed, requestKey: "unknown-creation", }, aliceSigner.did())).rejects.toThrow( "Registration receipt was not retained", ); await expect(confirmServedRegistration(pieces.runtime, space, { ...failed, registration: { status: "handled" }, }, aliceSigner.did())).rejects.toThrow( "Registration outcome was not retained", ); await expect(confirmServedRegistration(pieces.runtime, space, { ...failed, registration: { ...failed.registration, terminal: true }, }, aliceSigner.did())).rejects.toThrow( "Registration outcome was not retained", ); }); it("leaves skipped registration unchanged and refuses completion inside a serving wave", async () => { const skipped = await instantiate({ program: BASE_PROGRAM }); const pieces = await clientPieces(); const prepared = await prepareServedRegistration( pieces, skipped, aliceSigner.did(), ); expect( await finishServedRegistration(pieces, prepared, aliceSigner.did(), {}), ).toEqual(skipped); const pending = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); await expect( served( "unsafe-completion", (controller) => completeServedRegistration(controller, pending, aliceSigner.did()), ), ) .rejects.toThrow( "Registration completion requires a client runtime outside the serving wave", ); const incomplete = await finishServedRegistration( pieces, { receipt: pending }, aliceSigner.did(), {}, ); expect(incomplete.registration.error).toBe( "Registration has no delivery plan", ); expect(incomplete.registration.terminal).toBeUndefined(); }); it("retries a terminal registration failure after repair without creating another piece", async () => { const rootReceipt = await instantiate({ program: programOf(` import { computed, handler, pattern, Writable } from "commonfabric"; const addPiece = handler<{piece: Writable}, {blocked: Writable, panels: Writable[]>}>( ({piece}, {blocked, panels}) => { if (blocked.get()) throw new Error("Registration blocked"); panels.addUnique(piece); }, ); export default pattern(() => { const blocked = new Writable(true); const panels = new Writable[]>([]); return { blocked, panels, pieceRegistry: computed(() => panels.get().map(piece => piece)), addPiece: addPiece({blocked, panels}) }; }); `), }); const pieces = await clientPieces(); const root = await pieces.get(rootReceipt.pieceId); await pieces.linkDefaultPattern(root.getCell()); const pending = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); const events: string[] = []; const options = { append: async ( { stream, eventId, piece }: { stream: NormalizedFullLink; eventId: string; piece: Cell; }, ) => { events.push(eventId); await server.commitDelegatedAppend({ targetSpace: stream.space, targetStream: streamEntriesDocId(stream), targetStreamLink: stream, eventId, payload: { piece: piece.getAsLink() }, actingPrincipal: aliceSigner.did(), actingSession: aliceSigner.did(), capabilityRef: `stream-append:${streamEntriesDocId(stream)}`, sessionId: `repair:${crypto.randomUUID()}`, localSeq: 1, }); }, }; const failed = await completeServedRegistration( pieces, pending, aliceSigner.did(), options, ); expect(failed.registration.status).toBe("failed"); const repair = await pieces.runtime.editWithRetry((tx) => root.getCell().withTx(tx).key("blocked").set(false) ); expect(repair.error).toBeUndefined(); const retried = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, requestKey: pending.requestKey, }); const [handled, concurrent] = await Promise.all([ completeServedRegistration(pieces, retried, aliceSigner.did(), options), completeServedRegistration(pieces, retried, aliceSigner.did(), options), ]); expect(handled.registration.status).toBe("handled"); expect(concurrent.registration.status).toBe("handled"); expect(handled.pieceId).toBe(pending.pieceId); expect(events.length).toBe(3); expect(events[0]).not.toBe(events[1]); expect(events[1]).toBe(events[2]); const registry = await pieces.getRegisteredPieces(); expect(registry.length).toBe(1); }); it("retries a committed no-op registration after repair without creating another piece", async () => { const rootReceipt = await instantiate({ program: programOf(` import { computed, handler, pattern, Writable } from "commonfabric"; const addPiece = handler<{piece: Writable}, {blocked: Writable, panels: Writable[]>}>( ({piece}, {blocked, panels}) => { if (blocked.get()) return; panels.addUnique(piece); }, ); export default pattern(() => { const blocked = new Writable(true); const panels = new Writable[]>([]); return { blocked, panels, pieceRegistry: computed(() => panels.get().map(piece => piece)), addPiece: addPiece({blocked, panels}) }; }); `), }); const pieces = await clientPieces(); const root = await pieces.get(rootReceipt.pieceId); await pieces.linkDefaultPattern(root.getCell()); const pending = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); const events: string[] = []; const options = { append: async ( { stream, eventId, piece }: { stream: NormalizedFullLink; eventId: string; piece: Cell; }, ) => { events.push(eventId); await server.commitDelegatedAppend({ targetSpace: stream.space, targetStream: streamEntriesDocId(stream), targetStreamLink: stream, eventId, payload: { piece: piece.getAsLink() }, actingPrincipal: aliceSigner.did(), actingSession: aliceSigner.did(), capabilityRef: `stream-append:${streamEntriesDocId(stream)}`, sessionId: `repair:${crypto.randomUUID()}`, localSeq: 1, }); }, }; const appended = Promise.withResolvers(); const releaseAcknowledgment = Promise.withResolvers(); const uncertainCompletion = completeServedRegistration( pieces, pending, aliceSigner.did(), { append: async (input: Parameters[0]) => { await options.append(input); appended.resolve(); await releaseAcknowledgment.promise; throw new Error("Lost delayed append acknowledgment"); }, }, ); await appended.promise; const failed = await completeServedRegistration( pieces, pending, aliceSigner.did(), options, ).finally(() => releaseAcknowledgment.resolve()); const uncertain = await uncertainCompletion; expect(uncertain.registration.terminal).toBe(true); expect(failed.registration.status).toBe("failed"); expect(failed.registration.error).toBe( "The addPiece handler committed without registering the piece", ); const repair = await pieces.runtime.editWithRetry((tx) => root.getCell().withTx(tx).key("blocked").set(false) ); expect(repair.error).toBeUndefined(); const retried = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, requestKey: pending.requestKey, }); const [handled, concurrent] = await Promise.all([ completeServedRegistration(pieces, retried, aliceSigner.did(), options), completeServedRegistration(pieces, retried, aliceSigner.did(), options), ]); expect(handled.registration.status).toBe("handled"); expect(handled.registration.attempt).toBe(1); expect(concurrent.registration.status).toBe("handled"); expect(handled.pieceId).toBe(pending.pieceId); expect(events.length).toBe(4); expect(events[0]).toBe(events[1]); expect(events[1]).not.toBe(events[2]); expect(events[2]).toBe(events[3]); const registry = await pieces.getRegisteredPieces(); expect(registry.length).toBe(1); }); for ( const mode of [ "client event", "trusted delegated ingress", "lost append acknowledgment", ] ) { const delegated = mode !== "client event"; it(`registers through the computed root action using ${mode}`, async () => { // The root records the principal its `addPiece` acts for. const rootReceipt = await instantiate({ program: programOf(` import { computed, currentPrincipal, handler, pattern, Writable } from "commonfabric"; const addPiece = handler<{piece: Writable}, {panels: Writable[]>, adder: Writable}>( ({piece}, {panels, adder}) => { panels.addUnique(piece); adder.set(currentPrincipal() ?? ""); }, ); const removePiece = handler<{piece: Writable}, {panels: Writable[]>}>( ({piece}, {panels}) => { panels.set(panels.get().filter(member => !member.equals(piece))); }, ); export default pattern(() => { const panels = new Writable[]>([]); const adder = new Writable(""); return { panels, adder, pieceRegistry: computed(() => panels.get().map(piece => piece)), addPiece: addPiece({panels, adder}), removePiece: removePiece({panels}) }; }); `), }); const pieces = await clientPieces(); const root = await pieces.get(rootReceipt.pieceId); await pieces.linkDefaultPattern(root.getCell()); const pending = await instantiate( { program: BASE_PROGRAM }, undefined, { register: true }, ); let appends = 0; const options = delegated ? { append: async ( { stream, eventId, piece }: { stream: NormalizedFullLink; eventId: string; piece: Cell; }, ) => { appends++; await server.commitDelegatedAppend({ targetSpace: stream.space, targetStream: streamEntriesDocId(stream), targetStreamLink: stream, eventId, payload: { piece: piece.getAsLink() }, actingPrincipal: aliceSigner.did(), actingSession: aliceSigner.did(), capabilityRef: `stream-append:${streamEntriesDocId(stream)}`, sessionId: `lifecycle-test:${crypto.randomUUID()}`, localSeq: 1, }); if (mode === "lost append acknowledgment" && appends === 1) { throw new Error("Lost append acknowledgment"); } }, } : {}; const created = await completeServedRegistration( pieces, pending, aliceSigner.did(), options, ); if (mode === "client event") { expect(created.registration.error).toBeUndefined(); } const retried = await instantiate( { program: BASE_PROGRAM }, undefined, { requestKey: pending.requestKey, register: true }, ); expect( (await completeServedRegistration( pieces, retried, aliceSigner.did(), options, )).registration, ).toMatchObject({ status: "handled" }); if (delegated) { expect(appends).toBe(mode === "lost append acknowledgment" ? 2 : 1); } expect(created.registration.status).toBe( mode === "lost append acknowledgment" ? "failed" : "handled", ); const panels = root.getCell().asSchema({ type: "object", required: ["panels"], properties: { panels: { type: "array", items: { type: "unknown", asCell: ["cell"] }, }, }, }).key("panels"); await panels.pull(); expect(panels.get().length).toBe(1); expect( panels.key(0).resolveAsCell().equals( pieces.runtime.getCellFromEntityId( space, entityIdFrom(created.pieceId), ), ), ).toBe(true); expect( await root.getCell().asSchema({ type: "object", properties: { adder: { type: "string" } }, }).key("adder").pull(), ).toBe(aliceSigner.did()); expect(await pieces.remove(created.pieceId)).toBe(true); await panels.pull(); expect(panels.get().length).toBe(0); }); } it("confirms only the caller's retained creation identity and original receipt", async () => { const created = await instantiate({ program: BASE_PROGRAM }, undefined, { slug: "original", }); const other = await instantiate({ program: BASE_PROGRAM }); const pieces = await clientPieces(); for ( const altered of [ { ...created, requestKey: "missing-creation" }, { ...created, pieceId: other.pieceId }, { ...created, slug: "changed" }, { ...created, pattern: { ...created.pattern, symbol: "other" } }, { ...created, pattern: { ...created.pattern, identity: "other" } }, ] ) { await expect( confirmServedInstantiate( pieces.runtime, space, altered, aliceSigner.did(), ), ) .rejects.toThrow("Creation receipt was not retained"); } await expect( confirmServedInstantiate( pieces.runtime, space, created, serviceSigner.did(), ), ) .rejects.toThrow("Creation receipt was not retained"); }); it("resumes failed creation registration after a legitimate source update", async () => { const pending = await instantiate({ program: BASE_PROGRAM }, { seed: "retained", }, { requestKey: "registration-after-setsrc", register: true, }); const pieces = await clientPieces(); const failed = await completeServedRegistration( pieces, pending, aliceSigner.did(), ); expect(failed.registration.status).toBe("failed"); const successor = programOf( BASE_PROGRAM.files[0]!.contents.replace( "Served lifecycle", "Updated lifecycle", ), ); const { ref } = await served( "upload", (controller) => servedUploadPattern(controller, successor), ); await served( "setsrc", (controller) => servedSetPieceSource(controller, { pieceId: pending.pieceId, pattern: ref, actingUser: aliceSigner.did(), }), (runtime, receipt) => confirmServedSetSource(runtime, space, receipt), ); const rootReceipt = await instantiate({ program: programOf(` import { computed, handler, pattern, Writable } from "commonfabric"; const addPiece = handler<{piece: Writable}, {panels: Writable[]>}>( ({piece}, {panels}) => { panels.addUnique(piece); }, ); export default pattern(() => { const panels = new Writable[]>([]); return { panels, pieceRegistry: computed(() => panels.get().map(piece => piece)), addPiece: addPiece({panels}) }; }); `), }); const root = await pieces.get(rootReceipt.pieceId); await pieces.linkDefaultPattern(root.getCell()); const replay = await instantiate({ program: BASE_PROGRAM }, { seed: "replacement refused", }, { requestKey: pending.requestKey, register: true, }); expect(replay.pieceId).toBe(pending.pieceId); expect(replay.pattern).toEqual(pending.pattern); const handled = await completeServedRegistration( pieces, replay, aliceSigner.did(), ); expect(handled.registration.status).toBe("handled"); const updated = await (await clientPieces()).get(pending.pieceId); expect(getPatternIdentityRef(updated.getCell())).toEqual(ref); const argument = pieces.getArgument<{ seed: string }>(updated.getCell()); await argument.sync(); expect(argument.get().seed).toBe("retained"); const registry = await pieces.getRegisteredPieces(); expect(registry.map((piece) => piece.id)).toEqual([pending.pieceId]); }); it("retries one request key without creating another piece or resetting its argument", async () => { const first = await instantiate({ program: BASE_PROGRAM }, { seed: "first", }, { requestKey: "same-creation", }); const second = await instantiate({ program: BASE_PROGRAM }, { seed: "retry", }, { requestKey: "same-creation", }); expect(second.pieceId).toBe(first.pieceId); const pieces = await clientPieces(); const piece = await pieces.get(first.pieceId, false); const argument = pieces.getArgument<{ seed: string }>(piece.getCell()); await argument.sync(); expect(argument.get().seed).toBe("first"); }); it("creates a piece a later client reads with the pattern pointer and argument the verb wrote", async () => { const receipt = await instantiate({ program: BASE_PROGRAM }, { seed: "planted", }); expect(receipt.pattern.symbol).toBe("default"); const pieces = await clientPieces(); const piece = await pieces.get(receipt.pieceId); expect(getPatternIdentityRef(piece.getCell())).toEqual(receipt.pattern); const argument = pieces.getArgument<{ seed?: string }>(piece.getCell()); await argument.sync(); expect(argument.get()).toEqual({ seed: "planted" }); expect(getPieceSourceRevisions(piece.getCell())).toHaveLength(1); expect(host.stats().lifecycleVerbs).toEqual({ runs: 1, failures: 0 }); }); it("takes a pattern an earlier upload left in the space", async () => { const { ref } = await served( "upload", (pieces) => servedUploadPattern(pieces, BASE_PROGRAM), ); const receipt = await instantiate({ pattern: ref }); expect(receipt.pattern).toEqual(ref); const pieces = await clientPieces(); const piece = await pieces.get(receipt.pieceId); expect(getPatternIdentityRef(piece.getCell())).toEqual(ref); }); it("refuses a pattern the space does not hold", async () => { const refusal = await refusalOf(instantiate({ pattern: { identity: "no-such-identity", symbol: "default" }, })); expect(refusal.code).toBe("pattern-not-found"); }); it("claims the slug with the creation, refuses a taken name, and takes it under force", async () => { const first = await instantiate({ program: BASE_PROGRAM }, undefined, { slug: "named-piece", }); expect(first.slug).toBe("named-piece"); const pieces = await clientPieces(); expect(pieceId(await resolveSlugTargetCell(pieces, "named-piece"))) .toBe(first.pieceId); const refusal = await refusalOf( instantiate({ program: BASE_PROGRAM }, undefined, { slug: "named-piece", }), ); expect(refusal.code).toBe("slug-taken"); expect(refusal.message).toContain("nothing was created"); const taken = await instantiate({ program: BASE_PROGRAM }, undefined, { slug: "named-piece", force: true, }); // A client opened after the forced claim: the earlier one holds the // name's document from before it moved. const later = await clientPieces(); expect(pieceId(await resolveSlugTargetCell(later, "named-piece"))) .toBe(taken.pieceId); }); it("refuses to register a piece in a space with no root", async () => { const pending = await instantiate({ program: BASE_PROGRAM }, undefined, { register: true, }); const pieces = await clientPieces(); const receipt = await completeServedRegistration( pieces, pending, aliceSigner.did(), ); expect(receipt.registration.status).toBe("failed"); expect(receipt.registration.error).toContain("no default pattern"); expect(receipt.pieceId).toBe(pending.pieceId); const retried = await instantiate({ program: BASE_PROGRAM }, undefined, { requestKey: pending.requestKey, register: true, }); expect(retried.pieceId).toBe(pending.pieceId); }); it("refuses a program that does not compile, naming the failure", async () => { const refusal = await refusalOf(instantiate({ program: BROKEN_PROGRAM })); expect(refusal.code).toBe("compile-failed"); expect(refusal.message).toContain("undefinedName"); expect(host.stats().lifecycleVerbs).toEqual({ runs: 1, failures: 1 }); }); }); describe("setsrc", () => { // The served source update commits its setup transaction directly to // the store, outside the cycle's wave, so the update's module authority // registers from a durable verdict on the serving runtime and a later // runtime reads it from the stored closure. // A program is uploaded as a verb of its own, the way the route does // it, so its closure is durable before the update reads it. const patternOf = async (source: ServedPatternSource) => source.pattern ?? (await served( "upload", (pieces) => servedUploadPattern(pieces, source.program), )).ref; const setSource = async ( pieceId: string, source: ServedPatternSource, options: Omit< ServedSetSourceRequest, "pieceId" | "pattern" | "actingUser" > = {}, ) => { const pattern = await patternOf(source); return await served( "setsrc", (pieces) => servedSetPieceSource(pieces, { pieceId, pattern, ...options, actingUser: aliceSigner.did(), }), (runtime, receipt) => confirmServedSetSource(runtime, space, receipt), ); }; it("replaces the source a later client reads, with the revision the receipt names, and authorizes the successor over the predecessor", async () => { const created = await instantiate({ program: authorizedWriterProgram("v1"), }); const candidate = await patternOf({ program: authorizedWriterProgram("v2"), }); // Registered on the serving runtime from the committed transaction: // read inside the verb, before the cycle's wave could commit. let granted: boolean | undefined; const receipt = await served( "setsrc", async (pieces) => { const receipt = await servedSetPieceSource(pieces, { pieceId: created.pieceId, pattern: candidate, actingUser: aliceSigner.did(), }); granted = pieces.runtime.grantsModuleDelegation( space, receipt.pattern.identity, created.pattern.identity, ); return receipt; }, (runtime, receipt) => confirmServedSetSource(runtime, space, receipt), ); expect(receipt.pieceId).toBe(created.pieceId); expect(receipt.pattern.identity).not.toBe(created.pattern.identity); expect(receipt.seq).toBeGreaterThan(0); expect(receipt.detachedOrigin).toBeNull(); expect(granted).toBe(true); const pieces = await clientPieces(); const piece = await pieces.get(receipt.pieceId); expect(getPatternIdentityRef(piece.getCell())).toEqual(receipt.pattern); const revisions = getPieceSourceRevisions(piece.getCell()); expect(revisions.at(-1)?.revisionId).toBe(receipt.revisionId); expect(revisions.at(-1)?.pattern).toEqual(receipt.pattern); // The stored closure carries the delegation a fresh runtime registers // from. const tx = pieces.runtime.edit(); try { const closure = await loadVerifiedSourceClosure( pieces.runtime, space, receipt.pattern.identity, tx, ); expect( closure?.get(receipt.pattern.identity)?.delegatedModuleIdentities, ).toContain(created.pattern.identity); } finally { tx.abort(); } expect(host.stats().lifecycleVerbs).toEqual({ runs: 3, failures: 0 }); }); it("refuses a source whose argument schema is not backward compatible, and applies it under the dangerous override", async () => { // Created without a seed, so the stored argument satisfies either // schema and only the declared contract stands between the two. const created = await instantiate({ program: BASE_PROGRAM }); const refusal = await refusalOf( setSource(created.pieceId, { program: NUMERIC_SEED_PROGRAM }), ); expect(refusal.code).toBe("incompatible"); expect(refusal.message).toContain("not backward compatible"); const unchanged = await clientPieces(); expect( getPatternIdentityRef((await unchanged.get(created.pieceId)).getCell()), ).toEqual(created.pattern); const receipt = await setSource( created.pieceId, { program: NUMERIC_SEED_PROGRAM }, { dangerouslyAllowIncompatibleSchema: true }, ); const pieces = await clientPieces(); expect( getPatternIdentityRef((await pieces.get(created.pieceId)).getCell()), ).toEqual(receipt.pattern); }); it("refuses a piece the space does not hold, and an address that names none", async () => { const refusal = await refusalOf( setSource("no-such-piece", { program: BASE_PROGRAM }), ); expect(refusal.code).toBe("piece-not-found"); const malformed = await refusalOf( setSource("", { program: BASE_PROGRAM }), ); expect(malformed.code).toBe("piece-not-found"); }); it("refuses an update proved against a pattern the piece is no longer on", async () => { const created = await instantiate({ program: BASE_PROGRAM }); const refusal = await refusalOf( setSource(created.pieceId, { program: BASE_PROGRAM }, { expectedPattern: { identity: "elsewhere", symbol: "default" }, }), ); expect(refusal.code).toBe("source-moved"); }); it("refuses a pattern the space does not hold", async () => { const created = await instantiate({ program: BASE_PROGRAM }); const refusal = await refusalOf( setSource(created.pieceId, { pattern: { identity: "no-such-identity", symbol: "default" }, }), ); expect(refusal.code).toBe("pattern-not-found"); }); it("reports the refresh as deferred on the receipt the served update is built on", async () => { // The served option on the client's own runtime: the commit is the // store's as always there, and the piece is left to whoever runs it. const created = await instantiate({ program: BASE_PROGRAM }); const candidate = await patternOf({ program: NUMERIC_SEED_PROGRAM }); const pieces = await clientPieces(); const pattern = await pieces.runtime.patternManager .loadPatternByIdentity(candidate.identity, candidate.symbol, space); const piece = await pieces.get(created.pieceId); const receipt = await piece.setCompiledPattern(pattern!, { dangerouslyAllowIncompatibleSchema: true, served: { actingUser: aliceSigner.did() }, }); expect(receipt.status).toBe("committed"); expect(receipt.refresh).toEqual({ status: "deferred" }); expect(getPatternIdentityRef(piece.getCell())).toEqual(receipt.ref); }); }); });