import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { createSession, Identity } from "@commonfabric/identity"; import { getPatternIdentityRef, NAME, Pattern, Runtime, type RuntimeProgram, } from "@commonfabric/runner"; import { entityRefToString } from "@commonfabric/data-model/cell-rep"; import { StorageManager } from "@commonfabric/runner/storage/cache.deno"; import { PieceManager } from "../src/manager.ts"; import { PieceController } from "../src/ops/piece-controller.ts"; import { PiecesController } from "../src/ops/pieces-controller.ts"; const signer = await Identity.fromPassphrase("piece pull materialization"); function doublePattern(): Pattern { return { argumentSchema: { type: "object", properties: { input: { type: "number" }, }, }, resultSchema: { type: "object", properties: { output: { type: "number" }, }, }, derivedInternalCells: [{ partialCause: "output" }], result: { output: { $alias: { partialCause: "output", path: [] } }, }, nodes: [ { module: { type: "javascript", implementation: (input: number) => input * 2, }, inputs: { $alias: { cell: "argument", path: ["input"] } }, outputs: { $alias: { partialCause: "output", path: [] } }, }, ], }; } function tenfoldPattern(): Pattern { return { argumentSchema: { type: "object", properties: { input: { type: "number" }, }, }, resultSchema: { type: "object", properties: { output: { type: "number" }, }, }, derivedInternalCells: [{ partialCause: "output" }], result: { output: { $alias: { partialCause: "output", path: [] } }, }, nodes: [ { module: { type: "javascript", implementation: (input: number) => input * 10, }, inputs: { $alias: { cell: "argument", path: ["input"] } }, outputs: { $alias: { partialCause: "output", path: [] } }, }, ], }; } function namedPattern(name: string, multiplier: number): Pattern { return { argumentSchema: { type: "object", properties: { input: { type: "number" }, }, }, resultSchema: { type: "object", properties: { [NAME]: { type: "string" }, output: { type: "number" }, }, required: [NAME, "output"], }, derivedInternalCells: [{ partialCause: "output" }], result: { [NAME]: name, output: { $alias: { partialCause: "output", path: [] } }, }, nodes: [ { module: { type: "javascript", implementation: (input: number) => input * multiplier, }, inputs: { $alias: { cell: "argument", path: ["input"] } }, outputs: { $alias: { partialCause: "output", path: [] } }, }, ], }; } function compiledMultiplierProgram( version: string, multiplier: number, ): RuntimeProgram { return { main: "/main.tsx", files: [ { name: "/main.tsx", contents: [ "import { pattern, lift } from 'commonfabric';", `const multiply = lift((input: number) => input * ${multiplier});`, "export default pattern<{ input: number }>(({ input }) => ({", ` version: ${JSON.stringify(version)},`, " output: multiply(input),", "}));", ].join("\n"), }, ], }; } async function waitFor( predicate: () => boolean, timeoutMs: number = 1_000, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (predicate()) { return; } await new Promise((resolve) => setTimeout(resolve, 1)); } throw new Error(`Timed out waiting ${timeoutMs}ms`); } function trustPattern(runtime: Runtime, pattern: Pattern): Pattern { return runtime.unsafeTrustPattern(pattern, { reason: "piece pull materialization test fixture", }); } describe("piece pull materialization", () => { let storageManager: ReturnType; let runtime: Runtime; let manager: PieceManager; beforeEach(async () => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL("http://localhost:9999"), storageManager, }); const session = await createSession({ identity: signer, spaceName: "pull-materialization-" + crypto.randomUUID(), }); manager = new PieceManager(session, runtime); await manager.synced(); }); afterEach(async () => { await runtime?.dispose(); await storageManager?.close(); }); it("pulls before reading result values", async () => { const piece = await manager.runPersistent( trustPattern(runtime, doublePattern()), { input: 5 }, undefined, { start: true }, ); const controller = new PieceController(manager, piece); const inputCell = await controller.input.getCell(); await runtime.editWithRetry((tx) => { inputCell.withTx(tx).key("input").set(7); }); expect(await controller.result.get(["output"])).toBe(14); }); it("materializes piece results before setInput returns", async () => { const piece = await manager.runPersistent( trustPattern(runtime, doublePattern()), { input: 5 }, undefined, { start: true }, ); const controller = new PieceController(manager, piece); await controller.setInput({ input: 7 }); expect(manager.getResult(piece).get()).toEqual({ output: 14 }); }); it("materializes piece results before runWithPattern returns", async () => { const piece = await manager.runPersistent( trustPattern(runtime, doublePattern()), { input: 5 }, undefined, { start: true }, ); expect(manager.getResult(piece).get()).toEqual({ output: 10 }); await manager.runWithPattern( trustPattern(runtime, tenfoldPattern()), entityRefToString(piece.entityId), { input: 5 }, { start: true }, ); expect(manager.getResult(piece).get()).toEqual({ output: 50 }); }); it("persists setPattern replacement by identity for fresh runtime reloads", async () => { const firstPattern = await runtime.patternManager.compilePattern( compiledMultiplierProgram("v1", 2), { space: manager.getSpace() }, ); const piece = await manager.runPersistent( firstPattern, { input: 5 }, "compiled-set-pattern-" + crypto.randomUUID(), { start: true }, ); const id = entityRefToString(piece.entityId); const controller = new PieceController(manager, piece); const firstRef = getPatternIdentityRef(piece); expect(firstRef).toBeDefined(); expect(manager.getResult(piece).get()).toEqual({ version: "v1", output: 10, }); await controller.setPattern(compiledMultiplierProgram("v2", 10)); await manager.runtime.idle(); await manager.synced(); const secondRef = getPatternIdentityRef(piece); expect(secondRef).toBeDefined(); expect(secondRef!.identity).not.toEqual(firstRef!.identity); expect(manager.getResult(piece).get()).toEqual({ version: "v2", output: 50, }); const session = await createSession({ identity: signer, spaceName: manager.getSpaceName()!, }); const freshRuntime = new Runtime({ apiUrl: new URL("http://localhost:9999"), storageManager, }); const freshManager = new PieceManager(session, freshRuntime); const freshPieces = new PiecesController(freshManager); try { await freshManager.synced(); const freshPiece = await freshPieces.get(id, true); const freshCell = freshPiece.getCell(); const freshRef = getPatternIdentityRef(freshCell); expect(freshRef).toEqual(secondRef); expect(await freshPiece.result.get()).toEqual({ version: "v2", output: 50, }); const source = await freshPiece.getPatternSourceProgram(); expect(source?.files.some((file) => file.contents.includes("v2"))).toBe( true, ); } finally { await freshRuntime.dispose(); } }); it("waits for setup to settle before setupPersistent syncs pattern metadata", async () => { const pattern = doublePattern(); const patternRef = { identity: "test-pattern-identity", symbol: "default" }; const originalSetup = manager.runtime.setup.bind(manager.runtime); const originalGetArtifactEntryRef = manager.runtime.patternManager .getArtifactEntryRef.bind(manager.runtime.patternManager); const originalSyncPatternByIdentity = manager.syncPatternByIdentity.bind( manager, ); let setupResolved = false; let releaseSetup: (() => void) | undefined; manager.runtime.setup = ((...args) => { const piece = args[3]; return new Promise((resolve) => { releaseSetup = () => { setupResolved = true; resolve(piece); }; }); }) as typeof manager.runtime.setup; const getRefStub: unknown = () => patternRef; manager.runtime.patternManager.getArtifactEntryRef = getRefStub as typeof manager.runtime.patternManager.getArtifactEntryRef; manager.syncPatternByIdentity = (( ref: { identity: string; symbol: string }, ) => { expect(ref).toEqual(patternRef); expect(setupResolved).toBe(true); return Promise.resolve(pattern); }) as typeof manager.syncPatternByIdentity; try { const pending = manager.setupPersistent(pattern, { input: 5 }); await waitFor(() => releaseSetup !== undefined); expect(setupResolved).toBe(false); if (!releaseSetup) { throw new Error("Expected runtime.setup to be called"); } releaseSetup(); await pending; } finally { manager.runtime.setup = originalSetup; manager.runtime.patternManager.getArtifactEntryRef = originalGetArtifactEntryRef; manager.syncPatternByIdentity = originalSyncPatternByIdentity; } }); it("restarts stopped pieces when runWithPattern is called with start", async () => { const piece = await manager.runPersistent( trustPattern(runtime, doublePattern()), { input: 5 }, undefined, { start: true }, ); expect(runtime.runner.cancels.size).toBe(1); await manager.stopPiece(piece); expect(runtime.runner.cancels.size).toBe(0); await manager.runWithPattern( trustPattern(runtime, tenfoldPattern()), entityRefToString(piece.entityId), { input: 5 }, { start: true }, ); expect(runtime.runner.cancels.size).toBe(1); expect(manager.getResult(piece).get()).toEqual({ output: 50 }); }); it("updates piece names when runWithPattern changes patterns", async () => { const piece = await manager.runPersistent( trustPattern(runtime, namedPattern("double", 2)), { input: 5 }, undefined, { start: true }, ); const controller = new PieceController(manager, piece); expect(controller.name()).toBe("double"); expect(manager.getResult(piece).get()).toEqual({ [NAME]: "double", output: 10, }); await manager.runWithPattern( trustPattern(runtime, namedPattern("tenfold", 10)), entityRefToString(piece.entityId), { input: 5 }, { start: true }, ); expect(controller.name()).toBe("tenfold"); expect(manager.getResult(piece).get()).toEqual({ [NAME]: "tenfold", output: 50, }); }); });