Files
united-workforce/packages/workflow-runtime/__tests__/build-context.test.ts
T
xiaoju a98431a12a feat(#194): Phase 1 — parentState / childThread in CAS nodes
- Protocol: StartNodePayload.parentState, StateNodePayload.childThread
- CAS: putStartNode refs include parentState, collectRefs includes childThread
- Parsing: legacy nodes without new fields default to null
- Engine + fork: all callers pass parentState: null / childThread: null
- Tests: 8 new cases for refs, parsing, collect-refs (+208 lines)

Phase 1 of #194 (Merkle Call Stack). Closes #195.

小橘 <xiaoju@shazhou.work>
2026-05-12 01:42:10 +00:00

124 lines
3.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
createCasStore,
putContentNodeWithRefs,
putStartNode,
putStateNode,
} from "@uncaged/workflow-cas";
import { buildThreadContext, END, START } from "../src/index.js";
describe("buildThreadContext", () => {
let dir: string;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "wf-build-ctx-"));
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
test("walks ancestor chain, resolves prompt, orders steps chronologically", async () => {
const cas = createCasStore(join(dir, "cas"));
const promptHash = await cas.put("hello-task");
const bundleHash = "BHAAAAAAAAAAA";
const startHash = await putStartNode(
cas,
{ name: "demo", hash: bundleHash, depth: 2, parentState: null },
promptHash,
);
const art = await cas.put("artifact-a");
const chPlan = await putContentNodeWithRefs(cas, "plan body", [art]);
const statePlan = await putStateNode(cas, {
role: "planner",
meta: { phase: 1 },
start: startHash,
content: chPlan,
ancestors: [],
compact: null,
timestamp: 1000,
childThread: null,
});
const chCode = await putContentNodeWithRefs(cas, "code body", []);
const stateCode = await putStateNode(cas, {
role: "coder",
meta: { phase: 2 },
start: startHash,
content: chCode,
ancestors: [statePlan],
compact: null,
timestamp: 2000,
childThread: null,
});
const ctx = await buildThreadContext(stateCode, cas);
expect(ctx.threadId).toBe("");
expect(ctx.depth).toBe(2);
expect(ctx.start.role).toBe(START);
expect(ctx.start.content).toBe("hello-task");
expect(ctx.steps.map((s) => s.role)).toEqual(["planner", "coder"]);
expect(ctx.steps[0]?.refs).toEqual([art]);
expect(ctx.steps[1]?.refs).toEqual([]);
expect(ctx.steps[0]?.timestamp).toBe(1000);
expect(ctx.steps[1]?.timestamp).toBe(2000);
});
test("StartNode head yields empty steps", async () => {
const cas = createCasStore(join(dir, "cas"));
const promptHash = await cas.put("only-prompt");
const startHash = await putStartNode(
cas,
{ name: "solo", hash: "BHBBBBBBBBBBB", depth: 1, parentState: null },
promptHash,
);
const ctx = await buildThreadContext(startHash, cas);
expect(ctx.steps).toEqual([]);
expect(ctx.start.content).toBe("only-prompt");
expect(ctx.depth).toBe(1);
});
test("omits __end__ states from steps", async () => {
const cas = createCasStore(join(dir, "cas"));
const promptHash = await cas.put("task");
const bundleHash = "BHCCCCCCCCCCC";
const startHash = await putStartNode(
cas,
{ name: "demo", hash: bundleHash, depth: 0, parentState: null },
promptHash,
);
const ch1 = await putContentNodeWithRefs(cas, "step-one", []);
const state1 = await putStateNode(cas, {
role: "worker",
meta: { done: false },
start: startHash,
content: ch1,
ancestors: [],
compact: null,
timestamp: 500,
childThread: null,
});
const endContent = await putContentNodeWithRefs(cas, "finished", []);
const endState = await putStateNode(cas, {
role: END,
meta: { returnCode: 0, summary: "finished" },
start: startHash,
content: endContent,
ancestors: [state1],
compact: null,
timestamp: 600,
childThread: null,
});
const ctx = await buildThreadContext(endState, cas);
expect(ctx.steps.map((s) => s.role)).toEqual(["worker"]);
});
});