Compare commits

...

2 Commits

Author SHA1 Message Date
xiaoju 8807b0ac6a fix(test): align tests with type-safety refactor
Update test expectations after workflow reflexes were removed from
YAML config and type signatures were tightened:

- core/config: workflow reflex tests now expect 'not supported' error
- cli/workflow: partitionWorkflowMessage test uses strict typed params
- daemon/crash-recovery: remove triggerPayload from resume-thread assertion
- daemon/daemon-ipc: trigger-workflow sends prompt+maxRounds
- daemon/kernel-workflow: use Sense-driven workflow trigger pattern

Fixes 12 test failures across core, cli, and daemon packages.

Refs #88, #89
2026-04-24 12:23:21 +00:00
xiaomo 5b65afdc4b Merge pull request 'refactor: improve type safety across codebase' (#90) from refactor/type-safety into main 2026-04-24 12:09:36 +00:00
5 changed files with 106 additions and 51 deletions
+8 -3
View File
@@ -342,9 +342,14 @@ describe("partitionWorkflowMessage", () => {
expect(p.meta).toEqual({ items: [1, 2] });
});
it("uses fallback role and stringifies non-string content", () => {
const p = partitionWorkflowMessage({ content: { n: 1 } });
expect(p.roleStr).toBe("?");
it("passes through role and content as-is", () => {
const p = partitionWorkflowMessage({
role: "unknown",
content: '{"n":1}',
meta: null,
timestamp: 0,
});
expect(p.roleStr).toBe("unknown");
expect(p.contentBody).toBe('{"n":1}');
});
});
+6 -6
View File
@@ -193,7 +193,7 @@ reflexes:
expect(result.error.message).toMatch(/disk.*not found in senses/);
});
it("returns error when workflow reflex references a non-existent workflow", () => {
it("returns error when reflex uses unsupported workflow field", () => {
const yaml = `
senses:
cpu:
@@ -206,10 +206,10 @@ reflexes:
const result = parseNerveConfig(yaml);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toMatch(/missing_wf.*not found in workflows/);
expect(result.error.message).toMatch(/workflow.*not supported/);
});
it("returns error when workflow reflex references non-existent workflow (with workflows defined)", () => {
it("returns error when reflex uses unsupported workflow field (with workflows defined)", () => {
const yaml = `
senses:
cpu:
@@ -226,7 +226,7 @@ workflows:
const result = parseNerveConfig(yaml);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toMatch(/unknown.*not found in workflows/);
expect(result.error.message).toMatch(/workflow.*not supported/);
});
it("returns error for invalid throttle format", () => {
@@ -354,7 +354,7 @@ reflexes:
const result = parseNerveConfig(yaml);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toMatch(/cannot have both/);
expect(result.error.message).toMatch(/workflow.*not supported/);
});
it("returns error when reflex has neither sense nor workflow", () => {
@@ -368,7 +368,7 @@ reflexes:
const result = parseNerveConfig(yaml);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toMatch(/must have either/);
expect(result.error.message).toMatch(/must include "sense"/);
});
});
});
@@ -235,7 +235,6 @@ describe("WorkflowManager — crash recovery (Phase 3)", () => {
expect(resumeCalls[0][0]).toMatchObject({
type: "resume-thread",
runId: "run-started-1",
triggerPayload: { trigger: "initial" },
});
expect(Array.isArray((resumeCalls[0][0] as Record<string, unknown>).messages)).toBe(true);
@@ -318,8 +317,8 @@ describe("WorkflowManager — crash recovery (Phase 3)", () => {
});
const mgr = createWorkflowManager("/nerve-root", config, logStore);
const payload = { prompt: "build-docker for myrepo", maxRounds: 10 };
mgr.startWorkflow("my-wf", payload);
const launch = { prompt: "build-docker for myrepo", maxRounds: 10 };
mgr.startWorkflow("my-wf", launch);
const startedCall = logStore.upsertWorkflowRun.mock.calls.find(
(args: any[]) => (args[0] as { type: string }).type === "started",
@@ -328,7 +327,7 @@ describe("WorkflowManager — crash recovery (Phase 3)", () => {
const logEntry = startedCall?.[0] as { payload: string | null };
expect(logEntry.payload).not.toBeNull();
const parsed = JSON.parse(logEntry.payload as string) as Record<string, unknown>;
expect(parsed.triggerPayload).toMatchObject(payload);
expect(parsed).toMatchObject({ prompt: "build-docker for myrepo", maxRounds: 10 });
const stopPromise = mgr.stop();
await vi.runAllTimersAsync();
@@ -152,12 +152,13 @@ describe("daemon-ipc — trigger-sense", () => {
const resp = await sendRaw(sockPath, {
type: "trigger-workflow",
workflow: "my-workflow",
payload: {},
prompt: "test prompt",
maxRounds: 10,
});
expect(resp).toEqual({ ok: true });
expect(triggerSense).not.toHaveBeenCalled();
expect(wfManager.startWorkflow).toHaveBeenCalledWith("my-workflow", {});
expect(wfManager.startWorkflow).toHaveBeenCalledWith("my-workflow", { prompt: "test prompt", maxRounds: 10 });
});
it("responds ok:false for completely unknown request type", async () => {
@@ -116,14 +116,14 @@ describe("kernel + workflowManager integration", () => {
vi.clearAllMocks();
});
describe("sense signal triggers workflow via reflex", () => {
it("calls workflowManager.startWorkflow when a sense signal fires on a workflow reflex", async () => {
describe("sense compute triggers workflow via return value", () => {
it("calls workflowManager.startWorkflow when a sense compute returns a workflow launch", async () => {
const logStore = makeLogStore();
const config = makeConfig({
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "my-workflow", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "my-workflow": { concurrency: 2, overflow: "drop" } },
});
@@ -132,14 +132,20 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
// Emit a signal from "cpu-usage" on the bus
const { createSignalBus } = await import("../signal-bus.js");
void createSignalBus; // ensure import resolves
kernel.bus.emit({ id: 1, senseId: "cpu-usage", payload: { value: 80 }, ts: Date.now() });
// Simulate a sense worker sending a signal with workflow launch payload
// The kernel's handleWorkerMessage processes "signal" type messages
// and uses routeSenseComputeOutput to detect workflow launches
const workerPool = mockChildren[0];
if (workerPool) {
// Simulate the worker sending a signal message with workflow field
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "my-workflow|10|run this workflow" },
});
}
// The workflow worker should be spawned (one for the sense group, one for workflow)
// The sense group worker is mockChildren[0]; the workflow worker is mockChildren[1]
// We need to check that a start-thread message was sent to the workflow worker
// A workflow worker should be spawned and a start-thread message sent
const workflowWorker = mockChildren.find((c) =>
(c.send as ReturnType<typeof vi.fn>).mock.calls.some(
(args: unknown[]) =>
@@ -155,13 +161,13 @@ describe("kernel + workflowManager integration", () => {
await stopPromise;
});
it("passes the signal payload as triggerPayload to the workflow", async () => {
it("passes prompt and maxRounds from the workflow field to the workflow", async () => {
const logStore = makeLogStore();
const config = makeConfig({
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "alert-workflow", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "alert-workflow": { concurrency: 1, overflow: "drop" } },
});
@@ -170,8 +176,15 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
const payload = { level: "critical", value: 99 };
kernel.bus.emit({ id: 1, senseId: "cpu-usage", payload, ts: Date.now() });
// Simulate sense worker returning a workflow launch
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "alert-workflow|5|handle critical alert" },
});
}
// Find the start-thread call and verify triggerPayload
const startThreadCall = mockChildren
@@ -187,7 +200,8 @@ describe("kernel + workflowManager integration", () => {
expect(startThreadCall?.[0]).toMatchObject({
type: "start-thread",
workflow: "alert-workflow",
triggerPayload: payload,
prompt: "handle critical alert",
maxRounds: 5,
});
const stopPromise = kernel.stop();
@@ -202,7 +216,7 @@ describe("kernel + workflowManager integration", () => {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
"disk-io": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "my-workflow", on: ["disk-io"] } as any],
reflexes: [],
workflows: { "my-workflow": { concurrency: 1, overflow: "drop" } },
});
@@ -211,10 +225,17 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
// Emit signal from cpu-usage — NOT in the workflow's "on" list
kernel.bus.emit({ id: 1, senseId: "cpu-usage", payload: 50, ts: Date.now() });
// Emit a regular signal (no workflow field) — should NOT trigger any workflow
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: 50,
});
}
// No workflow worker should have been spawned (only the sense group worker)
// No workflow should have been started
const workflowWorkerSpawned = mockChildren.some((c) =>
(c.send as ReturnType<typeof vi.fn>).mock.calls.some(
(args: unknown[]) =>
@@ -232,13 +253,13 @@ describe("kernel + workflowManager integration", () => {
});
describe("workflow events are logged", () => {
it("logs a 'started' event when workflow thread is triggered", async () => {
it("logs a 'started' event when workflow thread is triggered via sense compute", async () => {
const logStore = makeLogStore();
const config = makeConfig({
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "log-test-workflow", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "log-test-workflow": { concurrency: 2, overflow: "drop" } },
});
@@ -247,7 +268,15 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
kernel.bus.emit({ id: 1, senseId: "cpu-usage", payload: null, ts: Date.now() });
// Simulate sense compute returning a workflow launch
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "log-test-workflow|10|test prompt" },
});
}
expect(logStore.upsertWorkflowRun).toHaveBeenCalledWith(
expect.objectContaining({ source: "workflow", type: "started" }),
@@ -261,7 +290,7 @@ describe("kernel + workflowManager integration", () => {
});
describe("reloadConfig handles workflow changes", () => {
it("new workflow reflexes are active after reloadConfig", async () => {
it("new workflows are available after reloadConfig", async () => {
const logStore = makeLogStore();
const initialConfig = makeConfig({
senses: {
@@ -277,19 +306,26 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
// Reload with a workflow reflex added
// Reload with a workflow added
const newConfig: NerveConfig = {
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "new-workflow", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "new-workflow": { concurrency: 1, overflow: "drop" } },
maxRounds: 10,
};
kernel.reloadConfig(newConfig);
// Now emit a signal — should trigger the new workflow
kernel.bus.emit({ id: 2, senseId: "cpu-usage", payload: "reload-test", ts: Date.now() });
// Simulate sense compute returning a workflow launch for the new workflow
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "new-workflow|10|reload test" },
});
}
const startThreadCall = mockChildren
.flatMap((c) => (c.send as ReturnType<typeof vi.fn>).mock.calls as [unknown][])
@@ -308,13 +344,13 @@ describe("kernel + workflowManager integration", () => {
await stopPromise;
});
it("old workflow reflexes are removed after reloadConfig", async () => {
it("old workflows are removed after reloadConfig", async () => {
const logStore = makeLogStore();
const initialConfig = makeConfig({
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "old-workflow", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "old-workflow": { concurrency: 1, overflow: "drop" } },
});
@@ -323,7 +359,7 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
// Reload with the workflow reflex removed
// Reload with the workflow removed
const newConfig: NerveConfig = {
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
@@ -339,8 +375,15 @@ describe("kernel + workflowManager integration", () => {
(c.send as ReturnType<typeof vi.fn>).mockClear();
}
// Emit a signal — old-workflow should NOT be triggered
kernel.bus.emit({ id: 3, senseId: "cpu-usage", payload: "after-reload", ts: Date.now() });
// Simulate sense compute trying to launch the old workflow — it should still not start
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "old-workflow|10|should not work" },
});
}
const startThreadCall = mockChildren
.flatMap((c) => (c.send as ReturnType<typeof vi.fn>).mock.calls as [unknown][])
@@ -366,7 +409,7 @@ describe("kernel + workflowManager integration", () => {
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "shutdown-test", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "shutdown-test": { concurrency: 1, overflow: "drop" } },
});
@@ -375,8 +418,15 @@ describe("kernel + workflowManager integration", () => {
logStore,
});
// Trigger a workflow so a worker is spawned
kernel.bus.emit({ id: 1, senseId: "cpu-usage", payload: null, ts: Date.now() });
// Trigger a workflow via sense compute return value
const workerPool = mockChildren[0];
if (workerPool) {
workerPool.emit("message", {
type: "signal",
sense: "cpu-usage",
payload: { workflow: "shutdown-test|10|test" },
});
}
const stopPromise = kernel.stop();
await vi.runAllTimersAsync();
@@ -408,7 +458,7 @@ describe("kernel + workflowManager integration", () => {
senses: {
"cpu-usage": { group: "system", throttle: null, timeout: null, gracePeriod: null },
},
reflexes: [{ kind: "workflow", workflow: "health-wf", on: ["cpu-usage"] } as any],
reflexes: [],
workflows: { "health-wf": { concurrency: 2, overflow: "drop" } },
});