import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import "@commontools/utils/equal-ignoring-symbols"; import { Identity } from "@commontools/identity"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { NAME, type Recipe } from "../src/builder/types.ts"; import { Runtime } from "../src/runtime.ts"; import { extractDefaultValues, mergeObjects } from "../src/runner.ts"; import { type ICommitNotification, type IExtendedStorageTransaction, type IStorageSubscription, type MediaType, type URI, } from "../src/storage/interface.ts"; const signer = await Identity.fromPassphrase("test operator"); const space = signer.did(); describe("runRecipe", () => { let storageManager: ReturnType; let runtime: Runtime; beforeEach(() => { storageManager = StorageManager.emulate({ as: signer }); // Create runtime with the shared storage provider // We need to bypass the URL-based configuration for this test runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); }); afterEach(async () => { await runtime?.storageManager.synced(); await runtime?.dispose(); await storageManager?.close(); }); it("should work with passthrough", async () => { const recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" }, }, description: "passthrough", }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "passthrough", }, inputs: { value: { $alias: { path: ["argument", "input"] } } }, outputs: { value: { $alias: { path: ["internal", "output"] } } }, }, ], } as Recipe; const resultCell = runtime.getCell( space, "should work with passthrough", ); const result = runtime.run( undefined, recipe, { input: 1 }, resultCell, ); const sourceCell = result.getSourceCell(); const sourceCellValue = await sourceCell!.pull(); expect(sourceCellValue).toMatchObject({ argument: { input: 1 }, internal: { output: 1 }, }); expect(result.getRaw()).toEqual({ output: { $alias: { path: ["internal", "output"], cell: result.getSourceCell()?.entityId, }, }, }); const resultValue = await result.pull(); expect(resultValue).toEqual({ output: 1 }); }); it("should work with nested recipes", async () => { const innerRecipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" }, }, }, resultSchema: {}, result: { $alias: { cell: 1, path: ["internal", "output"] } }, nodes: [ { module: { type: "passthrough", }, inputs: { value: { $alias: { cell: 1, path: ["argument", "input"] } }, }, outputs: { value: { $alias: { cell: 1, path: ["internal", "output"] } }, }, }, ], } as Recipe; const outerRecipe = { argumentSchema: { type: "object", properties: { value: { type: "number" }, result: { type: "number" }, }, }, resultSchema: {}, result: { result: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "recipe", implementation: innerRecipe }, inputs: { input: { $alias: { path: ["argument", "value"] } } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], } as Recipe; const resultCell = runtime.getCell( space, "should work with nested recipes", ); const result = runtime.run( undefined, outerRecipe, { value: 5 }, resultCell, ); const resultValue = await result.pull(); expect(resultValue).toEqual({ result: 5 }); }); it("should run a simple module", async () => { const mockRecipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "javascript", implementation: (value: number) => value * 2, }, inputs: { $alias: { path: ["argument", "value"] } }, outputs: { $alias: { path: ["internal", "result"] } }, }, ], }; const tx = runtime.edit(); const resultCell = runtime.getCell( space, "should run a simple module", undefined, tx, ); const result = runtime.run( tx, mockRecipe, { value: 1 }, resultCell, ); tx.commit(); const resultValue = await result.pull(); expect(JSON.stringify(resultValue)).toEqual( JSON.stringify({ result: 2 }), ); }); it("should run a simple module with no outputs", async () => { let ran = false; const mockRecipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "javascript", implementation: () => { ran = true; }, }, inputs: { $alias: { path: ["argument", "value"] } }, outputs: {}, }, ], }; const resultCell = runtime.getCell( space, "should run a simple module with no outputs", ); const result = await runtime.runSynced(resultCell, mockRecipe, { value: 1, }); const resultValue = await result.pull(); expect(resultValue).toEqual({ result: undefined }); expect(ran).toBe(true); }); it("should handle incorrect inputs gracefully", async () => { let ran = false; const mockRecipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "javascript", implementation: () => { ran = true; }, }, inputs: { $alias: { path: ["argument", "other"] } }, outputs: {}, }, ], }; const resultCell = runtime.getCell( space, "should handle incorrect inputs gracefully", ); const result = await runtime.runSynced(resultCell, mockRecipe, { value: 1, }); const resultValue2 = await result.pull(); expect(resultValue2).toEqual({ result: undefined }); expect(ran).toBe(true); }); it("should handle nested recipes", async () => { const nestedRecipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { $alias: { cell: 1, path: ["internal", "result"] } }, nodes: [ { module: { type: "javascript", implementation: (value: number) => value * 2, }, inputs: { $alias: { cell: 1, path: ["argument", "input"] } }, outputs: { $alias: { cell: 1, path: ["internal", "result"] } }, }, ], }; const mockRecipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "recipe", implementation: nestedRecipe }, inputs: { input: { $alias: { path: ["argument", "value"] } } }, outputs: { $alias: { path: ["internal", "result"] } }, }, ], }; const resultCell = runtime.getCell( space, "should handle nested recipes", ); const result = runtime.run( undefined, mockRecipe, { value: 1 }, resultCell, ); const resultValue = await result.pull(); expect(resultValue).toEqual({ result: 2 }); }); it("should allow passing a cell as a binding", async () => { const recipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { output: { $alias: { path: ["argument", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (value: number) => value * 2, }, inputs: { $alias: { path: ["argument", "input"] } }, outputs: { $alias: { path: ["argument", "output"] } }, }, ], }; const tx1 = runtime.edit(); const inputCell = runtime.getCell<{ input: number; output: number }>( space, "should allow passing a cell as a binding: input cell", undefined, tx1, ); inputCell.set({ input: 10, output: 0 }); await tx1.commit(); const resultCell = runtime.getCell( space, "should allow passing a cell as a binding", ); const result = runtime.run( undefined, recipe, inputCell, resultCell, ); const inputCellValue = await inputCell.pull(); expect(inputCellValue).toMatchObject({ input: 10, output: 20 }); let resultValue = await result.pull(); expect(resultValue).toEqual({ output: 20 }); // The result should alias the original cell. Let's verify by stopping the // recipe and sending a new value to the input cell. runtime.runner.stop(result); const tx2 = runtime.edit(); inputCell.withTx(tx2).send({ input: 10, output: 40 }); await tx2.commit(); resultValue = await result.pull(); expect(resultValue).toEqual({ output: 40 }); }); it("should allow stopping a recipe", async () => { const recipe: Recipe = { argumentSchema: {}, resultSchema: {}, result: { output: { $alias: { path: ["argument", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (value: number) => value * 2, }, inputs: { $alias: { path: ["argument", "input"] } }, outputs: { $alias: { path: ["argument", "output"] } }, }, ], }; const tx = runtime.edit(); const inputCell = runtime.getCell<{ input: number; output: number }>( space, "should allow stopping a recipe: input cell", undefined, tx, ); inputCell.set({ input: 10, output: 0 }); const resultCell = runtime.getCell( space, "should allow stopping a recipe", undefined, tx, ); // Commit the initial values before running the recipe await tx.commit(); const result = runtime.run( undefined, recipe, inputCell, resultCell, ); let inputCellValue = await inputCell.pull(); expect(inputCellValue).toMatchObject({ input: 10, output: 20 }); const tx2 = runtime.edit(); inputCell.withTx(tx2).send({ input: 20, output: 20 }); await tx2.commit(); inputCellValue = await inputCell.pull(); expect(inputCellValue).toMatchObject({ input: 20, output: 40 }); // Stop the recipe runtime.runner.stop(result); const tx3 = runtime.edit(); inputCell.withTx(tx3).send({ input: 40, output: 40 }); await tx3.commit(); inputCellValue = await inputCell.pull(); expect(inputCellValue).toMatchObject({ input: 40, output: 40 }); // Restart the recipe runtime.run( undefined, recipe, undefined, result, ); inputCellValue = await inputCell.pull(); expect(inputCellValue).toMatchObject({ input: 40, output: 80 }); }); it("should apply default values from argument schema", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number", default: 42 }, multiplier: { type: "number", default: 2 }, }, required: ["input"], }, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "javascript", implementation: (args: { input: number; multiplier: number }) => args.input * args.multiplier, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "result"] } }, }, ], }; // Test with partial arguments (should use default for multiplier) const resultWithPartialCell = runtime.getCell( space, "default values test - partial", ); const resultWithPartial = runtime.run( undefined, recipe, { input: 10 }, resultWithPartialCell, ); const partialValue = await resultWithPartial.pull(); expect(partialValue).toEqual({ result: 20 }); // Test with no arguments (should use default for input) const resultWithDefaultsCell = runtime.getCell( space, "default values test - all defaults", ); const resultWithDefaults = runtime.run( undefined, recipe, {}, resultWithDefaultsCell, ); const defaultsValue = await resultWithDefaults.pull(); expect(defaultsValue).toEqual({ result: 84 }); // 42 * 2 }); it("should handle complex nested schema types", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { config: { type: "object", properties: { values: { type: "array", items: { type: "number" }, }, operation: { type: "string", enum: ["sum", "avg", "max"] }, }, required: ["values", "operation"], }, }, required: ["config"], }, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } } }, nodes: [ { module: { type: "javascript", implementation: ( args: { config: { values: number[]; operation: string } }, ) => { const values = args.config.values; switch (args.config.operation) { case "sum": return values.reduce((a, b) => a + b, 0); case "avg": return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; case "max": return Math.max(...values); default: return 0; } }, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "result"] } }, }, ], }; const resultCell = runtime.getCell( space, "complex schema test", ); const result = runtime.run( undefined, recipe, { config: { values: [10, 20, 30, 40], operation: "avg" } }, resultCell, ); const resultValue = await result.pull(); expect(resultValue).toEqual({ result: 25 }); // Test with a different operation const result2 = runtime.run( undefined, recipe, { config: { values: [10, 20, 30, 40], operation: "max" } }, resultCell, ); const result2Value = await result2.pull(); expect(result2Value).toEqual({ result: 40 }); }); it("should merge arguments with defaults from schema", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { options: { type: "object", properties: { enabled: { type: "boolean", default: true }, value: { type: "number", default: 100 }, name: { type: "string", default: "default" }, }, }, input: { type: "number", default: 1 }, }, }, resultSchema: {}, result: { result: { $alias: { path: ["internal", "result"] } }, options: { $alias: { path: ["argument", "options"] } }, }, nodes: [ { module: { type: "javascript", implementation: (args: { input: number; options: any }) => { return args.options.enabled ? args.input * args.options.value : 0; }, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "result"] } }, }, ], }; // Provide partial options - should merge with defaults const resultCell = runtime.getCell( space, "merge defaults test", ); const result = runtime.run( undefined, recipe, { options: { value: 10 }, input: 5 }, resultCell, ); const resultValue = await result.pull() as any; expect(resultValue.options).toEqual({ enabled: true, value: 10, name: "default", }); expect(resultValue.result).toEqual(50); // 5 * 10 }); it("should preserve NAME between runs", async () => { const recipe: Recipe = { argumentSchema: {}, resultSchema: {}, initial: { internal: { counter: 0 } }, result: { [NAME]: "counter", counter: { $alias: { path: ["internal", "counter"] } }, }, nodes: [ { module: { type: "javascript", implementation: (input: any) => { return input.value; }, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "counter"] } }, }, ], }; const resultCell = runtime.getCell( space, "state preservation test", ); // First run runtime.run( undefined, recipe, { value: 1 }, resultCell, ); let cellValue = await resultCell.pull(); expect(cellValue?.[NAME]).toEqual("counter"); expect(cellValue?.counter).toEqual(1); // Now change the name const tx = runtime.edit(); resultCell.withTx(tx).getAsQueryResult()[NAME] = "my counter"; await tx.commit(); // Second run with same recipe but different argument runtime.run( undefined, recipe, { value: 2 }, resultCell, ); cellValue = await resultCell.pull(); expect(cellValue?.[NAME]).toEqual("my counter"); expect(cellValue?.counter).toEqual(2); }); it("should create separate copies of initial values for each recipe instance", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, }, }, initial: { internal: { counter: 10, nested: { value: "initial" }, }, }, resultSchema: {}, result: { counter: { $alias: { path: ["internal", "counter"] } }, nested: { $alias: { path: ["internal", "nested"] } }, }, nodes: [ { module: { type: "javascript", implementation: (args: { input: number }) => { return { counter: args.input, }; }, }, inputs: { $alias: { path: ["argument", "input"] } }, outputs: { $alias: { path: ["internal", "counter"] } }, }, ], }; // Create first instance const result1Cell = runtime.getCell( space, "should create separate copies of initial values 1", ); const result1 = runtime.run( undefined, recipe, { input: 5 }, result1Cell, ); await result1.pull(); // Create second instance const result2Cell = runtime.getCell( space, "should create separate copies of initial values 2", ); const result2 = runtime.run( undefined, recipe, { input: 10 }, result2Cell, ); await result2.pull(); // Get the internal state objects // We cast away our Immutable, so we can do this test const internal1 = (result1.getSourceCell()?.getRaw() as any).internal; const internal2 = (result2.getSourceCell()?.getRaw() as any).internal; // Verify they are different objects expect(internal1).not.toBe(internal2); expect(internal1.nested).not.toBe(internal2.nested); // Modify nested object in first instance internal1.nested.value = "modified"; // Verify second instance is unaffected expect(internal2.nested.value).toBe("initial"); const result2Value = await result2.pull() as any; expect(result2Value.nested.value).toBe("initial"); }); }); describe("storage subscription", () => { let runtime: Runtime; let storageManager: ReturnType; beforeEach(() => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); }); afterEach(async () => { await runtime?.dispose(); await storageManager?.close(); }); it("clears cached recipes when storage notifies of changes", () => { const internals = runtime.runner as unknown as { resultRecipeCache: Map; createStorageSubscription(): IStorageSubscription; }; const uri = "recipe-cache-test" as URI; const key = `${space}/${uri}`; internals.resultRecipeCache.set(key, "cached-recipe"); const notification = { type: "commit", space, changes: [ { address: { id: uri, type: "application/json" as MediaType, path: [], }, before: undefined, after: undefined, }, ], } satisfies ICommitNotification; const subscription = internals.createStorageSubscription(); subscription.next(notification); expect(internals.resultRecipeCache.has(key)).toBe(false); }); }); describe("setup/start", () => { let storageManager: ReturnType; let runtime: Runtime; beforeEach(() => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); }); afterEach(async () => { await runtime?.storageManager.synced(); await runtime?.dispose(); await storageManager?.close(); }); it("setup does not schedule; start schedules and runs", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "passthrough" }, inputs: { value: { $alias: { path: ["argument", "input"] } } }, outputs: { value: { $alias: { path: ["internal", "output"] } } }, }, ], }; const resultCell = runtime.getCell(space, "setup does not schedule"); // Only setup – should not run the node yet runtime.setup(undefined, recipe, { input: 1 }, resultCell); // Output hasn't been computed yet let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: undefined }); // Start – should schedule and compute output runtime.start(resultCell); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 1 }); }); it("setup with same recipe updates argument without restart", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "passthrough" }, inputs: { value: { $alias: { path: ["argument", "input"] } } }, outputs: { value: { $alias: { path: ["internal", "output"] } } }, }, ], }; const resultCell = runtime.getCell(space, "setup updates argument"); runtime.setup(undefined, recipe, { input: 1 }, resultCell); runtime.start(resultCell); let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 1 }); // Update only via setup; scheduler should react to argument change runtime.setup(undefined, recipe, { input: 2 }, resultCell); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 2 }); }); it("start is idempotent when called multiple times", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "start idempotent"); runtime.setup(undefined, recipe, { input: 7 }, resultCell); runtime.start(resultCell); runtime.start(resultCell); let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 7 }); // Change input and ensure only a single recomputation occurs in effect runtime.setup(undefined, recipe, { input: 9 }, resultCell); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 9 }); }); it("stop and restart works with setup/start", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "stop and restart"); runtime.setup(undefined, recipe, { input: 1 }, resultCell); runtime.start(resultCell); let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 1 }); // Stop the scheduling runtime.runner.stop(resultCell); // Change argument via setup; without start nothing should recompute yet runtime.setup(undefined, recipe, { input: 5 }, resultCell); cellValue = await resultCell.pull(); // Still the old output expect(cellValue).toEqual({ output: 1 }); // Restart runtime.start(resultCell); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 5 }); }); it("setup with Module wraps to recipe and runs on start", async () => { const mod = { type: "javascript" as const, implementation: (v: { input: number }) => ({ output: v.input * 3 }), }; const resultCell = runtime.getCell(space, "setup with module"); runtime.setup(undefined, mod as any, { input: 2 } as any, resultCell); // Not started yet; no output let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: undefined }); runtime.start(resultCell); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 6 }); }); it("setup without recipe reuses previous recipe", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "passthrough" }, inputs: { value: { $alias: { path: ["argument", "input"] } } }, outputs: { value: { $alias: { path: ["internal", "output"] } } }, }, ], }; const resultCell = runtime.getCell(space, "setup reuse previous recipe"); runtime.setup(undefined, recipe, { input: 5 }, resultCell); runtime.start(resultCell); const cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 5 }); // Stop and setup without specifying recipe; should reuse stored one runtime.runner.stop(resultCell); runtime.setup( undefined, undefined as any, { input: 10 } as any, resultCell, ); // Not started yet; result still aliases internal and shows previous value const rawValue = resultCell.get(); expect(rawValue).toMatchObjectIgnoringSymbols({ output: { $alias: { path: ["internal", "output"] } }, }); // Verify a recipe id is present after setup without passing recipe const source = resultCell.getSourceCell()!; const typeValue = source.key("$TYPE").get(); expect(typeof typeValue).toEqual("string"); // Also verify the argument was updated in the process cell const sourceValue = await source.pull(); expect((sourceValue as any).argument.input).toEqual(10); // Start again (scheduling) just to ensure no errors runtime.start(resultCell); await resultCell.pull(); }); it("setup with cell argument and start reacts to cell updates", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" }, output: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["argument", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (value: number) => value * 2, }, inputs: { $alias: { path: ["argument", "input"] } }, outputs: { $alias: { path: ["argument", "output"] } }, }, ], }; const tx = runtime.edit(); const inputCell = runtime.getCell<{ input: number; output: number }>( space, "setup with cell arg: input", undefined, tx, ); inputCell.set({ input: 3, output: 0 }); await tx.commit(); const resultCell = runtime.getCell(space, "setup with cell arg"); runtime.setup(undefined, recipe, inputCell, resultCell); runtime.start(resultCell); let cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 6 }); const tx2 = runtime.edit(); inputCell.withTx(tx2).send({ input: 4, output: 0 }); await tx2.commit(); cellValue = await resultCell.pull(); expect(cellValue).toEqual({ output: 8 }); }); }); describe("runner utils", () => { let storageManager: ReturnType; let runtime: Runtime; let tx: IExtendedStorageTransaction; beforeEach(() => { storageManager = StorageManager.emulate({ as: signer }); // Create runtime with the shared storage provider // We need to bypass the URL-based configuration for this test runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); tx = runtime.edit(); }); afterEach(async () => { await tx.commit(); await runtime?.dispose(); await storageManager?.close(); }); describe("extractDefaultValues", () => { it("should extract default values from a schema", () => { const schema = { type: "object" as const, properties: { name: { type: "string" as const, default: "John" }, age: { type: "number" as const, default: 30 }, address: { type: "object" as const, properties: { street: { type: "string" as const, default: "Main St" }, city: { type: "string" as const, default: "New York" }, }, }, }, }; const result = extractDefaultValues(schema); expect(result).toEqual({ name: "John", age: 30, address: { street: "Main St", city: "New York", }, }); }); }); describe("mergeObjects", () => { it("should merge multiple objects", () => { const obj1 = { a: 1, b: { x: 10 } }; const obj2 = { b: { y: 20 }, c: 3 }; const obj3 = { a: 4, d: 5 }; const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: 1, b: { x: 10, y: 20 }, c: 3, d: 5, }); }); it("should handle undefined values", () => { const obj1 = { a: 1 }; const obj2 = undefined; const obj3 = { b: 2 }; const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: 1, b: 2 }); }); it("should give precedence to earlier objects in the case of a conflict", () => { const obj1 = { a: 1 }; const obj2 = { a: 2, b: { c: 3 } }; const obj3 = { a: 3, b: { c: 4 } }; const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: 1, b: { c: 3 } }); }); it("should treat cell aliases and references as values", () => { const testCell = runtime.getCell<{ a: any }>( space, "should treat cell aliases and references as values 1", undefined, tx, ); const obj1 = { a: { $alias: { path: [] } } }; const obj2 = { a: 2, b: { c: testCell.getAsLink() } }; const obj3 = { a: testCell.key("a").getAsWriteRedirectLink(), b: { c: 4 }, }; const result = mergeObjects(obj1, obj2, obj3); expect(result).toEqual({ a: { $alias: { path: [] } }, b: { c: testCell.getAsLink() }, }); }); }); describe("start() lazy sync behavior", () => { it("start() returns Promise that resolves to true", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input * 2, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "start returns promise"); runtime.setup(undefined, recipe, { input: 5 }, resultCell); const result = await runtime.start(resultCell); expect(result).toBe(true); await runtime.idle(); expect(resultCell.getAsQueryResult()).toEqual({ output: 10 }); }); it("start() returns true immediately if already running", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "start idempotent"); runtime.setup(undefined, recipe, { input: 1 }, resultCell); await runtime.start(resultCell); await runtime.idle(); // Second call should return true immediately const result = await runtime.start(resultCell); expect(result).toBe(true); }); it("start() runs synchronously when data is available", async () => { const recipe: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input * 3, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "start sync behavior"); runtime.setup(undefined, recipe, { input: 4 }, resultCell); // start() should execute synchronously when data is available // The charm should be registered in cancels map immediately runtime.start(resultCell); // Should be running now (check via runner.cancels having the key) expect( runtime.runner["cancels"].has(runtime.runner["getDocKey"](resultCell)), ).toBe(true); await runtime.idle(); expect(resultCell.getAsQueryResult()).toEqual({ output: 12 }); }); it("restarts with new recipe when $TYPE changes via setup()", async () => { const recipe1: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input * 2, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const recipe2: Recipe = { argumentSchema: { type: "object", properties: { input: { type: "number" } }, }, resultSchema: {}, result: { output: { $alias: { path: ["internal", "output"] } } }, nodes: [ { module: { type: "javascript", implementation: (v: { input: number }) => v.input * 10, }, inputs: { $alias: { path: ["argument"] } }, outputs: { $alias: { path: ["internal", "output"] } }, }, ], }; const resultCell = runtime.getCell(space, "recipe change restart"); // Run with first recipe runtime.run(undefined, recipe1, { input: 5 }, resultCell); await runtime.idle(); expect(resultCell.getAsQueryResult()).toEqual({ output: 10 }); // 5 * 2 // Change recipe via setup (not run or start) // The $TYPE sink should detect the change and restart await runtime.setup(undefined, recipe2, { input: 5 }, resultCell); await runtime.idle(); expect(resultCell.getAsQueryResult()).toEqual({ output: 50 }); // 5 * 10 }); }); });