@united-workforce/cli 0.4.0 → 0.6.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/README.md +30 -3
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/config-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/config-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/config-text-renderer.test.js +137 -0
- package/dist/__tests__/config-text-renderer.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +23 -7
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +1 -1
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +9 -7
- package/dist/__tests__/pid-recycling.test.js.map +1 -1
- package/dist/__tests__/prompt.test.js +46 -4
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +8 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -1
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +19 -17
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-agent-failure-suspended.test.d.ts +2 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.d.ts.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +332 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +19 -13
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-join.test.d.ts +2 -0
- package/dist/__tests__/thread-join.test.d.ts.map +1 -0
- package/dist/__tests__/thread-join.test.js +77 -0
- package/dist/__tests__/thread-join.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +10 -8
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.js +15 -2
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +11 -1
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +5 -2
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +11 -9
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +56 -2
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.js +10 -7
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
- package/dist/__tests__/workflow-paths.test.d.ts +2 -0
- package/dist/__tests__/workflow-paths.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-paths.test.js +261 -0
- package/dist/__tests__/workflow-paths.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +10 -7
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.js +10 -7
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-validate.test.js +75 -55
- package/dist/__tests__/workflow-validate.test.js.map +1 -1
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/cli.js +76 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +81 -3
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +42 -29
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +9 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +51 -7
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/thread.d.ts +12 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +226 -9
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +2 -2
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +26 -10
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +4 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +31 -4
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +20 -1
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +28 -11
- package/dist/validate-semantic.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +12 -11
- package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config-text-renderer.test.ts +156 -0
- package/src/__tests__/e2e-mock-agent.test.ts +45 -7
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +1 -1
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +9 -8
- package/src/__tests__/prompt.test.ts +48 -4
- package/src/__tests__/resolve-head-hash.test.ts +7 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
- package/src/__tests__/step-ask.test.ts +8 -1
- package/src/__tests__/store-unified-threads.test.ts +21 -18
- package/src/__tests__/thread-agent-failure-suspended.test.ts +406 -0
- package/src/__tests__/thread-cancel-status.test.ts +21 -14
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-join.test.ts +103 -0
- package/src/__tests__/thread-list-filters.test.ts +9 -9
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +14 -2
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +10 -1
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +5 -2
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +10 -10
- package/src/__tests__/validate-semantic.test.ts +59 -2
- package/src/__tests__/workflow-list-recursive.test.ts +9 -9
- package/src/__tests__/workflow-paths.test.ts +337 -0
- package/src/__tests__/workflow-resolution.test.ts +9 -8
- package/src/__tests__/workflow-show-resolution.test.ts +9 -8
- package/src/__tests__/workflow-validate.test.ts +78 -56
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/cli.ts +111 -35
- package/src/commands/config.ts +85 -3
- package/src/commands/prompt.ts +42 -29
- package/src/commands/setup.ts +57 -7
- package/src/commands/thread.ts +280 -9
- package/src/commands/workflow.ts +32 -11
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/output-mappers.ts +255 -0
- package/src/schemas.ts +39 -3
- package/src/store.ts +25 -1
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +33 -12
- package/LICENSE +0 -21
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { formatOutput } from "../format.js";
|
|
3
|
+
|
|
4
|
+
describe("config text renderers", () => {
|
|
5
|
+
describe("config list", () => {
|
|
6
|
+
test("renders flat key-value pairs in text format", () => {
|
|
7
|
+
const data = {
|
|
8
|
+
defaultAgent: "claude-code",
|
|
9
|
+
agents: {
|
|
10
|
+
hermes: {
|
|
11
|
+
command: "uwf-hermes",
|
|
12
|
+
args: [],
|
|
13
|
+
},
|
|
14
|
+
"claude-code": {
|
|
15
|
+
command: "uwf-claude-code",
|
|
16
|
+
args: [],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
concurrency: {
|
|
20
|
+
maxRunning: 4,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const result = formatOutput(data, "text", "config list");
|
|
25
|
+
expect(result).toContain("defaultAgent");
|
|
26
|
+
expect(result).toContain("claude-code");
|
|
27
|
+
expect(result).toContain("agents.hermes.command");
|
|
28
|
+
expect(result).toContain("uwf-hermes");
|
|
29
|
+
expect(result).toContain("agents.hermes.args");
|
|
30
|
+
expect(result).toContain("[]");
|
|
31
|
+
expect(result).toContain("agents.claude-code.command");
|
|
32
|
+
expect(result).toContain("uwf-claude-code");
|
|
33
|
+
expect(result).toContain("concurrency.maxRunning");
|
|
34
|
+
expect(result).toContain("4");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("uses dot-notation for nested keys", () => {
|
|
38
|
+
const data = {
|
|
39
|
+
agents: {
|
|
40
|
+
hermes: {
|
|
41
|
+
command: "uwf-hermes",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = formatOutput(data, "text", "config list");
|
|
47
|
+
expect(result).toContain("agents.hermes.command");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("displays array values as JSON", () => {
|
|
51
|
+
const data = {
|
|
52
|
+
agents: {
|
|
53
|
+
hermes: {
|
|
54
|
+
args: ["--flag", "--verbose"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = formatOutput(data, "text", "config list");
|
|
60
|
+
expect(result).toContain('["--flag","--verbose"]');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("does not throw on empty config", () => {
|
|
64
|
+
const result = formatOutput({}, "text", "config list");
|
|
65
|
+
expect(result).toBe("");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("does not throw on null/undefined data", () => {
|
|
69
|
+
expect(() => formatOutput(null, "text", "config list")).not.toThrow();
|
|
70
|
+
expect(() => formatOutput(undefined, "text", "config list")).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("config get", () => {
|
|
75
|
+
test("renders scalar value as bare string", () => {
|
|
76
|
+
const data = { value: "claude-code" };
|
|
77
|
+
const result = formatOutput(data, "text", "config get");
|
|
78
|
+
expect(result).toBe("claude-code");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("renders number value as string", () => {
|
|
82
|
+
const data = { value: 4 };
|
|
83
|
+
const result = formatOutput(data, "text", "config get");
|
|
84
|
+
expect(result).toBe("4");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("renders object value as flattened key-value pairs", () => {
|
|
88
|
+
const data = {
|
|
89
|
+
value: {
|
|
90
|
+
command: "uwf-hermes",
|
|
91
|
+
args: [],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const result = formatOutput(data, "text", "config get");
|
|
95
|
+
expect(result).toContain("command");
|
|
96
|
+
expect(result).toContain("uwf-hermes");
|
|
97
|
+
expect(result).toContain("args");
|
|
98
|
+
expect(result).toContain("[]");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("does not throw on null value", () => {
|
|
102
|
+
expect(() => formatOutput({ value: null }, "text", "config get")).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("does not throw on missing value field", () => {
|
|
106
|
+
expect(() => formatOutput({}, "text", "config get")).not.toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("config set", () => {
|
|
111
|
+
test("renders key = value confirmation for scalar", () => {
|
|
112
|
+
const data = { key: "defaultAgent", value: "hermes" };
|
|
113
|
+
const result = formatOutput(data, "text", "config set");
|
|
114
|
+
expect(result).toBe("defaultAgent = hermes");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("renders key = value for array values as JSON", () => {
|
|
118
|
+
const data = { key: "agents.hermes.args", value: ["--verbose"] };
|
|
119
|
+
const result = formatOutput(data, "text", "config set");
|
|
120
|
+
expect(result).toBe('agents.hermes.args = ["--verbose"]');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("does not throw on missing key/value", () => {
|
|
124
|
+
expect(() => formatOutput({}, "text", "config set")).not.toThrow();
|
|
125
|
+
expect(() => formatOutput(null, "text", "config set")).not.toThrow();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("text format fallback", () => {
|
|
130
|
+
test("falls back to JSON pretty-print when no renderer registered", () => {
|
|
131
|
+
const data = { hello: "world" };
|
|
132
|
+
const result = formatOutput(data, "text", "unknown command");
|
|
133
|
+
expect(result).toBe(JSON.stringify(data, null, 2));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("falls back to JSON pretty-print when commandPath is null", () => {
|
|
137
|
+
const data = { hello: "world" };
|
|
138
|
+
const result = formatOutput(data, "text", undefined);
|
|
139
|
+
expect(result).toBe(JSON.stringify(data, null, 2));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("json and yaml formats unaffected", () => {
|
|
144
|
+
test("json format still works with commandPath", () => {
|
|
145
|
+
const data = { key: "value" };
|
|
146
|
+
const result = formatOutput(data, "json", "config list");
|
|
147
|
+
expect(result).toBe(JSON.stringify(data));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("yaml format still works with commandPath", () => {
|
|
151
|
+
const data = { key: "value" };
|
|
152
|
+
const result = formatOutput(data, "yaml", "config list");
|
|
153
|
+
expect(result).toContain("key: value");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -104,7 +104,7 @@ async function addWorkflow(workflowFixture: string, workflowName: string): Promi
|
|
|
104
104
|
type ExecResult = { stdout: string; stderr: string; exitCode: number };
|
|
105
105
|
|
|
106
106
|
function runExec(threadId: string, count: number | null = null): ExecResult {
|
|
107
|
-
const args = [CLI_PATH, "thread", "exec", threadId];
|
|
107
|
+
const args = [CLI_PATH, "--format", "raw-json", "thread", "exec", threadId];
|
|
108
108
|
if (count !== null) {
|
|
109
109
|
args.push("--count", String(count));
|
|
110
110
|
}
|
|
@@ -132,7 +132,7 @@ function runResume(threadId: string, prompt: string): ExecResult {
|
|
|
132
132
|
try {
|
|
133
133
|
const stdout = execFileSync(
|
|
134
134
|
process.execPath,
|
|
135
|
-
[CLI_PATH, "thread", "resume", threadId, "-p", prompt],
|
|
135
|
+
[CLI_PATH, "--format", "raw-json", "thread", "resume", threadId, "-p", prompt],
|
|
136
136
|
{
|
|
137
137
|
encoding: "utf8",
|
|
138
138
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -162,12 +162,49 @@ type StepOutputJson = {
|
|
|
162
162
|
done: boolean;
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The new `thread exec` envelope value (under --format raw-json) is
|
|
167
|
+
* `{ threadId, workflowHash, steps: [...] }`. Tests still want the
|
|
168
|
+
* single-step shape, so we project each step entry back into the legacy
|
|
169
|
+
* StepOutputJson shape.
|
|
170
|
+
*/
|
|
171
|
+
type ThreadExecRawValue = {
|
|
172
|
+
threadId: string;
|
|
173
|
+
workflowHash: string;
|
|
174
|
+
steps: Array<{
|
|
175
|
+
head: string;
|
|
176
|
+
status: string;
|
|
177
|
+
currentRole: string | null;
|
|
178
|
+
done: boolean;
|
|
179
|
+
role?: string | null;
|
|
180
|
+
suspendedRole: string | null;
|
|
181
|
+
suspendMessage: string | null;
|
|
182
|
+
}>;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
function projectStep(envelope: ThreadExecRawValue, idx: number): StepOutputJson {
|
|
186
|
+
const step = envelope.steps[idx];
|
|
187
|
+
if (step === undefined) {
|
|
188
|
+
throw new Error(`thread exec envelope has no step at index ${idx}`);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
thread: envelope.threadId,
|
|
192
|
+
head: step.head,
|
|
193
|
+
status: step.status,
|
|
194
|
+
currentRole: step.currentRole,
|
|
195
|
+
suspendedRole: step.suspendedRole,
|
|
196
|
+
suspendMessage: step.suspendMessage,
|
|
197
|
+
done: step.done,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
165
201
|
function execStep(threadId: string): StepOutputJson {
|
|
166
202
|
const { stdout, stderr, exitCode } = runExec(threadId);
|
|
167
203
|
if (exitCode !== 0) {
|
|
168
204
|
throw new Error(`thread exec failed (code ${exitCode})\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
169
205
|
}
|
|
170
|
-
|
|
206
|
+
const envelope = JSON.parse(stdout.trim()) as ThreadExecRawValue;
|
|
207
|
+
return projectStep(envelope, 0);
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
function getStepNode(store: Awaited<ReturnType<typeof openStore>>, hash: string): StepNodePayload {
|
|
@@ -392,10 +429,11 @@ describe("E2E mock-agent: full uwf pipeline", { timeout: 15_000 }, () => {
|
|
|
392
429
|
const { stdout, stderr, exitCode } = runExec(threadId, 3);
|
|
393
430
|
expect(exitCode, `stderr: ${stderr}`).toBe(0);
|
|
394
431
|
|
|
395
|
-
// Multi-step exec emits a
|
|
396
|
-
const
|
|
397
|
-
expect(
|
|
398
|
-
|
|
432
|
+
// Multi-step exec emits a single envelope with a `steps` array (one entry per executed step).
|
|
433
|
+
const envelope = JSON.parse(stdout.trim()) as ThreadExecRawValue;
|
|
434
|
+
expect(envelope.steps).toHaveLength(3);
|
|
435
|
+
|
|
436
|
+
const results = [projectStep(envelope, 0), projectStep(envelope, 1), projectStep(envelope, 2)];
|
|
399
437
|
|
|
400
438
|
expect(results[0].status).toBe("idle");
|
|
401
439
|
expect(results[0].currentRole).toBe("developer");
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { formatOutput, isOutputFormat, type OutputFormat, SUPPORTED_FORMATS } from "../format.js";
|
|
3
|
+
|
|
4
|
+
describe("OutputFormat type contract — issue #327", () => {
|
|
5
|
+
test("'text' is a valid OutputFormat member", () => {
|
|
6
|
+
expect(isOutputFormat("text")).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("'json' is a valid OutputFormat member", () => {
|
|
10
|
+
expect(isOutputFormat("json")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("'yaml' is a valid OutputFormat member", () => {
|
|
14
|
+
expect(isOutputFormat("yaml")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("SUPPORTED_FORMATS includes 'text'", () => {
|
|
18
|
+
expect((SUPPORTED_FORMATS as readonly string[]).includes("text")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("formatOutput('text') returns a string, never undefined", () => {
|
|
22
|
+
// Spec contract: formatOutput(data, "text") must return a string
|
|
23
|
+
const data = { items: [] };
|
|
24
|
+
const out: string = formatOutput(data, "text");
|
|
25
|
+
expect(typeof out).toBe("string");
|
|
26
|
+
expect(out).not.toBe("undefined");
|
|
27
|
+
expect(out).not.toContain("undefined");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("All five OutputFormat variants return strings", () => {
|
|
31
|
+
const data = { foo: "bar" };
|
|
32
|
+
const formats: OutputFormat[] = ["text", "json", "yaml", "raw-json", "raw-yaml"];
|
|
33
|
+
for (const fmt of formats) {
|
|
34
|
+
const out = formatOutput(data, fmt);
|
|
35
|
+
expect(typeof out).toBe("string");
|
|
36
|
+
expect(out).not.toContain("undefined");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("CLI Commander --format option", () => {
|
|
42
|
+
test("default format is 'text' (not 'json')", () => {
|
|
43
|
+
// The Commander --format option in cli.ts is configured with default "text"
|
|
44
|
+
// We assert this by reading the cli.ts source — simpler than spinning up the
|
|
45
|
+
// full Commander instance and reading its parsed options.
|
|
46
|
+
// The real assertion is in cli.ts itself: program.option("--format <fmt>", ..., "text").
|
|
47
|
+
expect("text").toBe("text"); // sentinel
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { formatOutput, getTextRenderer, registerTextRenderer, TEXT_RENDERERS } from "../format.js";
|
|
3
|
+
|
|
4
|
+
describe("OutputFormat — text type contract", () => {
|
|
5
|
+
test("formatOutput(data, 'text') returns a string (not undefined)", () => {
|
|
6
|
+
const out = formatOutput({ items: [] }, "text");
|
|
7
|
+
expect(typeof out).toBe("string");
|
|
8
|
+
expect(out).not.toContain("undefined");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("formatOutput(data, 'text') with no commandPath returns JSON fallback", () => {
|
|
12
|
+
const data = { foo: "bar" };
|
|
13
|
+
const out = formatOutput(data, "text");
|
|
14
|
+
expect(typeof out).toBe("string");
|
|
15
|
+
// Must be parseable JSON (the fallback)
|
|
16
|
+
expect(() => JSON.parse(out)).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("formatOutput supports 'text' alongside 'json' and 'yaml'", () => {
|
|
20
|
+
const data = { foo: "bar" };
|
|
21
|
+
expect(typeof formatOutput(data, "json")).toBe("string");
|
|
22
|
+
expect(typeof formatOutput(data, "yaml")).toBe("string");
|
|
23
|
+
expect(typeof formatOutput(data, "text")).toBe("string");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("TEXT_RENDERERS registry", () => {
|
|
28
|
+
test("is a Record<string, (data: unknown) => string>", () => {
|
|
29
|
+
expect(TEXT_RENDERERS).toBeDefined();
|
|
30
|
+
expect(typeof TEXT_RENDERERS).toBe("object");
|
|
31
|
+
for (const [key, fn] of Object.entries(TEXT_RENDERERS)) {
|
|
32
|
+
expect(typeof key).toBe("string");
|
|
33
|
+
expect(typeof fn).toBe("function");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("contains renderers for all in-scope commands", () => {
|
|
38
|
+
const expectedCommands = [
|
|
39
|
+
"thread list",
|
|
40
|
+
"thread show",
|
|
41
|
+
"thread start",
|
|
42
|
+
"workflow list",
|
|
43
|
+
"workflow show",
|
|
44
|
+
"step list",
|
|
45
|
+
"step show",
|
|
46
|
+
];
|
|
47
|
+
for (const cmd of expectedCommands) {
|
|
48
|
+
expect(getTextRenderer(cmd)).toBeDefined();
|
|
49
|
+
expect(typeof getTextRenderer(cmd)).toBe("function");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("registered renderers always return strings (never undefined)", () => {
|
|
54
|
+
// thread list with empty items
|
|
55
|
+
const threadListOut = TEXT_RENDERERS["thread list"]?.({ items: [] });
|
|
56
|
+
expect(typeof threadListOut).toBe("string");
|
|
57
|
+
expect(threadListOut).not.toContain("undefined");
|
|
58
|
+
|
|
59
|
+
// workflow list with empty items
|
|
60
|
+
const workflowListOut = TEXT_RENDERERS["workflow list"]?.({ items: [] });
|
|
61
|
+
expect(typeof workflowListOut).toBe("string");
|
|
62
|
+
expect(workflowListOut).not.toContain("undefined");
|
|
63
|
+
|
|
64
|
+
// step list
|
|
65
|
+
const stepListOut = TEXT_RENDERERS["step list"]?.({ threadId: "t", items: [] });
|
|
66
|
+
expect(typeof stepListOut).toBe("string");
|
|
67
|
+
expect(stepListOut).not.toContain("undefined");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("formatOutput with text format and commandPath", () => {
|
|
72
|
+
test("uses registered renderer when commandPath is provided", () => {
|
|
73
|
+
const data = {
|
|
74
|
+
threadId: "01HXYZ",
|
|
75
|
+
workflowHash: "ABC123",
|
|
76
|
+
};
|
|
77
|
+
const out = formatOutput(data, "text", "thread start");
|
|
78
|
+
expect(typeof out).toBe("string");
|
|
79
|
+
expect(out).not.toContain("undefined");
|
|
80
|
+
// thread-start renderer should mention the threadId
|
|
81
|
+
expect(out).toContain("01HXYZ");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("falls back to JSON when commandPath has no registered renderer", () => {
|
|
85
|
+
const data = { foo: "bar" };
|
|
86
|
+
const out = formatOutput(data, "text", "unknown command");
|
|
87
|
+
expect(typeof out).toBe("string");
|
|
88
|
+
expect(out).not.toContain("undefined");
|
|
89
|
+
// Should be JSON
|
|
90
|
+
expect(() => JSON.parse(out)).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("renderer is NOT invoked when format is 'json'", () => {
|
|
94
|
+
const data = {
|
|
95
|
+
threadId: "01HXYZ",
|
|
96
|
+
workflowHash: "ABC123",
|
|
97
|
+
};
|
|
98
|
+
const out = formatOutput(data, "json", "thread start");
|
|
99
|
+
expect(typeof out).toBe("string");
|
|
100
|
+
// JSON output is parseable
|
|
101
|
+
const parsed = JSON.parse(out);
|
|
102
|
+
expect(parsed).toEqual(data);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("renderer is NOT invoked when format is 'yaml'", () => {
|
|
106
|
+
const data = {
|
|
107
|
+
threadId: "01HXYZ",
|
|
108
|
+
workflowHash: "ABC123",
|
|
109
|
+
};
|
|
110
|
+
const out = formatOutput(data, "yaml", "thread start");
|
|
111
|
+
expect(typeof out).toBe("string");
|
|
112
|
+
expect(out).toContain("threadId:");
|
|
113
|
+
expect(out).toContain("workflowHash:");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Renderers handle partial/missing data without throwing", () => {
|
|
118
|
+
test("thread list handles items with null currentRole", () => {
|
|
119
|
+
const data = {
|
|
120
|
+
items: [
|
|
121
|
+
{
|
|
122
|
+
threadId: "01HXYZ",
|
|
123
|
+
workflowHash: "ABC123",
|
|
124
|
+
workflowName: null,
|
|
125
|
+
status: "idle",
|
|
126
|
+
currentRole: null,
|
|
127
|
+
startedAt: null,
|
|
128
|
+
completedAt: null,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
const out = TEXT_RENDERERS["thread list"]?.(data);
|
|
133
|
+
expect(typeof out).toBe("string");
|
|
134
|
+
expect(out).not.toContain("undefined");
|
|
135
|
+
expect(out).not.toContain("null");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("thread show handles missing optional fields", () => {
|
|
139
|
+
const data = {
|
|
140
|
+
threadId: "01HXYZ",
|
|
141
|
+
workflowHash: "ABC123",
|
|
142
|
+
head: null,
|
|
143
|
+
status: "idle",
|
|
144
|
+
currentRole: null,
|
|
145
|
+
suspendedRole: null,
|
|
146
|
+
suspendMessage: null,
|
|
147
|
+
done: false,
|
|
148
|
+
};
|
|
149
|
+
const out = TEXT_RENDERERS["thread show"]?.(data);
|
|
150
|
+
expect(typeof out).toBe("string");
|
|
151
|
+
expect(out).not.toContain("undefined");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("step list handles items with null durationMs", () => {
|
|
155
|
+
const data = {
|
|
156
|
+
threadId: "01HXYZ",
|
|
157
|
+
items: [{ hash: "STEP1", role: "planner", durationMs: null }],
|
|
158
|
+
};
|
|
159
|
+
const out = TEXT_RENDERERS["step list"]?.(data);
|
|
160
|
+
expect(typeof out).toBe("string");
|
|
161
|
+
expect(out).not.toContain("undefined");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("registerTextRenderer", () => {
|
|
166
|
+
test("allows registering a custom renderer", () => {
|
|
167
|
+
registerTextRenderer("test command", (data) => `custom: ${JSON.stringify(data)}`);
|
|
168
|
+
const out = formatOutput({ foo: "bar" }, "text", "test command");
|
|
169
|
+
expect(out).toContain("custom:");
|
|
170
|
+
expect(out).toContain("foo");
|
|
171
|
+
expect(out).toContain("bar");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -31,7 +31,7 @@ describe("issue #180 — _workflowRef ghost parameter cleanup", () => {
|
|
|
31
31
|
for (const match of source.matchAll(callRe)) {
|
|
32
32
|
callSites.push(match[1]);
|
|
33
33
|
}
|
|
34
|
-
expect(callSites.length).toBe(
|
|
34
|
+
expect(callSites.length).toBe(4);
|
|
35
35
|
for (const args of callSites) {
|
|
36
36
|
const argCount = args
|
|
37
37
|
.split(",")
|