@united-workforce/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +89 -1
- package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
- package/dist/__tests__/broker-prompt.test.d.ts +10 -0
- package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/broker-prompt.test.js +129 -0
- package/dist/__tests__/broker-prompt.test.js.map +1 -0
- package/dist/__tests__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.js +278 -0
- package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +17 -5
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.js +5 -2
- package/dist/__tests__/setup-no-llm.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -6
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.js +6 -6
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +2 -2
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +1 -1
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/commands/broker-step.d.ts +110 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +450 -0
- package/dist/commands/broker-step.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -23
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.js +3 -3
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +8 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +6 -5
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +11 -154
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +4 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +77 -151
- package/dist/commands/thread.js.map +1 -1
- package/package.json +12 -11
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/setup-agent-discovery.test.ts +17 -5
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- package/src/__tests__/thread-poke.test.ts +6 -6
- package/src/__tests__/thread-resume.test.ts +2 -2
- package/src/__tests__/thread-suspend-step.test.ts +1 -1
- package/src/cli.ts +0 -0
- package/src/commands/broker-step.ts +636 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +3 -3
- package/src/commands/setup.ts +9 -1
- package/src/commands/step.ts +21 -204
- package/src/commands/thread.ts +87 -192
- package/dist/.build-fingerprint +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.js +0 -79
- package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
- package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
- package/src/__tests__/spawn-agent-json.test.ts +0 -100
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { putSchema } from "@ocas/core";
|
|
7
|
-
import { openStore } from "@ocas/fs";
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
9
|
-
import { registerUwfSchemas } from "../schemas.js";
|
|
10
|
-
import { seedThreads } from "./thread-test-helpers.js";
|
|
11
|
-
// ── schemas ──────────────────────────────────────────────────────────────────
|
|
12
|
-
const OUTPUT_SCHEMA = {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
$status: { type: "string", enum: ["done", "failed"] },
|
|
16
|
-
result: { type: "string" },
|
|
17
|
-
},
|
|
18
|
-
required: ["$status"],
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
};
|
|
21
|
-
// ── fixture ──────────────────────────────────────────────────────────────────
|
|
22
|
-
let tmpDir;
|
|
23
|
-
let savedOcasHome;
|
|
24
|
-
beforeEach(async () => {
|
|
25
|
-
savedOcasHome = process.env.OCAS_HOME;
|
|
26
|
-
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-roundtrip-test-"));
|
|
27
|
-
});
|
|
28
|
-
afterEach(async () => {
|
|
29
|
-
if (savedOcasHome === undefined) {
|
|
30
|
-
delete process.env.OCAS_HOME;
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
process.env.OCAS_HOME = savedOcasHome;
|
|
34
|
-
}
|
|
35
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
36
|
-
});
|
|
37
|
-
describe("C1: adapter JSON round-trip integration", () => {
|
|
38
|
-
test("mock agent outputs JSON, CLI parses it and updates thread head in CAS", async () => {
|
|
39
|
-
// 1. Set up CAS store with workflow, start node, and output schema
|
|
40
|
-
const casDir = join(tmpDir, "cas");
|
|
41
|
-
await mkdir(casDir, { recursive: true });
|
|
42
|
-
const store = await openStore(casDir);
|
|
43
|
-
const schemas = await registerUwfSchemas(store);
|
|
44
|
-
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
|
|
45
|
-
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
46
|
-
name: "test-roundtrip",
|
|
47
|
-
description: "roundtrip integration test",
|
|
48
|
-
roles: {
|
|
49
|
-
worker: {
|
|
50
|
-
description: "Worker role",
|
|
51
|
-
goal: "Do work",
|
|
52
|
-
capabilities: [],
|
|
53
|
-
procedure: "work",
|
|
54
|
-
output: "result",
|
|
55
|
-
frontmatter: outputSchemaHash,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
graph: {
|
|
59
|
-
$START: {
|
|
60
|
-
new: { role: "worker", prompt: "Do the work", location: null },
|
|
61
|
-
resume: { role: "worker", prompt: "Resume the work", location: null },
|
|
62
|
-
},
|
|
63
|
-
worker: { done: { role: "$END", prompt: "completed", location: null } },
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const startHash = await store.cas.put(schemas.startNode, {
|
|
67
|
-
workflow: workflowHash,
|
|
68
|
-
prompt: "Test round-trip task",
|
|
69
|
-
});
|
|
70
|
-
process.env.OCAS_HOME = casDir;
|
|
71
|
-
const threadId = "01ROUNDTRIPTEST0000000000";
|
|
72
|
-
await seedThreads(tmpDir, { [threadId]: startHash });
|
|
73
|
-
// 2. Pre-create CAS nodes that the mock agent would produce
|
|
74
|
-
const outputHash = await store.cas.put(outputSchemaHash, {
|
|
75
|
-
$status: "done",
|
|
76
|
-
result: "test-ok",
|
|
77
|
-
});
|
|
78
|
-
// Use text schema for detail (simple placeholder)
|
|
79
|
-
const detailHash = await store.cas.put(schemas.text, "mock detail");
|
|
80
|
-
const startedAtMs = 1716600000000;
|
|
81
|
-
const completedAtMs = 1716600001500;
|
|
82
|
-
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
83
|
-
start: startHash,
|
|
84
|
-
prev: null,
|
|
85
|
-
role: "worker",
|
|
86
|
-
output: outputHash,
|
|
87
|
-
detail: detailHash,
|
|
88
|
-
agent: "uwf-mock",
|
|
89
|
-
edgePrompt: "Do the work",
|
|
90
|
-
startedAtMs,
|
|
91
|
-
completedAtMs,
|
|
92
|
-
cwd: tmpDir,
|
|
93
|
-
});
|
|
94
|
-
// 3. Create a minimal mock agent shell script that just outputs JSON
|
|
95
|
-
// The step node is already in CAS — the agent just needs to print the JSON line
|
|
96
|
-
const mockAgentPath = join(tmpDir, "mock-agent.sh");
|
|
97
|
-
const adapterJson = JSON.stringify({
|
|
98
|
-
stepHash,
|
|
99
|
-
detailHash,
|
|
100
|
-
role: "worker",
|
|
101
|
-
frontmatter: { $status: "done", result: "test-ok" },
|
|
102
|
-
body: "",
|
|
103
|
-
startedAtMs,
|
|
104
|
-
completedAtMs,
|
|
105
|
-
});
|
|
106
|
-
await writeFile(mockAgentPath, `#!/bin/sh\necho '${adapterJson}'\n`, { mode: 0o755 });
|
|
107
|
-
// 4. Write config.yaml
|
|
108
|
-
const configPath = join(tmpDir, "config.yaml");
|
|
109
|
-
await writeFile(configPath, `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`);
|
|
110
|
-
// 5. Run CLI with agent override pointing to our mock
|
|
111
|
-
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
|
|
112
|
-
let stdout;
|
|
113
|
-
let stderr;
|
|
114
|
-
let exitCode;
|
|
115
|
-
try {
|
|
116
|
-
stdout = execFileSync(process.execPath, [cliPath, "--format", "raw-json", "thread", "exec", threadId, "--agent", mockAgentPath], {
|
|
117
|
-
encoding: "utf8",
|
|
118
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
-
env: {
|
|
120
|
-
...process.env,
|
|
121
|
-
UWF_HOME: tmpDir,
|
|
122
|
-
OCAS_HOME: casDir,
|
|
123
|
-
},
|
|
124
|
-
cwd: tmpDir,
|
|
125
|
-
timeout: 30000,
|
|
126
|
-
});
|
|
127
|
-
stderr = "";
|
|
128
|
-
exitCode = 0;
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
const err = e;
|
|
132
|
-
stdout = err.stdout ?? "";
|
|
133
|
-
stderr = err.stderr ?? "";
|
|
134
|
-
exitCode = err.status ?? 1;
|
|
135
|
-
}
|
|
136
|
-
// 6. Verify
|
|
137
|
-
if (exitCode !== 0) {
|
|
138
|
-
throw new Error(`CLI exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
139
|
-
}
|
|
140
|
-
// Parse CLI output (raw-json envelope value: { threadId, workflowHash, steps: [...] })
|
|
141
|
-
const cliOutput = JSON.parse(stdout.trim());
|
|
142
|
-
expect(cliOutput).toHaveProperty("threadId", threadId);
|
|
143
|
-
expect(cliOutput.steps).toHaveLength(1);
|
|
144
|
-
const firstStep = cliOutput.steps[0];
|
|
145
|
-
expect(firstStep).toHaveProperty("head", stepHash);
|
|
146
|
-
expect(firstStep.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
|
|
147
|
-
// Verify the CAS step node exists and has correct metadata
|
|
148
|
-
const storeAfter = await openStore(casDir);
|
|
149
|
-
const stepNode = storeAfter.cas.get(firstStep.head);
|
|
150
|
-
expect(stepNode).not.toBeNull();
|
|
151
|
-
const payload = stepNode.payload;
|
|
152
|
-
expect(payload.role).toBe("worker");
|
|
153
|
-
expect(payload.agent).toBe("uwf-mock");
|
|
154
|
-
expect(payload.startedAtMs).toBe(1716600000000);
|
|
155
|
-
expect(payload.completedAtMs).toBe(1716600001500);
|
|
156
|
-
expect(payload.output).toBe(outputHash);
|
|
157
|
-
expect(payload.detail).toBe(detailHash);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
//# sourceMappingURL=adapter-json-roundtrip.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter-json-roundtrip.test.js","sourceRoot":"","sources":["../../src/__tests__/adapter-json-roundtrip.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-agent-json.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/spawn-agent-json.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
/**
|
|
3
|
-
* B-group tests: validate JSON parsing logic used by spawnAgent.
|
|
4
|
-
*
|
|
5
|
-
* We test the parsing logic inline since spawnAgent is a private function.
|
|
6
|
-
* These tests verify the contract: last line of stdout must be valid JSON
|
|
7
|
-
* with a valid stepHash CasRef.
|
|
8
|
-
*/
|
|
9
|
-
const CASREF_PATTERN = /^[0-9A-HJ-NP-TV-Z]{13}$/;
|
|
10
|
-
function isCasRef(s) {
|
|
11
|
-
return CASREF_PATTERN.test(s);
|
|
12
|
-
}
|
|
13
|
-
function parseAgentStdout(stdout) {
|
|
14
|
-
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
15
|
-
let parsed;
|
|
16
|
-
try {
|
|
17
|
-
parsed = JSON.parse(line);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
throw new Error(`agent stdout last line is not valid JSON: ${line || "(empty)"}`);
|
|
21
|
-
}
|
|
22
|
-
const obj = parsed;
|
|
23
|
-
if (typeof obj !== "object" ||
|
|
24
|
-
obj === null ||
|
|
25
|
-
typeof obj.stepHash !== "string" ||
|
|
26
|
-
!isCasRef(obj.stepHash)) {
|
|
27
|
-
throw new Error(`agent stdout JSON missing valid stepHash: ${line}`);
|
|
28
|
-
}
|
|
29
|
-
return obj;
|
|
30
|
-
}
|
|
31
|
-
const VALID_OUTPUT = {
|
|
32
|
-
stepHash: "0123456789ABC",
|
|
33
|
-
detailHash: "DEFGH12345678",
|
|
34
|
-
role: "planner",
|
|
35
|
-
frontmatter: { $status: "ready", plan: "somehash" },
|
|
36
|
-
body: "Plan body",
|
|
37
|
-
startedAtMs: 1000,
|
|
38
|
-
completedAtMs: 2000,
|
|
39
|
-
};
|
|
40
|
-
describe("spawnAgent JSON parsing", () => {
|
|
41
|
-
test("B1. parses valid JSON from agent stdout", () => {
|
|
42
|
-
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
|
|
43
|
-
const result = parseAgentStdout(stdout);
|
|
44
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
45
|
-
expect(result.detailHash).toBe("DEFGH12345678");
|
|
46
|
-
expect(result.role).toBe("planner");
|
|
47
|
-
expect(result.frontmatter).toEqual({ $status: "ready", plan: "somehash" });
|
|
48
|
-
expect(result.body).toBe("Plan body");
|
|
49
|
-
expect(result.startedAtMs).toBe(1000);
|
|
50
|
-
expect(result.completedAtMs).toBe(2000);
|
|
51
|
-
});
|
|
52
|
-
test("B2. extracts stepHash for head pointer", () => {
|
|
53
|
-
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
|
|
54
|
-
const result = parseAgentStdout(stdout);
|
|
55
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
56
|
-
expect(isCasRef(result.stepHash)).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
test("B3. handles debug lines before JSON", () => {
|
|
59
|
-
const debugLines = "[debug] loading context...\n[debug] running agent...\n";
|
|
60
|
-
const stdout = `${debugLines + JSON.stringify(VALID_OUTPUT)}\n`;
|
|
61
|
-
const result = parseAgentStdout(stdout);
|
|
62
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
63
|
-
});
|
|
64
|
-
test("B4. rejects non-JSON last line", () => {
|
|
65
|
-
const stdout = "not-json-at-all\n";
|
|
66
|
-
expect(() => parseAgentStdout(stdout)).toThrow("not valid JSON");
|
|
67
|
-
});
|
|
68
|
-
test("B5. rejects JSON missing stepHash", () => {
|
|
69
|
-
const incomplete = { detailHash: "DEFGH12345678", role: "planner" };
|
|
70
|
-
const stdout = `${JSON.stringify(incomplete)}\n`;
|
|
71
|
-
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
|
|
72
|
-
});
|
|
73
|
-
test("B6. rejects JSON with invalid stepHash", () => {
|
|
74
|
-
const bad = { ...VALID_OUTPUT, stepHash: "not-a-hash" };
|
|
75
|
-
const stdout = `${JSON.stringify(bad)}\n`;
|
|
76
|
-
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
//# sourceMappingURL=spawn-agent-json.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-agent-json.test.js","sourceRoot":"","sources":["../../src/__tests__/spawn-agent-json.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { putSchema } from "@ocas/core";
|
|
7
|
-
import { openStore } from "@ocas/fs";
|
|
8
|
-
import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol";
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
10
|
-
import { registerUwfSchemas } from "../schemas.js";
|
|
11
|
-
import { seedThreads } from "./thread-test-helpers.js";
|
|
12
|
-
|
|
13
|
-
// ── schemas ──────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const OUTPUT_SCHEMA = {
|
|
16
|
-
type: "object" as const,
|
|
17
|
-
properties: {
|
|
18
|
-
$status: { type: "string" as const, enum: ["done", "failed"] },
|
|
19
|
-
result: { type: "string" as const },
|
|
20
|
-
},
|
|
21
|
-
required: ["$status"],
|
|
22
|
-
additionalProperties: false,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// ── fixture ──────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
let tmpDir: string;
|
|
28
|
-
let savedOcasHome: string | undefined;
|
|
29
|
-
|
|
30
|
-
beforeEach(async () => {
|
|
31
|
-
savedOcasHome = process.env.OCAS_HOME;
|
|
32
|
-
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-roundtrip-test-"));
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(async () => {
|
|
36
|
-
if (savedOcasHome === undefined) {
|
|
37
|
-
delete process.env.OCAS_HOME;
|
|
38
|
-
} else {
|
|
39
|
-
process.env.OCAS_HOME = savedOcasHome;
|
|
40
|
-
}
|
|
41
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("C1: adapter JSON round-trip integration", () => {
|
|
45
|
-
test("mock agent outputs JSON, CLI parses it and updates thread head in CAS", async () => {
|
|
46
|
-
// 1. Set up CAS store with workflow, start node, and output schema
|
|
47
|
-
const casDir = join(tmpDir, "cas");
|
|
48
|
-
await mkdir(casDir, { recursive: true });
|
|
49
|
-
const store = await openStore(casDir);
|
|
50
|
-
const schemas = await registerUwfSchemas(store);
|
|
51
|
-
|
|
52
|
-
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
|
|
53
|
-
|
|
54
|
-
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
55
|
-
name: "test-roundtrip",
|
|
56
|
-
description: "roundtrip integration test",
|
|
57
|
-
roles: {
|
|
58
|
-
worker: {
|
|
59
|
-
description: "Worker role",
|
|
60
|
-
goal: "Do work",
|
|
61
|
-
capabilities: [],
|
|
62
|
-
procedure: "work",
|
|
63
|
-
output: "result",
|
|
64
|
-
frontmatter: outputSchemaHash,
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
graph: {
|
|
68
|
-
$START: {
|
|
69
|
-
new: { role: "worker", prompt: "Do the work", location: null },
|
|
70
|
-
resume: { role: "worker", prompt: "Resume the work", location: null },
|
|
71
|
-
},
|
|
72
|
-
worker: { done: { role: "$END", prompt: "completed", location: null } },
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const startHash = await store.cas.put(schemas.startNode, {
|
|
77
|
-
workflow: workflowHash,
|
|
78
|
-
prompt: "Test round-trip task",
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
process.env.OCAS_HOME = casDir;
|
|
82
|
-
|
|
83
|
-
const threadId = "01ROUNDTRIPTEST0000000000" as ThreadId;
|
|
84
|
-
await seedThreads(tmpDir, { [threadId]: startHash });
|
|
85
|
-
|
|
86
|
-
// 2. Pre-create CAS nodes that the mock agent would produce
|
|
87
|
-
const outputHash = await store.cas.put(outputSchemaHash, {
|
|
88
|
-
$status: "done",
|
|
89
|
-
result: "test-ok",
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Use text schema for detail (simple placeholder)
|
|
93
|
-
const detailHash = await store.cas.put(schemas.text, "mock detail");
|
|
94
|
-
|
|
95
|
-
const startedAtMs = 1716600000000;
|
|
96
|
-
const completedAtMs = 1716600001500;
|
|
97
|
-
|
|
98
|
-
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
99
|
-
start: startHash,
|
|
100
|
-
prev: null,
|
|
101
|
-
role: "worker",
|
|
102
|
-
output: outputHash,
|
|
103
|
-
detail: detailHash,
|
|
104
|
-
agent: "uwf-mock",
|
|
105
|
-
edgePrompt: "Do the work",
|
|
106
|
-
startedAtMs,
|
|
107
|
-
completedAtMs,
|
|
108
|
-
cwd: tmpDir,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// 3. Create a minimal mock agent shell script that just outputs JSON
|
|
112
|
-
// The step node is already in CAS — the agent just needs to print the JSON line
|
|
113
|
-
const mockAgentPath = join(tmpDir, "mock-agent.sh");
|
|
114
|
-
const adapterJson = JSON.stringify({
|
|
115
|
-
stepHash,
|
|
116
|
-
detailHash,
|
|
117
|
-
role: "worker",
|
|
118
|
-
frontmatter: { $status: "done", result: "test-ok" },
|
|
119
|
-
body: "",
|
|
120
|
-
startedAtMs,
|
|
121
|
-
completedAtMs,
|
|
122
|
-
});
|
|
123
|
-
await writeFile(mockAgentPath, `#!/bin/sh\necho '${adapterJson}'\n`, { mode: 0o755 });
|
|
124
|
-
|
|
125
|
-
// 4. Write config.yaml
|
|
126
|
-
const configPath = join(tmpDir, "config.yaml");
|
|
127
|
-
await writeFile(
|
|
128
|
-
configPath,
|
|
129
|
-
`defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
// 5. Run CLI with agent override pointing to our mock
|
|
133
|
-
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
|
|
134
|
-
let stdout: string;
|
|
135
|
-
let stderr: string;
|
|
136
|
-
let exitCode: number;
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
stdout = execFileSync(
|
|
140
|
-
process.execPath,
|
|
141
|
-
[cliPath, "--format", "raw-json", "thread", "exec", threadId, "--agent", mockAgentPath],
|
|
142
|
-
{
|
|
143
|
-
encoding: "utf8",
|
|
144
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
145
|
-
env: {
|
|
146
|
-
...process.env,
|
|
147
|
-
UWF_HOME: tmpDir,
|
|
148
|
-
OCAS_HOME: casDir,
|
|
149
|
-
},
|
|
150
|
-
cwd: tmpDir,
|
|
151
|
-
timeout: 30000,
|
|
152
|
-
},
|
|
153
|
-
);
|
|
154
|
-
stderr = "";
|
|
155
|
-
exitCode = 0;
|
|
156
|
-
} catch (e: unknown) {
|
|
157
|
-
const err = e as NodeJS.ErrnoException & {
|
|
158
|
-
stdout?: string;
|
|
159
|
-
stderr?: string;
|
|
160
|
-
status?: number;
|
|
161
|
-
};
|
|
162
|
-
stdout = err.stdout ?? "";
|
|
163
|
-
stderr = err.stderr ?? "";
|
|
164
|
-
exitCode = err.status ?? 1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 6. Verify
|
|
168
|
-
if (exitCode !== 0) {
|
|
169
|
-
throw new Error(`CLI exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Parse CLI output (raw-json envelope value: { threadId, workflowHash, steps: [...] })
|
|
173
|
-
const cliOutput = JSON.parse(stdout.trim());
|
|
174
|
-
expect(cliOutput).toHaveProperty("threadId", threadId);
|
|
175
|
-
expect(cliOutput.steps).toHaveLength(1);
|
|
176
|
-
const firstStep = cliOutput.steps[0];
|
|
177
|
-
expect(firstStep).toHaveProperty("head", stepHash);
|
|
178
|
-
expect(firstStep.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
|
|
179
|
-
|
|
180
|
-
// Verify the CAS step node exists and has correct metadata
|
|
181
|
-
const storeAfter = await openStore(casDir);
|
|
182
|
-
const stepNode = storeAfter.cas.get(firstStep.head as CasRef);
|
|
183
|
-
expect(stepNode).not.toBeNull();
|
|
184
|
-
|
|
185
|
-
const payload = stepNode!.payload as StepNodePayload;
|
|
186
|
-
expect(payload.role).toBe("worker");
|
|
187
|
-
expect(payload.agent).toBe("uwf-mock");
|
|
188
|
-
expect(payload.startedAtMs).toBe(1716600000000);
|
|
189
|
-
expect(payload.completedAtMs).toBe(1716600001500);
|
|
190
|
-
expect(payload.output).toBe(outputHash);
|
|
191
|
-
expect(payload.detail).toBe(detailHash);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* B-group tests: validate JSON parsing logic used by spawnAgent.
|
|
5
|
-
*
|
|
6
|
-
* We test the parsing logic inline since spawnAgent is a private function.
|
|
7
|
-
* These tests verify the contract: last line of stdout must be valid JSON
|
|
8
|
-
* with a valid stepHash CasRef.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const CASREF_PATTERN = /^[0-9A-HJ-NP-TV-Z]{13}$/;
|
|
12
|
-
|
|
13
|
-
function isCasRef(s: string): boolean {
|
|
14
|
-
return CASREF_PATTERN.test(s);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type AdapterOutput = {
|
|
18
|
-
stepHash: string;
|
|
19
|
-
detailHash: string;
|
|
20
|
-
role: string;
|
|
21
|
-
frontmatter: Record<string, unknown>;
|
|
22
|
-
body: string;
|
|
23
|
-
startedAtMs: number;
|
|
24
|
-
completedAtMs: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function parseAgentStdout(stdout: string): AdapterOutput {
|
|
28
|
-
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
29
|
-
let parsed: unknown;
|
|
30
|
-
try {
|
|
31
|
-
parsed = JSON.parse(line);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(`agent stdout last line is not valid JSON: ${line || "(empty)"}`);
|
|
34
|
-
}
|
|
35
|
-
const obj = parsed as Record<string, unknown>;
|
|
36
|
-
if (
|
|
37
|
-
typeof obj !== "object" ||
|
|
38
|
-
obj === null ||
|
|
39
|
-
typeof obj.stepHash !== "string" ||
|
|
40
|
-
!isCasRef(obj.stepHash as string)
|
|
41
|
-
) {
|
|
42
|
-
throw new Error(`agent stdout JSON missing valid stepHash: ${line}`);
|
|
43
|
-
}
|
|
44
|
-
return obj as unknown as AdapterOutput;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const VALID_OUTPUT: AdapterOutput = {
|
|
48
|
-
stepHash: "0123456789ABC",
|
|
49
|
-
detailHash: "DEFGH12345678",
|
|
50
|
-
role: "planner",
|
|
51
|
-
frontmatter: { $status: "ready", plan: "somehash" },
|
|
52
|
-
body: "Plan body",
|
|
53
|
-
startedAtMs: 1000,
|
|
54
|
-
completedAtMs: 2000,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
describe("spawnAgent JSON parsing", () => {
|
|
58
|
-
test("B1. parses valid JSON from agent stdout", () => {
|
|
59
|
-
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
|
|
60
|
-
const result = parseAgentStdout(stdout);
|
|
61
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
62
|
-
expect(result.detailHash).toBe("DEFGH12345678");
|
|
63
|
-
expect(result.role).toBe("planner");
|
|
64
|
-
expect(result.frontmatter).toEqual({ $status: "ready", plan: "somehash" });
|
|
65
|
-
expect(result.body).toBe("Plan body");
|
|
66
|
-
expect(result.startedAtMs).toBe(1000);
|
|
67
|
-
expect(result.completedAtMs).toBe(2000);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("B2. extracts stepHash for head pointer", () => {
|
|
71
|
-
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
|
|
72
|
-
const result = parseAgentStdout(stdout);
|
|
73
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
74
|
-
expect(isCasRef(result.stepHash)).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("B3. handles debug lines before JSON", () => {
|
|
78
|
-
const debugLines = "[debug] loading context...\n[debug] running agent...\n";
|
|
79
|
-
const stdout = `${debugLines + JSON.stringify(VALID_OUTPUT)}\n`;
|
|
80
|
-
const result = parseAgentStdout(stdout);
|
|
81
|
-
expect(result.stepHash).toBe("0123456789ABC");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("B4. rejects non-JSON last line", () => {
|
|
85
|
-
const stdout = "not-json-at-all\n";
|
|
86
|
-
expect(() => parseAgentStdout(stdout)).toThrow("not valid JSON");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("B5. rejects JSON missing stepHash", () => {
|
|
90
|
-
const incomplete = { detailHash: "DEFGH12345678", role: "planner" };
|
|
91
|
-
const stdout = `${JSON.stringify(incomplete)}\n`;
|
|
92
|
-
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("B6. rejects JSON with invalid stepHash", () => {
|
|
96
|
-
const bad = { ...VALID_OUTPUT, stepHash: "not-a-hash" };
|
|
97
|
-
const stdout = `${JSON.stringify(bad)}\n`;
|
|
98
|
-
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
|
|
99
|
-
});
|
|
100
|
-
});
|