@united-workforce/cli 0.2.1-rc.9 → 0.4.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.
Files changed (219) hide show
  1. package/README.md +15 -8
  2. package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
  3. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  4. package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
  5. package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
  6. package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
  7. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
  8. package/dist/__tests__/build-step-entry.test.d.ts +2 -0
  9. package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
  10. package/dist/__tests__/build-step-entry.test.js +173 -0
  11. package/dist/__tests__/build-step-entry.test.js.map +1 -0
  12. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
  13. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
  14. package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
  15. package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
  16. package/dist/__tests__/config.test.js +26 -302
  17. package/dist/__tests__/config.test.js.map +1 -1
  18. package/dist/__tests__/current-role.test.js +7 -6
  19. package/dist/__tests__/current-role.test.js.map +1 -1
  20. package/dist/__tests__/e2e-mock-agent.test.js +20 -23
  21. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  22. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
  23. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
  24. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
  25. package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
  26. package/dist/__tests__/moderator-evaluate.test.js +9 -50
  27. package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
  28. package/dist/__tests__/pid-recycling.test.d.ts +2 -0
  29. package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
  30. package/dist/__tests__/pid-recycling.test.js +271 -0
  31. package/dist/__tests__/pid-recycling.test.js.map +1 -0
  32. package/dist/__tests__/prompt.test.js +321 -0
  33. package/dist/__tests__/prompt.test.js.map +1 -1
  34. package/dist/__tests__/resolve-head-hash.test.js +4 -4
  35. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  36. package/dist/__tests__/setup-agent-discovery.test.js +21 -30
  37. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  38. package/dist/__tests__/setup-complexity.test.js +2 -168
  39. package/dist/__tests__/setup-complexity.test.js.map +1 -1
  40. package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
  41. package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
  42. package/dist/__tests__/setup-no-llm.test.js +52 -0
  43. package/dist/__tests__/setup-no-llm.test.js.map +1 -0
  44. package/dist/__tests__/solve-issue-tea-worktree.test.js +24 -27
  45. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  46. package/dist/__tests__/step-ask.test.d.ts +2 -0
  47. package/dist/__tests__/step-ask.test.d.ts.map +1 -0
  48. package/dist/__tests__/step-ask.test.js +499 -0
  49. package/dist/__tests__/step-ask.test.js.map +1 -0
  50. package/dist/__tests__/step-show-json.test.js +1 -0
  51. package/dist/__tests__/step-show-json.test.js.map +1 -1
  52. package/dist/__tests__/step-timing.test.js +2 -0
  53. package/dist/__tests__/step-timing.test.js.map +1 -1
  54. package/dist/__tests__/store-global-cas.test.js +2 -2
  55. package/dist/__tests__/store-global-cas.test.js.map +1 -1
  56. package/dist/__tests__/store-unified-threads.test.js +9 -9
  57. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  58. package/dist/__tests__/thread-cancel-status.test.js +6 -6
  59. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  60. package/dist/__tests__/thread-list-filters.test.js +344 -9
  61. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  62. package/dist/__tests__/thread-poke.test.d.ts +2 -0
  63. package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
  64. package/dist/__tests__/thread-poke.test.js +412 -0
  65. package/dist/__tests__/thread-poke.test.js.map +1 -0
  66. package/dist/__tests__/thread-resume.test.js +10 -14
  67. package/dist/__tests__/thread-resume.test.js.map +1 -1
  68. package/dist/__tests__/thread-show-status.test.js +17 -28
  69. package/dist/__tests__/thread-show-status.test.js.map +1 -1
  70. package/dist/__tests__/thread-suspend-step.test.js +8 -14
  71. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  72. package/dist/__tests__/thread-suspended-display.test.js +10 -22
  73. package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
  74. package/dist/__tests__/thread.test.js +4 -4
  75. package/dist/__tests__/thread.test.js.map +1 -1
  76. package/dist/__tests__/validate-semantic.test.js +49 -21
  77. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  78. package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
  79. package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
  80. package/dist/__tests__/workflow-list-recursive.test.js +283 -0
  81. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
  82. package/dist/__tests__/workflow-resolution.test.js +36 -21
  83. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  84. package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
  85. package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
  86. package/dist/__tests__/workflow-show-resolution.test.js +210 -0
  87. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
  88. package/dist/__tests__/workflow-validate.test.d.ts +2 -0
  89. package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
  90. package/dist/__tests__/workflow-validate.test.js +687 -0
  91. package/dist/__tests__/workflow-validate.test.js.map +1 -0
  92. package/dist/background/background.d.ts +22 -1
  93. package/dist/background/background.d.ts.map +1 -1
  94. package/dist/background/background.js +83 -6
  95. package/dist/background/background.js.map +1 -1
  96. package/dist/background/index.d.ts +1 -1
  97. package/dist/background/index.d.ts.map +1 -1
  98. package/dist/background/index.js +1 -1
  99. package/dist/background/index.js.map +1 -1
  100. package/dist/background/types.d.ts +1 -0
  101. package/dist/background/types.d.ts.map +1 -1
  102. package/dist/cli.js +66 -31
  103. package/dist/cli.js.map +1 -1
  104. package/dist/commands/config.d.ts +3 -1
  105. package/dist/commands/config.d.ts.map +1 -1
  106. package/dist/commands/config.js +7 -33
  107. package/dist/commands/config.js.map +1 -1
  108. package/dist/commands/prompt.d.ts.map +1 -1
  109. package/dist/commands/prompt.js +15 -2
  110. package/dist/commands/prompt.js.map +1 -1
  111. package/dist/commands/setup.d.ts +7 -39
  112. package/dist/commands/setup.d.ts.map +1 -1
  113. package/dist/commands/setup.js +27 -302
  114. package/dist/commands/setup.js.map +1 -1
  115. package/dist/commands/step.d.ts +44 -1
  116. package/dist/commands/step.d.ts.map +1 -1
  117. package/dist/commands/step.js +255 -11
  118. package/dist/commands/step.js.map +1 -1
  119. package/dist/commands/thread.d.ts +16 -3
  120. package/dist/commands/thread.d.ts.map +1 -1
  121. package/dist/commands/thread.js +379 -140
  122. package/dist/commands/thread.js.map +1 -1
  123. package/dist/commands/workflow.d.ts +9 -1
  124. package/dist/commands/workflow.d.ts.map +1 -1
  125. package/dist/commands/workflow.js +130 -6
  126. package/dist/commands/workflow.js.map +1 -1
  127. package/dist/moderator/__tests__/evaluate.test.js +31 -17
  128. package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
  129. package/dist/moderator/evaluate.d.ts.map +1 -1
  130. package/dist/moderator/evaluate.js +4 -16
  131. package/dist/moderator/evaluate.js.map +1 -1
  132. package/dist/moderator/index.d.ts +1 -2
  133. package/dist/moderator/index.d.ts.map +1 -1
  134. package/dist/moderator/index.js +0 -1
  135. package/dist/moderator/index.js.map +1 -1
  136. package/dist/moderator/types.d.ts +6 -10
  137. package/dist/moderator/types.d.ts.map +1 -1
  138. package/dist/moderator/types.js +1 -3
  139. package/dist/moderator/types.js.map +1 -1
  140. package/dist/schemas.d.ts +2 -0
  141. package/dist/schemas.d.ts.map +1 -1
  142. package/dist/schemas.js +5 -3
  143. package/dist/schemas.js.map +1 -1
  144. package/dist/store.d.ts +28 -9
  145. package/dist/store.d.ts.map +1 -1
  146. package/dist/store.js +75 -16
  147. package/dist/store.js.map +1 -1
  148. package/dist/validate-semantic.d.ts.map +1 -1
  149. package/dist/validate-semantic.js +83 -66
  150. package/dist/validate-semantic.js.map +1 -1
  151. package/dist/validate.d.ts +6 -0
  152. package/dist/validate.d.ts.map +1 -1
  153. package/dist/validate.js +24 -0
  154. package/dist/validate.js.map +1 -1
  155. package/package.json +8 -10
  156. package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
  157. package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
  158. package/src/__tests__/build-step-entry.test.ts +203 -0
  159. package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
  160. package/src/__tests__/config.test.ts +33 -321
  161. package/src/__tests__/current-role.test.ts +7 -6
  162. package/src/__tests__/e2e-mock-agent.test.ts +20 -23
  163. package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
  164. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
  165. package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
  166. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
  167. package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
  168. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
  169. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
  170. package/src/__tests__/moderator-evaluate.test.ts +9 -52
  171. package/src/__tests__/pid-recycling.test.ts +328 -0
  172. package/src/__tests__/prompt.test.ts +397 -0
  173. package/src/__tests__/resolve-head-hash.test.ts +4 -4
  174. package/src/__tests__/setup-agent-discovery.test.ts +26 -51
  175. package/src/__tests__/setup-complexity.test.ts +1 -203
  176. package/src/__tests__/setup-no-llm.test.ts +68 -0
  177. package/src/__tests__/solve-issue-tea-worktree.test.ts +24 -30
  178. package/src/__tests__/step-ask.test.ts +670 -0
  179. package/src/__tests__/step-show-json.test.ts +1 -0
  180. package/src/__tests__/step-timing.test.ts +2 -0
  181. package/src/__tests__/store-global-cas.test.ts +2 -2
  182. package/src/__tests__/store-unified-threads.test.ts +9 -9
  183. package/src/__tests__/thread-cancel-status.test.ts +6 -6
  184. package/src/__tests__/thread-list-filters.test.ts +434 -8
  185. package/src/__tests__/thread-poke.test.ts +545 -0
  186. package/src/__tests__/thread-resume.test.ts +10 -14
  187. package/src/__tests__/thread-show-status.test.ts +17 -29
  188. package/src/__tests__/thread-suspend-step.test.ts +8 -14
  189. package/src/__tests__/thread-suspended-display.test.ts +10 -22
  190. package/src/__tests__/thread.test.ts +4 -4
  191. package/src/__tests__/validate-semantic.test.ts +59 -31
  192. package/src/__tests__/workflow-list-recursive.test.ts +370 -0
  193. package/src/__tests__/workflow-resolution.test.ts +39 -21
  194. package/src/__tests__/workflow-show-resolution.test.ts +285 -0
  195. package/src/__tests__/workflow-validate.test.ts +806 -0
  196. package/src/background/background.ts +88 -6
  197. package/src/background/index.ts +2 -0
  198. package/src/background/types.ts +1 -0
  199. package/src/cli.ts +97 -47
  200. package/src/commands/config.ts +7 -35
  201. package/src/commands/prompt.ts +15 -2
  202. package/src/commands/setup.ts +29 -357
  203. package/src/commands/step.ts +339 -12
  204. package/src/commands/thread.ts +463 -169
  205. package/src/commands/workflow.ts +159 -4
  206. package/src/moderator/__tests__/evaluate.test.ts +34 -17
  207. package/src/moderator/evaluate.ts +5 -17
  208. package/src/moderator/index.ts +1 -6
  209. package/src/moderator/types.ts +6 -14
  210. package/src/schemas.ts +13 -3
  211. package/src/store.ts +86 -20
  212. package/src/validate-semantic.ts +109 -78
  213. package/src/validate.ts +27 -0
  214. package/dist/__tests__/setup-validate.test.d.ts +0 -2
  215. package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
  216. package/dist/__tests__/setup-validate.test.js +0 -108
  217. package/dist/__tests__/setup-validate.test.js.map +0 -1
  218. package/src/__tests__/setup-validate.test.ts +0 -148
  219. /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
@@ -0,0 +1,122 @@
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { CasRef, ThreadId } from "@united-workforce/protocol";
5
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
6
+ import {
7
+ clearThreadFailedAttempts,
8
+ completeThread,
9
+ createUwfStore,
10
+ setThread,
11
+ type UwfStore,
12
+ } from "../store.js";
13
+
14
+ let tmpDir: string;
15
+ let originalEnv: string | undefined;
16
+
17
+ beforeEach(async () => {
18
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-clear-failed-"));
19
+ originalEnv = process.env.OCAS_HOME;
20
+ process.env.OCAS_HOME = join(tmpDir, "cas");
21
+ await mkdir(process.env.OCAS_HOME, { recursive: true });
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await rm(tmpDir, { recursive: true, force: true });
26
+ if (originalEnv === undefined) {
27
+ delete process.env.OCAS_HOME;
28
+ } else {
29
+ process.env.OCAS_HOME = originalEnv;
30
+ }
31
+ });
32
+
33
+ function failedVarName(threadId: ThreadId, role: string): string {
34
+ return `@uwf/thread-failed/${threadId}/${role}`;
35
+ }
36
+
37
+ async function seedFailedAttempt(uwf: UwfStore, threadId: ThreadId, role: string): Promise<void> {
38
+ const listHash = (await uwf.store.cas.put(
39
+ uwf.schemas.text,
40
+ JSON.stringify(["STEP00000000A"]),
41
+ )) as CasRef;
42
+ uwf.varStore.set(failedVarName(threadId, role), listHash);
43
+ }
44
+
45
+ async function seedHead(uwf: UwfStore, label: string): Promise<CasRef> {
46
+ return (await uwf.store.cas.put(uwf.schemas.text, label)) as CasRef;
47
+ }
48
+
49
+ function countFailedVars(uwf: UwfStore, threadId: ThreadId): number {
50
+ return uwf.varStore.list({ namePrefix: `@uwf/thread-failed/${threadId}/` }).length;
51
+ }
52
+
53
+ describe("clearThreadFailedAttempts", () => {
54
+ test("removes all failed-attempts vars for the given thread only", async () => {
55
+ const uwf = await createUwfStore(tmpDir);
56
+ const threadId = "01JTESTCLEAR0000000000001A" as ThreadId;
57
+ const otherThread = "01JTESTCLEAR0000000000002B" as ThreadId;
58
+
59
+ await seedFailedAttempt(uwf, threadId, "planner");
60
+ await seedFailedAttempt(uwf, threadId, "reviewer");
61
+ await seedFailedAttempt(uwf, otherThread, "planner");
62
+
63
+ expect(countFailedVars(uwf, threadId)).toBe(2);
64
+ expect(countFailedVars(uwf, otherThread)).toBe(1);
65
+
66
+ clearThreadFailedAttempts(uwf.varStore, threadId);
67
+
68
+ expect(countFailedVars(uwf, threadId)).toBe(0);
69
+ // The other thread's lineage is untouched.
70
+ expect(countFailedVars(uwf, otherThread)).toBe(1);
71
+ });
72
+
73
+ test("is a no-op when no failed-attempts vars exist", async () => {
74
+ const uwf = await createUwfStore(tmpDir);
75
+ const threadId = "01JTESTCLEAR0000000000003C" as ThreadId;
76
+ expect(() => clearThreadFailedAttempts(uwf.varStore, threadId)).not.toThrow();
77
+ expect(countFailedVars(uwf, threadId)).toBe(0);
78
+ });
79
+ });
80
+
81
+ describe("completeThread clears failed-attempts lineage", () => {
82
+ test("completion clears the thread's failed-attempts vars", async () => {
83
+ const uwf = await createUwfStore(tmpDir);
84
+ const threadId = "01JTESTCLEAR0000000000004D" as ThreadId;
85
+ const head = await seedHead(uwf, "head");
86
+
87
+ setThread(uwf.varStore, threadId, {
88
+ head,
89
+ status: "idle",
90
+ suspendedRole: null,
91
+ suspendMessage: null,
92
+ completedAt: null,
93
+ });
94
+ await seedFailedAttempt(uwf, threadId, "planner");
95
+ expect(countFailedVars(uwf, threadId)).toBe(1);
96
+
97
+ completeThread(uwf.varStore, threadId, "end");
98
+
99
+ expect(countFailedVars(uwf, threadId)).toBe(0);
100
+ });
101
+
102
+ test("cancellation clears the thread's failed-attempts vars", async () => {
103
+ const uwf = await createUwfStore(tmpDir);
104
+ const threadId = "01JTESTCLEAR0000000000005E" as ThreadId;
105
+ const head = await seedHead(uwf, "head");
106
+
107
+ setThread(uwf.varStore, threadId, {
108
+ head,
109
+ status: "running",
110
+ suspendedRole: null,
111
+ suspendMessage: null,
112
+ completedAt: null,
113
+ });
114
+ await seedFailedAttempt(uwf, threadId, "planner");
115
+ await seedFailedAttempt(uwf, threadId, "reviewer");
116
+ expect(countFailedVars(uwf, threadId)).toBe(2);
117
+
118
+ completeThread(uwf.varStore, threadId, "cancelled");
119
+
120
+ expect(countFailedVars(uwf, threadId)).toBe(0);
121
+ });
122
+ });
@@ -21,22 +21,8 @@ describe("config command", () => {
21
21
  return configPath;
22
22
  }
23
23
 
24
- // Sample test config
25
- const sampleConfig = `providers:
26
- dashscope:
27
- baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
28
- apiKey: sk-test-dashscope-key
29
- openai:
30
- baseUrl: https://api.openai.com/v1
31
- apiKey: sk-test-openai-key
32
- models:
33
- default:
34
- provider: dashscope
35
- name: qwen-max
36
- gpt4:
37
- provider: openai
38
- name: gpt-4
39
- agents:
24
+ // Sample test config — engine-only (no providers/models/defaultModel/modelOverrides)
25
+ const sampleConfig = `agents:
40
26
  hermes:
41
27
  command: uwf-hermes
42
28
  args:
@@ -48,7 +34,6 @@ agents:
48
34
  - --profile
49
35
  - work
50
36
  defaultAgent: hermes
51
- defaultModel: default
52
37
  `;
53
38
 
54
39
  describe("helper functions", () => {
@@ -56,11 +41,7 @@ defaultModel: default
56
41
  test("splits dot notation correctly", () => {
57
42
  expect(parseDotPath("a.b.c")).toEqual(["a", "b", "c"]);
58
43
  expect(parseDotPath("defaultAgent")).toEqual(["defaultAgent"]);
59
- expect(parseDotPath("providers.dashscope.baseUrl")).toEqual([
60
- "providers",
61
- "dashscope",
62
- "baseUrl",
63
- ]);
44
+ expect(parseDotPath("agents.hermes.command")).toEqual(["agents", "hermes", "command"]);
64
45
  });
65
46
  });
66
47
 
@@ -102,84 +83,14 @@ defaultModel: default
102
83
  });
103
84
 
104
85
  describe("maskApiKeys", () => {
105
- test("deep clones and masks all apiKey values in providers", () => {
86
+ test("returns deep clone (no mutation) engine config has no apiKey to mask", () => {
106
87
  const config = {
107
- providers: {
108
- dashscope: {
109
- baseUrl: "https://example.com",
110
- apiKey: "sk-test-key-12345",
111
- },
112
- openai: {
113
- baseUrl: "https://api.openai.com",
114
- apiKey: "sk-another-secret",
115
- },
116
- },
117
- models: {
118
- default: { provider: "dashscope" },
119
- },
88
+ agents: { hermes: { command: "uwf-hermes", args: [] } },
89
+ defaultAgent: "hermes",
120
90
  };
121
91
  const masked = maskApiKeys(config);
122
- expect(masked).toEqual({
123
- providers: {
124
- dashscope: {
125
- baseUrl: "https://example.com",
126
- apiKey: "***MASKED***",
127
- },
128
- openai: {
129
- baseUrl: "https://api.openai.com",
130
- apiKey: "***MASKED***",
131
- },
132
- },
133
- models: {
134
- default: { provider: "dashscope" },
135
- },
136
- });
137
- // Ensure it's a deep clone
138
- expect(masked).not.toBe(config);
139
- });
140
-
141
- test("handles config without providers", () => {
142
- const config = { models: { default: { provider: "test" } } };
143
- const masked = maskApiKeys(config);
144
92
  expect(masked).toEqual(config);
145
- });
146
-
147
- test("does not mask non-provider apiKey fields", () => {
148
- const config = {
149
- apiKey: "root-level-key",
150
- providers: {
151
- dashscope: { apiKey: "sk-secret" },
152
- },
153
- models: {
154
- default: { provider: "dashscope" },
155
- },
156
- };
157
- const masked = maskApiKeys(config);
158
- // Root-level apiKey should NOT be masked
159
- expect(masked.apiKey).toBe("root-level-key");
160
- // Provider apiKey SHOULD be masked
161
- const providers = masked.providers as Record<string, Record<string, unknown>>;
162
- expect(providers.dashscope.apiKey).toBe("***MASKED***");
163
- });
164
-
165
- test("handles empty provider object", () => {
166
- const config = {
167
- providers: { dashscope: {} },
168
- };
169
- const masked = maskApiKeys(config);
170
- expect(masked).toEqual({ providers: { dashscope: {} } });
171
- });
172
-
173
- test("handles provider with null apiKey", () => {
174
- const config = {
175
- providers: {
176
- dashscope: { apiKey: null, baseUrl: "https://example.com" },
177
- },
178
- };
179
- const masked = maskApiKeys(config);
180
- const providers = masked.providers as Record<string, Record<string, unknown>>;
181
- expect(providers.dashscope.apiKey).toBe("***MASKED***");
182
- expect(providers.dashscope.baseUrl).toBe("https://example.com");
93
+ expect(masked).not.toBe(config);
183
94
  });
184
95
  });
185
96
  });
@@ -192,26 +103,8 @@ defaultModel: default
192
103
  const result = await cmdConfigList(tempDir);
193
104
  expect(result).toBeDefined();
194
105
  expect(typeof result).toBe("object");
195
- expect(result).toHaveProperty("providers");
196
- expect(result).toHaveProperty("models");
197
106
  expect(result).toHaveProperty("agents");
198
107
  expect(result).toHaveProperty("defaultAgent");
199
- expect(result).toHaveProperty("defaultModel");
200
- } finally {
201
- rmSync(tempDir, { recursive: true, force: true });
202
- }
203
- });
204
-
205
- test("masks all apiKey values in providers section", async () => {
206
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
207
- try {
208
- createTestConfig(tempDir, sampleConfig);
209
- const result = (await cmdConfigList(tempDir)) as Record<string, unknown>;
210
- const providers = result.providers as Record<string, unknown>;
211
- const dashscope = providers.dashscope as Record<string, unknown>;
212
- const openai = providers.openai as Record<string, unknown>;
213
- expect(dashscope.apiKey).toBe("***MASKED***");
214
- expect(openai.apiKey).toBe("***MASKED***");
215
108
  } finally {
216
109
  rmSync(tempDir, { recursive: true, force: true });
217
110
  }
@@ -260,53 +153,6 @@ defaultModel: default
260
153
  }
261
154
  });
262
155
 
263
- test("retrieves top-level string value (defaultModel)", async () => {
264
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
265
- try {
266
- createTestConfig(tempDir, sampleConfig);
267
- const result = await cmdConfigGet(tempDir, "defaultModel");
268
- expect(result).toBe("default");
269
- } finally {
270
- rmSync(tempDir, { recursive: true, force: true });
271
- }
272
- });
273
-
274
- test("retrieves nested object (providers.dashscope)", async () => {
275
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
276
- try {
277
- createTestConfig(tempDir, sampleConfig);
278
- const result = await cmdConfigGet(tempDir, "providers.dashscope");
279
- expect(result).toEqual({
280
- baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
281
- apiKey: "sk-test-dashscope-key",
282
- });
283
- } finally {
284
- rmSync(tempDir, { recursive: true, force: true });
285
- }
286
- });
287
-
288
- test("retrieves deeply nested string (providers.dashscope.baseUrl)", async () => {
289
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
290
- try {
291
- createTestConfig(tempDir, sampleConfig);
292
- const result = await cmdConfigGet(tempDir, "providers.dashscope.baseUrl");
293
- expect(result).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1");
294
- } finally {
295
- rmSync(tempDir, { recursive: true, force: true });
296
- }
297
- });
298
-
299
- test("retrieves nested string in models (models.default.provider)", async () => {
300
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
301
- try {
302
- createTestConfig(tempDir, sampleConfig);
303
- const result = await cmdConfigGet(tempDir, "models.default.provider");
304
- expect(result).toBe("dashscope");
305
- } finally {
306
- rmSync(tempDir, { recursive: true, force: true });
307
- }
308
- });
309
-
310
156
  test("retrieves array value (agents.hermes.args)", async () => {
311
157
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
312
158
  try {
@@ -355,7 +201,6 @@ defaultModel: default
355
201
  createTestConfig(tempDir, sampleConfig);
356
202
  const result = await cmdConfigSet(tempDir, "defaultAgent", "claude-code");
357
203
  expect(result).toEqual({ key: "defaultAgent", value: "claude-code" });
358
- // Verify it was written
359
204
  const updated = await cmdConfigGet(tempDir, "defaultAgent");
360
205
  expect(updated).toBe("claude-code");
361
206
  } finally {
@@ -363,42 +208,6 @@ defaultModel: default
363
208
  }
364
209
  });
365
210
 
366
- test("sets nested string value (providers.dashscope.baseUrl)", async () => {
367
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
368
- try {
369
- createTestConfig(tempDir, sampleConfig);
370
- const newUrl = "https://new-api.example.com/v1";
371
- const result = await cmdConfigSet(tempDir, "providers.dashscope.baseUrl", newUrl);
372
- expect(result).toEqual({
373
- key: "providers.dashscope.baseUrl",
374
- value: newUrl,
375
- });
376
- // Verify it was written
377
- const updated = await cmdConfigGet(tempDir, "providers.dashscope.baseUrl");
378
- expect(updated).toBe(newUrl);
379
- } finally {
380
- rmSync(tempDir, { recursive: true, force: true });
381
- }
382
- });
383
-
384
- test("creates new nested path (providers.newprovider.baseUrl)", async () => {
385
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
386
- try {
387
- createTestConfig(tempDir, sampleConfig);
388
- const newUrl = "https://new-provider.com/v1";
389
- const result = await cmdConfigSet(tempDir, "providers.newprovider.baseUrl", newUrl);
390
- expect(result).toEqual({
391
- key: "providers.newprovider.baseUrl",
392
- value: newUrl,
393
- });
394
- // Verify it was created
395
- const updated = await cmdConfigGet(tempDir, "providers.newprovider.baseUrl");
396
- expect(updated).toBe(newUrl);
397
- } finally {
398
- rmSync(tempDir, { recursive: true, force: true });
399
- }
400
- });
401
-
402
211
  test("sets array value for args key with valid JSON array", async () => {
403
212
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
404
213
  try {
@@ -409,7 +218,6 @@ defaultModel: default
409
218
  key: "agents.hermes.args",
410
219
  value: ["--new", "--flags"],
411
220
  });
412
- // Verify it was written
413
221
  const updated = await cmdConfigGet(tempDir, "agents.hermes.args");
414
222
  expect(updated).toEqual(["--new", "--flags"]);
415
223
  } finally {
@@ -422,11 +230,8 @@ defaultModel: default
422
230
  try {
423
231
  createTestConfig(tempDir, sampleConfig);
424
232
  await cmdConfigSet(tempDir, "defaultAgent", "claude-code");
425
- // Verify other values are preserved
426
- const defaultModel = await cmdConfigGet(tempDir, "defaultModel");
427
- expect(defaultModel).toBe("default");
428
- const dashscopeUrl = await cmdConfigGet(tempDir, "providers.dashscope.baseUrl");
429
- expect(dashscopeUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1");
233
+ const cmd = await cmdConfigGet(tempDir, "agents.hermes.command");
234
+ expect(cmd).toBe("uwf-hermes");
430
235
  } finally {
431
236
  rmSync(tempDir, { recursive: true, force: true });
432
237
  }
@@ -437,7 +242,6 @@ defaultModel: default
437
242
  try {
438
243
  const result = await cmdConfigSet(tempDir, "defaultAgent", "hermes");
439
244
  expect(result).toEqual({ key: "defaultAgent", value: "hermes" });
440
- // Verify file was created
441
245
  const configPath = getConfigPath(tempDir);
442
246
  const content = readFileSync(configPath, "utf8");
443
247
  expect(content).toContain("defaultAgent: hermes");
@@ -468,23 +272,6 @@ defaultModel: default
468
272
  }
469
273
  });
470
274
 
471
- test("sets deeply nested model config (models.gpt4.provider)", async () => {
472
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
473
- try {
474
- createTestConfig(tempDir, sampleConfig);
475
- const result = await cmdConfigSet(tempDir, "models.gpt4.provider", "new-provider");
476
- expect(result).toEqual({
477
- key: "models.gpt4.provider",
478
- value: "new-provider",
479
- });
480
- // Verify it was written
481
- const updated = await cmdConfigGet(tempDir, "models.gpt4.provider");
482
- expect(updated).toBe("new-provider");
483
- } finally {
484
- rmSync(tempDir, { recursive: true, force: true });
485
- }
486
- });
487
-
488
275
  test("sets agent command (agents.claude-code.command)", async () => {
489
276
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
490
277
  try {
@@ -494,7 +281,6 @@ defaultModel: default
494
281
  key: "agents.claude-code.command",
495
282
  value: "new-command",
496
283
  });
497
- // Verify it was written
498
284
  const updated = await cmdConfigGet(tempDir, "agents.claude-code.command");
499
285
  expect(updated).toBe("new-command");
500
286
  } finally {
@@ -503,97 +289,87 @@ defaultModel: default
503
289
  });
504
290
  });
505
291
 
506
- describe("cmdConfigSet validation", () => {
507
- test("rejects unknown top-level key", async () => {
508
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
509
- try {
510
- createTestConfig(tempDir, sampleConfig);
511
- await expect(cmdConfigSet(tempDir, "unknownKey", "value")).rejects.toThrow(
512
- /Unknown config key.*unknownKey/,
513
- );
514
- } finally {
515
- rmSync(tempDir, { recursive: true, force: true });
516
- }
517
- });
518
-
519
- test("rejects unknown nested key in providers", async () => {
292
+ describe("cmdConfigSet — LLM keys removed from engine config (issue #143)", () => {
293
+ test("rejects providers as unknown top-level key", async () => {
520
294
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
521
295
  try {
522
296
  createTestConfig(tempDir, sampleConfig);
523
297
  await expect(
524
- cmdConfigSet(tempDir, "providers.myProvider.unknownField", "value"),
525
- ).rejects.toThrow(/Unknown field.*unknownField.*providers/);
298
+ cmdConfigSet(tempDir, "providers.openai.baseUrl", "https://api.openai.com/v1"),
299
+ ).rejects.toThrow(/Unknown config key/);
526
300
  } finally {
527
301
  rmSync(tempDir, { recursive: true, force: true });
528
302
  }
529
303
  });
530
304
 
531
- test("rejects unknown nested key in models", async () => {
305
+ test("rejects models as unknown top-level key", async () => {
532
306
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
533
307
  try {
534
308
  createTestConfig(tempDir, sampleConfig);
535
- await expect(cmdConfigSet(tempDir, "models.default.invalidField", "value")).rejects.toThrow(
536
- /Unknown field.*invalidField.*models/,
309
+ await expect(cmdConfigSet(tempDir, "models.default.provider", "openai")).rejects.toThrow(
310
+ /Unknown config key/,
537
311
  );
538
312
  } finally {
539
313
  rmSync(tempDir, { recursive: true, force: true });
540
314
  }
541
315
  });
542
316
 
543
- test("rejects unknown nested key in agents", async () => {
317
+ test("rejects defaultModel as unknown top-level key", async () => {
544
318
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
545
319
  try {
546
320
  createTestConfig(tempDir, sampleConfig);
547
- await expect(cmdConfigSet(tempDir, "agents.hermes.badField", "value")).rejects.toThrow(
548
- /Unknown field.*badField.*agents/,
321
+ await expect(cmdConfigSet(tempDir, "defaultModel", "default")).rejects.toThrow(
322
+ /Unknown config key/,
549
323
  );
550
324
  } finally {
551
325
  rmSync(tempDir, { recursive: true, force: true });
552
326
  }
553
327
  });
554
328
 
555
- test("rejects nested path on scalar key (defaultAgent)", async () => {
329
+ test("rejects modelOverrides as unknown top-level key", async () => {
556
330
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
557
331
  try {
558
332
  createTestConfig(tempDir, sampleConfig);
559
- await expect(cmdConfigSet(tempDir, "defaultAgent.foo", "value")).rejects.toThrow(
560
- /defaultAgent.*scalar|Cannot set property/i,
333
+ await expect(cmdConfigSet(tempDir, "modelOverrides.extract", "fast")).rejects.toThrow(
334
+ /Unknown config key/,
561
335
  );
562
336
  } finally {
563
337
  rmSync(tempDir, { recursive: true, force: true });
564
338
  }
565
339
  });
340
+ });
566
341
 
567
- test("rejects nested path on scalar key (defaultModel)", async () => {
342
+ describe("cmdConfigSet validation", () => {
343
+ test("rejects unknown top-level key", async () => {
568
344
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
569
345
  try {
570
346
  createTestConfig(tempDir, sampleConfig);
571
- await expect(cmdConfigSet(tempDir, "defaultModel.bar", "value")).rejects.toThrow(
572
- /defaultModel.*scalar|Cannot set property/i,
347
+ await expect(cmdConfigSet(tempDir, "unknownKey", "value")).rejects.toThrow(
348
+ /Unknown config key.*unknownKey/,
573
349
  );
574
350
  } finally {
575
351
  rmSync(tempDir, { recursive: true, force: true });
576
352
  }
577
353
  });
578
354
 
579
- test("rejects incomplete nested path (providers without field)", async () => {
355
+ test("rejects unknown nested key in agents", async () => {
580
356
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
581
357
  try {
582
358
  createTestConfig(tempDir, sampleConfig);
583
- await expect(cmdConfigSet(tempDir, "providers.myProvider", "value")).rejects.toThrow(
584
- /incomplete path|must specify a field/i,
359
+ await expect(cmdConfigSet(tempDir, "agents.hermes.badField", "value")).rejects.toThrow(
360
+ /Unknown field.*badField.*agents/,
585
361
  );
586
362
  } finally {
587
363
  rmSync(tempDir, { recursive: true, force: true });
588
364
  }
589
365
  });
590
366
 
591
- test("rejects incomplete nested path (models without field)", async () => {
367
+ test("rejects nested path on scalar key (defaultAgent)", async () => {
592
368
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
593
369
  try {
594
370
  createTestConfig(tempDir, sampleConfig);
595
- await expect(cmdConfigSet(tempDir, "models.myModel", "value")).rejects.toThrow(
596
- /incomplete path|must specify a field/i,
371
+ await expect(cmdConfigSet(tempDir, "defaultAgent.foo", "value")).rejects.toThrow(
372
+ /defaultAgent.*scalar|Cannot set property/i,
597
373
  );
598
374
  } finally {
599
375
  rmSync(tempDir, { recursive: true, force: true });
@@ -612,36 +388,6 @@ defaultModel: default
612
388
  }
613
389
  });
614
390
 
615
- test("allows valid nested keys in providers", async () => {
616
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
617
- try {
618
- createTestConfig(tempDir, sampleConfig);
619
- await cmdConfigSet(tempDir, "providers.newprovider.baseUrl", "https://example.com");
620
- await cmdConfigSet(tempDir, "providers.newprovider.apiKey", "sk-test");
621
- const baseUrl = await cmdConfigGet(tempDir, "providers.newprovider.baseUrl");
622
- const apiKey = await cmdConfigGet(tempDir, "providers.newprovider.apiKey");
623
- expect(baseUrl).toBe("https://example.com");
624
- expect(apiKey).toBe("sk-test");
625
- } finally {
626
- rmSync(tempDir, { recursive: true, force: true });
627
- }
628
- });
629
-
630
- test("allows valid nested keys in models", async () => {
631
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
632
- try {
633
- createTestConfig(tempDir, sampleConfig);
634
- await cmdConfigSet(tempDir, "models.gpt4.provider", "openai");
635
- await cmdConfigSet(tempDir, "models.gpt4.name", "gpt-4o");
636
- const provider = await cmdConfigGet(tempDir, "models.gpt4.provider");
637
- const name = await cmdConfigGet(tempDir, "models.gpt4.name");
638
- expect(provider).toBe("openai");
639
- expect(name).toBe("gpt-4o");
640
- } finally {
641
- rmSync(tempDir, { recursive: true, force: true });
642
- }
643
- });
644
-
645
391
  test("allows valid nested keys in agents", async () => {
646
392
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
647
393
  try {
@@ -681,30 +427,6 @@ defaultModel: default
681
427
  }
682
428
  });
683
429
 
684
- test("modelOverrides — accepts valid 2-segment path", async () => {
685
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
686
- try {
687
- createTestConfig(tempDir, sampleConfig);
688
- await cmdConfigSet(tempDir, "modelOverrides.extract", "gpt4");
689
- const value = await cmdConfigGet(tempDir, "modelOverrides.extract");
690
- expect(value).toBe("gpt4");
691
- } finally {
692
- rmSync(tempDir, { recursive: true, force: true });
693
- }
694
- });
695
-
696
- test("modelOverrides — rejects incomplete path (1 segment only)", async () => {
697
- const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
698
- try {
699
- createTestConfig(tempDir, sampleConfig);
700
- await expect(cmdConfigSet(tempDir, "modelOverrides", "gpt4")).rejects.toThrow(
701
- /incomplete path|must specify a field/i,
702
- );
703
- } finally {
704
- rmSync(tempDir, { recursive: true, force: true });
705
- }
706
- });
707
-
708
430
  test("rejects unknown top-level key (regression)", async () => {
709
431
  const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
710
432
  try {
@@ -726,15 +448,5 @@ defaultModel: default
726
448
  );
727
449
  expect(configSource).not.toContain("apiKeyEnv");
728
450
  });
729
-
730
- test("config.test.ts has no references to apiKeyEnv (except this test)", () => {
731
- const testSource = readFileSync(__filename, "utf8");
732
- // Remove this test block's own mentions before checking
733
- const withoutThisTest = testSource.replace(
734
- /describe\("no legacy apiKeyEnv references"[\s\S]*$/,
735
- "",
736
- );
737
- expect(withoutThisTest).not.toContain("apiKeyEnv");
738
- });
739
451
  });
740
452
  });