shadow-claw 1.40.0 → 1.40.1

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 (86) hide show
  1. package/.well-known/mcp/server-card.json +2 -2
  2. package/.well-known/mcp/servers.json +1 -1
  3. package/.well-known/mcp.json +2 -2
  4. package/dist/cli/cli.js +2 -2
  5. package/dist/lib/components/index.js +2 -2
  6. package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
  7. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  8. package/dist/lib/components/shadow-claw.js +1 -1
  9. package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
  10. package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
  11. package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
  12. package/dist/lib/index.js +2 -2
  13. package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
  14. package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
  15. package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
  16. package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
  17. package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
  18. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
  19. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
  20. package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
  21. package/dist/public/.well-known/mcp/server-card.json +2 -2
  22. package/dist/public/.well-known/mcp/servers.json +1 -1
  23. package/dist/public/.well-known/mcp.json +2 -2
  24. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  25. package/dist/public/docs/example/article/index.html +2 -2
  26. package/dist/public/docs/publishing/index.html +2 -2
  27. package/dist/public/docs/skill-creator/index.html +2 -2
  28. package/dist/public/index.html +2 -2
  29. package/dist/public/index.js +1 -1
  30. package/dist/public/main/index.html +2 -2
  31. package/dist/public/main/memory/index.html +2 -2
  32. package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
  33. package/dist/public/service-worker.js +1 -1
  34. package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
  35. package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
  36. package/dist/server.js +1 -1
  37. package/package.json +1 -1
  38. package/src/cli/cli.ts +2 -2
  39. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
  40. package/src/core/effect.test.ts +19 -0
  41. package/src/core/orchestrator/orchestrator-state.ts +2 -1
  42. package/src/core/orchestrator/orchestrator.test.ts +686 -5
  43. package/src/core/orchestrator/orchestrator.ts +3 -10
  44. package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
  45. package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
  46. package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
  47. package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
  48. package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
  49. package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
  50. package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
  51. package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
  52. package/src/core/orchestrator/utils/initTasks.ts +5 -4
  53. package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
  54. package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
  55. package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
  56. package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
  57. package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
  58. package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
  59. package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
  60. package/src/core/orchestrator/utils/operations/task.ts +1 -1
  61. package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
  62. package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
  63. package/src/core/orchestrator/utils/settings.test.ts +248 -81
  64. package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
  65. package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
  66. package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
  67. package/src/server/routes/mcp.ts +2 -2
  68. package/src/stores/file-viewer.test.ts +11 -0
  69. package/src/stores/theme.test.ts +41 -2
  70. package/src/stores/toast.test.ts +25 -0
  71. package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
  72. package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
  73. package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
  74. package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
  75. package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
  76. package/src/worker/tools/node-transformers-executor.test.ts +105 -6
  77. package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
  78. package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
  79. package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
  80. package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
  81. package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
  82. package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
  83. package/src/worker/tools/tasks/update-task.test.ts +78 -14
  84. package/src/worker/tools/ui/javascript.test.ts +70 -17
  85. package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
  86. package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
@@ -1,7 +1,13 @@
1
- import { jest } from "@jest/globals";
1
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2
2
  import { DEFAULT_GROUP_ID } from "../../../../config/config.js";
3
3
 
4
- import {
4
+ const mockDefaultSetConfig = jest.fn() as any;
5
+
6
+ jest.unstable_mockModule("../../../../db/setConfig.js", () => ({
7
+ setConfig: mockDefaultSetConfig,
8
+ }));
9
+
10
+ const {
5
11
  closeTerminalSession,
6
12
  openTerminalSession,
7
13
  sendTerminalInput,
@@ -11,18 +17,23 @@ import {
11
17
  setVMBootMode,
12
18
  setVMBootHost,
13
19
  setVMNetworkRelayURL,
14
- } from "./vm.js";
20
+ } = await import("./vm.js");
15
21
 
16
22
  import type { OrchestratorState } from "../../orchestrator-state.js";
17
23
 
18
- function makeState() {
24
+ function makeState(withWorker = true) {
19
25
  return {
20
- agentWorker: { postMessage: jest.fn() } as any,
26
+ agentWorker: withWorker ? ({ postMessage: jest.fn() } as any) : undefined,
21
27
  vmBootMode: "auto",
22
28
  } as unknown as OrchestratorState;
23
29
  }
24
30
 
25
31
  describe("vm operations", () => {
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ mockDefaultSetConfig.mockResolvedValue(undefined);
35
+ });
36
+
26
37
  it("openTerminalSession sends open message with default and custom groupId", () => {
27
38
  const state = makeState();
28
39
  openTerminalSession(state, "group1");
@@ -62,22 +73,34 @@ describe("vm operations", () => {
62
73
  });
63
74
  });
64
75
 
65
- it("syncTerminalWorkspace sends sync message", () => {
76
+ it("syncTerminalWorkspace sends sync message with default and custom groupId", () => {
66
77
  const state = makeState();
67
78
  syncTerminalWorkspace(state, "group-sync");
68
79
  expect(state.agentWorker?.postMessage).toHaveBeenCalledWith({
69
80
  payload: { groupId: "group-sync" },
70
81
  type: "vm-workspace-sync",
71
82
  });
83
+
84
+ syncTerminalWorkspace(state);
85
+ expect(state.agentWorker?.postMessage).toHaveBeenCalledWith({
86
+ payload: { groupId: DEFAULT_GROUP_ID },
87
+ type: "vm-workspace-sync",
88
+ });
72
89
  });
73
90
 
74
- it("flushTerminalWorkspace sends flush message", () => {
91
+ it("flushTerminalWorkspace sends flush message with default and custom groupId", () => {
75
92
  const state = makeState();
76
93
  flushTerminalWorkspace(state, "group-flush");
77
94
  expect(state.agentWorker?.postMessage).toHaveBeenCalledWith({
78
95
  payload: { groupId: "group-flush" },
79
96
  type: "vm-workspace-flush",
80
97
  });
98
+
99
+ flushTerminalWorkspace(state);
100
+ expect(state.agentWorker?.postMessage).toHaveBeenCalledWith({
101
+ payload: { groupId: DEFAULT_GROUP_ID },
102
+ type: "vm-workspace-flush",
103
+ });
81
104
  });
82
105
 
83
106
  it("answerUserPrompt sends ask-user-response message", () => {
@@ -110,6 +133,26 @@ describe("vm operations", () => {
110
133
  );
111
134
  expect(state.vmBootMode).toBe("disabled");
112
135
  expect(mockSetConfig).toHaveBeenCalledWith({}, "vm_boot_mode", "disabled");
136
+
137
+ // Test other valid modes: ext2, auto, disabled
138
+ await setVMBootMode(state, {} as any, "ext2", mockSetConfig as any);
139
+ expect(state.vmBootMode).toBe("ext2");
140
+
141
+ await setVMBootMode(state, {} as any, "auto", mockSetConfig as any);
142
+ expect(state.vmBootMode).toBe("auto");
143
+
144
+ await setVMBootMode(state, {} as any, "disabled", mockSetConfig as any);
145
+ expect(state.vmBootMode).toBe("disabled");
146
+ });
147
+
148
+ it("setVMBootMode uses default setConfig and handles absent agentWorker", async () => {
149
+ const state = makeState(false);
150
+ await setVMBootMode(state, {} as any, "auto");
151
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
152
+ {},
153
+ "vm_boot_mode",
154
+ "auto",
155
+ );
113
156
  });
114
157
 
115
158
  it("setVMBootHost sets boot host, updates config and notifies worker", async () => {
@@ -133,6 +176,12 @@ describe("vm operations", () => {
133
176
  });
134
177
  });
135
178
 
179
+ it("setVMBootHost handles non-string host, default setConfig, and absent agentWorker", async () => {
180
+ const state = makeState(false);
181
+ await setVMBootHost(state, {} as any, null as any);
182
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith({}, "vm_boot_host", "");
183
+ });
184
+
136
185
  it("setVMNetworkRelayURL sets relay URL, updates config and notifies worker", async () => {
137
186
  const state = makeState();
138
187
  const mockSetConfig = jest.fn().mockResolvedValue(undefined as never);
@@ -153,4 +202,14 @@ describe("vm operations", () => {
153
202
  type: "set-vm-mode",
154
203
  });
155
204
  });
205
+
206
+ it("setVMNetworkRelayURL handles non-string relay URL, default setConfig, and absent agentWorker", async () => {
207
+ const state = makeState(false);
208
+ await setVMNetworkRelayURL(state, {} as any, undefined as any);
209
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
210
+ {},
211
+ "vm_network_relay_url",
212
+ "",
213
+ );
214
+ });
156
215
  });
@@ -116,5 +116,11 @@ describe("parseDirectToolCommand", () => {
116
116
  it("should return null if JSON parses to a primitive", () => {
117
117
  const msg = createMsg('/test_tool "hello"');
118
118
  expect(parseDirectToolCommand(policy, assistantName, msg)).toBeNull();
119
+
120
+ const msgNumber = createMsg("/test_tool 123");
121
+ expect(parseDirectToolCommand(policy, assistantName, msgNumber)).toBeNull();
122
+
123
+ const msgNull = createMsg("/test_tool null");
124
+ expect(parseDirectToolCommand(policy, assistantName, msgNull)).toBeNull();
119
125
  });
120
126
  });
@@ -1,8 +1,27 @@
1
- import { jest } from "@jest/globals";
1
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2
+ import {
3
+ CONFIG_KEYS,
4
+ DEFAULT_TASK_SERVER_URL,
5
+ } from "../../../config/config.js";
6
+ import type { ShadowClawDatabase } from "../../../db/db.js";
7
+ import type { OrchestratorState } from "../orchestrator-state.js";
2
8
 
3
- import { CONFIG_KEYS } from "../../../config/config.js";
9
+ const mockDefaultSetConfig =
10
+ jest.fn<
11
+ (_db: ShadowClawDatabase, _key: string, _value: string) => Promise<void>
12
+ >();
13
+ const mockSyncProxyConfigToServiceWorker =
14
+ jest.fn<(_state: OrchestratorState) => void>();
4
15
 
5
- import {
16
+ jest.unstable_mockModule("../../../db/setConfig.js", () => ({
17
+ setConfig: mockDefaultSetConfig,
18
+ }));
19
+
20
+ jest.unstable_mockModule("./syncProxyConfigToServiceWorker.js", () => ({
21
+ syncProxyConfigToServiceWorker: mockSyncProxyConfigToServiceWorker,
22
+ }));
23
+
24
+ const {
6
25
  setContextCompressionEnabled,
7
26
  setGitProxyUrl,
8
27
  setMaxIterations,
@@ -12,28 +31,26 @@ import {
12
31
  setRateLimitCallsPerMinute,
13
32
  setReasoningEffort,
14
33
  setStreamingEnabled,
34
+ setTaskServerEnabled,
15
35
  setTaskServerUrl,
16
36
  setUseProxy,
17
37
  setVMBashFullInternetAccess,
18
38
  setVMBashTimeout,
19
- } from "./settings.js";
20
-
21
- import type { OrchestratorState } from "../orchestrator-state.js";
39
+ } = await import("./settings.js");
22
40
 
23
- // Minimal mock that satisfies the setConfig dependency.
24
- function mockSetConfig() {
41
+ function mockCustomSetConfig() {
25
42
  const calls: Array<{ key: string; value: string }> = [];
26
-
27
- return {
28
- calls,
29
- mockFn: jest.fn(async (_db: any, key: string, value: string) => {
43
+ const mockFn = jest.fn(
44
+ async (_db: ShadowClawDatabase, key: string, value: string) => {
30
45
  calls.push({ key, value });
31
- }),
32
- };
46
+ },
47
+ );
48
+ return { calls, mockFn };
33
49
  }
34
50
 
35
- // A bare-bones state object with only the fields each test needs.
36
- function makeState(overrides: Partial<OrchestratorState> = {}) {
51
+ function makeState(
52
+ overrides: Partial<OrchestratorState> = {},
53
+ ): OrchestratorState {
37
54
  return {
38
55
  contextCompressionEnabled: false,
39
56
  gitProxyUrl: "/git-proxy",
@@ -45,7 +62,8 @@ function makeState(overrides: Partial<OrchestratorState> = {}) {
45
62
  rateLimitCallsPerMinute: 0,
46
63
  reasoningEffort: "none",
47
64
  streamingEnabled: true,
48
- taskServerUrl: "/schedule",
65
+ taskServerEnabled: false,
66
+ taskServerUrl: DEFAULT_TASK_SERVER_URL,
49
67
  useProxy: false,
50
68
  vmBashFullInternetAccess: false,
51
69
  ...overrides,
@@ -53,13 +71,19 @@ function makeState(overrides: Partial<OrchestratorState> = {}) {
53
71
  }
54
72
 
55
73
  describe("settings (functional utilities)", () => {
74
+ const mockDb = {} as ShadowClawDatabase;
75
+
76
+ beforeEach(() => {
77
+ jest.clearAllMocks();
78
+ mockDefaultSetConfig.mockResolvedValue(undefined);
79
+ });
80
+
56
81
  describe("setContextCompressionEnabled", () => {
57
- it("normalizes truthy values and persists to db", async () => {
82
+ it("normalizes truthy values and persists to db with custom setConfig", async () => {
58
83
  const state = makeState();
59
- const { calls, mockFn } = mockSetConfig();
60
- const db = {} as any;
84
+ const { calls, mockFn } = mockCustomSetConfig();
61
85
 
62
- await setContextCompressionEnabled(state, db, true, mockFn);
86
+ await setContextCompressionEnabled(state, mockDb, true, mockFn);
63
87
 
64
88
  expect(state.contextCompressionEnabled).toBe(true);
65
89
  expect(calls).toEqual([
@@ -67,44 +91,51 @@ describe("settings (functional utilities)", () => {
67
91
  ]);
68
92
  });
69
93
 
70
- it("normalizes falsy values", async () => {
94
+ it("normalizes falsy values and persists with default setConfig", async () => {
71
95
  const state = makeState({ contextCompressionEnabled: true });
72
- const { mockFn } = mockSetConfig();
73
96
 
74
- await setContextCompressionEnabled(state, {} as any, 0 as any, mockFn);
97
+ await setContextCompressionEnabled(state, mockDb, false);
75
98
 
76
99
  expect(state.contextCompressionEnabled).toBe(false);
100
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
101
+ mockDb,
102
+ CONFIG_KEYS.CONTEXT_COMPRESSION_ENABLED,
103
+ "false",
104
+ );
77
105
  });
78
106
  });
79
107
 
80
108
  describe("setGitProxyUrl", () => {
81
109
  it("defaults to /git-proxy when empty", async () => {
82
110
  const state = makeState();
83
- const { calls, mockFn } = mockSetConfig();
111
+ const { calls, mockFn } = mockCustomSetConfig();
84
112
 
85
- await setGitProxyUrl(state, {} as any, "", mockFn);
113
+ await setGitProxyUrl(state, mockDb, "", mockFn);
86
114
 
87
115
  expect(state.gitProxyUrl).toBe("/git-proxy");
88
116
  expect(calls[0].value).toBe("/git-proxy");
89
117
  });
90
118
 
91
- it("stores the provided url", async () => {
119
+ it("stores the provided url using default setConfig", async () => {
92
120
  const state = makeState();
93
- const { calls, mockFn } = mockSetConfig();
94
121
 
95
- await setGitProxyUrl(state, {} as any, "/custom-git", mockFn);
122
+ await setGitProxyUrl(state, mockDb, "/custom-git");
96
123
 
97
124
  expect(state.gitProxyUrl).toBe("/custom-git");
98
- expect(calls[0].value).toBe("/custom-git");
125
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
126
+ mockDb,
127
+ CONFIG_KEYS.GIT_PROXY_URL,
128
+ "/custom-git",
129
+ );
99
130
  });
100
131
  });
101
132
 
102
133
  describe("setMaxIterations", () => {
103
- it("stores value on state and in db", async () => {
134
+ it("stores value on state and in db with custom setConfig", async () => {
104
135
  const state = makeState();
105
- const { calls, mockFn } = mockSetConfig();
136
+ const { calls, mockFn } = mockCustomSetConfig();
106
137
 
107
- await setMaxIterations(state, {} as any, 50, mockFn);
138
+ await setMaxIterations(state, mockDb, 50, mockFn);
108
139
 
109
140
  expect(state.maxIterations).toBe(50);
110
141
  expect(calls[0]).toEqual({
@@ -112,16 +143,28 @@ describe("settings (functional utilities)", () => {
112
143
  value: "50",
113
144
  });
114
145
  });
146
+
147
+ it("stores value using default setConfig", async () => {
148
+ const state = makeState();
149
+
150
+ await setMaxIterations(state, mockDb, 10);
151
+
152
+ expect(state.maxIterations).toBe(10);
153
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
154
+ mockDb,
155
+ CONFIG_KEYS.MAX_ITERATIONS,
156
+ "10",
157
+ );
158
+ });
115
159
  });
116
160
 
117
161
  describe("setMaxTokens", () => {
118
162
  it("stores large values without clamping to model maximum", async () => {
119
163
  const state = makeState({ model: "test-model" });
120
- const { calls, mockFn } = mockSetConfig();
164
+ const { calls, mockFn } = mockCustomSetConfig();
121
165
 
122
- await setMaxTokens(state, {} as any, 999999, mockFn);
166
+ await setMaxTokens(state, mockDb, 999999, mockFn);
123
167
 
124
- // No upper clamp — user can set any value they want
125
168
  expect(state.maxTokens).toBe(999999);
126
169
  expect(calls[0]).toEqual({
127
170
  key: CONFIG_KEYS.MAX_TOKENS,
@@ -129,65 +172,101 @@ describe("settings (functional utilities)", () => {
129
172
  });
130
173
  });
131
174
 
132
- it("clamps below 1 to 1", async () => {
175
+ it("clamps below 1 to 1 and uses default setConfig", async () => {
133
176
  const state = makeState();
134
- const { mockFn } = mockSetConfig();
135
177
 
136
- await setMaxTokens(state, {} as any, -10, mockFn);
178
+ await setMaxTokens(state, mockDb, -10);
137
179
 
138
180
  expect(state.maxTokens).toBe(1);
181
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
182
+ mockDb,
183
+ CONFIG_KEYS.MAX_TOKENS,
184
+ "1",
185
+ );
139
186
  });
140
187
  });
141
188
 
142
189
  describe("setProxyUrl", () => {
143
190
  it("defaults to /proxy when empty", async () => {
144
191
  const state = makeState();
145
- const { calls, mockFn } = mockSetConfig();
192
+ const { calls, mockFn } = mockCustomSetConfig();
146
193
 
147
- await setProxyUrl(state, {} as any, "", mockFn);
194
+ await setProxyUrl(state, mockDb, "", mockFn);
148
195
 
149
196
  expect(state.proxyUrl).toBe("/proxy");
150
197
  expect(calls[0].value).toBe("/proxy");
198
+ expect(mockSyncProxyConfigToServiceWorker).toHaveBeenCalledWith(state);
199
+ });
200
+
201
+ it("stores the custom url and uses default setConfig", async () => {
202
+ const state = makeState();
203
+
204
+ await setProxyUrl(state, mockDb, "http://localhost:8080/proxy");
205
+
206
+ expect(state.proxyUrl).toBe("http://localhost:8080/proxy");
207
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
208
+ mockDb,
209
+ CONFIG_KEYS.PROXY_URL,
210
+ "http://localhost:8080/proxy",
211
+ );
212
+ expect(mockSyncProxyConfigToServiceWorker).toHaveBeenCalledWith(state);
151
213
  });
152
214
  });
153
215
 
154
216
  describe("setRateLimitAutoAdapt", () => {
155
- it("normalizes and persists boolean", async () => {
217
+ it("normalizes and persists boolean with custom setConfig", async () => {
156
218
  const state = makeState({ rateLimitAutoAdapt: true });
157
- const { calls, mockFn } = mockSetConfig();
219
+ const { calls, mockFn } = mockCustomSetConfig();
158
220
 
159
- await setRateLimitAutoAdapt(state, {} as any, false, mockFn);
221
+ await setRateLimitAutoAdapt(state, mockDb, false, mockFn);
160
222
 
161
223
  expect(state.rateLimitAutoAdapt).toBe(false);
162
224
  expect(calls[0].value).toBe("false");
163
225
  });
226
+
227
+ it("normalizes and persists boolean with default setConfig", async () => {
228
+ const state = makeState({ rateLimitAutoAdapt: false });
229
+
230
+ await setRateLimitAutoAdapt(state, mockDb, true);
231
+
232
+ expect(state.rateLimitAutoAdapt).toBe(true);
233
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
234
+ mockDb,
235
+ CONFIG_KEYS.RATE_LIMIT_AUTO_ADAPT,
236
+ "true",
237
+ );
238
+ });
164
239
  });
165
240
 
166
241
  describe("setRateLimitCallsPerMinute", () => {
167
242
  it("normalizes to a non-negative integer", async () => {
168
243
  const state = makeState();
169
- const { calls, mockFn } = mockSetConfig();
244
+ const { calls, mockFn } = mockCustomSetConfig();
170
245
 
171
- await setRateLimitCallsPerMinute(state, {} as any, 3.7, mockFn);
246
+ await setRateLimitCallsPerMinute(state, mockDb, 3.7, mockFn);
172
247
 
173
248
  expect(state.rateLimitCallsPerMinute).toBe(3);
174
249
  expect(calls[0].value).toBe("3");
175
250
  });
176
251
 
177
- it("normalizes NaN to 0", async () => {
252
+ it("normalizes NaN to 0 and uses default setConfig", async () => {
178
253
  const state = makeState();
179
- const { mockFn } = mockSetConfig();
180
254
 
181
- await setRateLimitCallsPerMinute(state, {} as any, NaN, mockFn);
255
+ await setRateLimitCallsPerMinute(state, mockDb, NaN);
182
256
 
183
257
  expect(state.rateLimitCallsPerMinute).toBe(0);
258
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
259
+ mockDb,
260
+ CONFIG_KEYS.RATE_LIMIT_CALLS_PER_MINUTE,
261
+ "0",
262
+ );
184
263
  });
185
264
 
186
265
  it("normalizes negative to 0", async () => {
187
266
  const state = makeState();
188
- const { mockFn } = mockSetConfig();
267
+ const { mockFn } = mockCustomSetConfig();
189
268
 
190
- await setRateLimitCallsPerMinute(state, {} as any, -5, mockFn);
269
+ await setRateLimitCallsPerMinute(state, mockDb, -5, mockFn);
191
270
 
192
271
  expect(state.rateLimitCallsPerMinute).toBe(0);
193
272
  });
@@ -196,103 +275,191 @@ describe("settings (functional utilities)", () => {
196
275
  describe("setReasoningEffort", () => {
197
276
  it("normalizes and lowercases effort string", async () => {
198
277
  const state = makeState();
199
- const { calls, mockFn } = mockSetConfig();
278
+ const { calls, mockFn } = mockCustomSetConfig();
200
279
 
201
- await setReasoningEffort(state, {} as any, " HIGH ", mockFn);
280
+ await setReasoningEffort(state, mockDb, " HIGH ", mockFn);
202
281
 
203
282
  expect(state.reasoningEffort).toBe("high");
204
283
  expect(calls[0].value).toBe("high");
205
284
  });
206
285
 
207
- it("defaults to 'none' for empty input", async () => {
286
+ it("defaults to 'none' for empty input with default setConfig", async () => {
208
287
  const state = makeState();
209
- const { mockFn } = mockSetConfig();
210
288
 
211
- await setReasoningEffort(state, {} as any, "", mockFn);
289
+ await setReasoningEffort(state, mockDb, "");
212
290
 
213
291
  expect(state.reasoningEffort).toBe("none");
292
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
293
+ mockDb,
294
+ CONFIG_KEYS.REASONING_EFFORT,
295
+ "none",
296
+ );
214
297
  });
215
298
 
216
299
  it("defaults to 'none' for non-string input", async () => {
217
300
  const state = makeState();
218
- const { mockFn } = mockSetConfig();
301
+ const { mockFn } = mockCustomSetConfig();
219
302
 
220
- await setReasoningEffort(state, {} as any, 42 as any, mockFn);
303
+ await setReasoningEffort(state, mockDb, 42 as unknown as string, mockFn);
221
304
 
222
305
  expect(state.reasoningEffort).toBe("none");
223
306
  });
224
307
  });
225
308
 
226
309
  describe("setStreamingEnabled", () => {
227
- it("normalizes truthy and persists", async () => {
310
+ it("normalizes truthy and persists with custom setConfig", async () => {
228
311
  const state = makeState({ streamingEnabled: false });
229
- const { calls, mockFn } = mockSetConfig();
312
+ const { calls, mockFn } = mockCustomSetConfig();
230
313
 
231
- await setStreamingEnabled(state, {} as any, true, mockFn);
314
+ await setStreamingEnabled(state, mockDb, true, mockFn);
232
315
 
233
316
  expect(state.streamingEnabled).toBe(true);
234
317
  expect(calls[0].value).toBe("true");
235
318
  });
319
+
320
+ it("normalizes falsy and persists with default setConfig", async () => {
321
+ const state = makeState({ streamingEnabled: true });
322
+
323
+ await setStreamingEnabled(state, mockDb, false);
324
+
325
+ expect(state.streamingEnabled).toBe(false);
326
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
327
+ mockDb,
328
+ CONFIG_KEYS.STREAMING_ENABLED,
329
+ "false",
330
+ );
331
+ });
332
+ });
333
+
334
+ describe("setTaskServerEnabled", () => {
335
+ it("normalizes truthy values and persists with custom setConfig", async () => {
336
+ const state = makeState({ taskServerEnabled: false });
337
+ const { calls, mockFn } = mockCustomSetConfig();
338
+
339
+ await setTaskServerEnabled(state, mockDb, true, mockFn);
340
+
341
+ expect(state.taskServerEnabled).toBe(true);
342
+ expect(calls).toEqual([
343
+ { key: CONFIG_KEYS.TASK_SERVER_ENABLED, value: "true" },
344
+ ]);
345
+ });
346
+
347
+ it("normalizes falsy values and persists with default setConfig", async () => {
348
+ const state = makeState({ taskServerEnabled: true });
349
+
350
+ await setTaskServerEnabled(state, mockDb, false);
351
+
352
+ expect(state.taskServerEnabled).toBe(false);
353
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
354
+ mockDb,
355
+ CONFIG_KEYS.TASK_SERVER_ENABLED,
356
+ "false",
357
+ );
358
+ });
236
359
  });
237
360
 
238
361
  describe("setTaskServerUrl", () => {
239
- it("defaults to config default when empty", async () => {
362
+ it("defaults to config default when empty with custom setConfig", async () => {
240
363
  const state = makeState();
241
- const { calls, mockFn } = mockSetConfig();
364
+ const { calls, mockFn } = mockCustomSetConfig();
242
365
 
243
- await setTaskServerUrl(state, {} as any, "", mockFn);
366
+ await setTaskServerUrl(state, mockDb, "", mockFn);
244
367
 
245
- expect(state.taskServerUrl).toBe("http://localhost:8888/schedule");
246
- expect(calls[0].value).toBe("http://localhost:8888/schedule");
368
+ expect(state.taskServerUrl).toBe(DEFAULT_TASK_SERVER_URL);
369
+ expect(calls[0].value).toBe(DEFAULT_TASK_SERVER_URL);
370
+ });
371
+
372
+ it("persists provided url with default setConfig", async () => {
373
+ const state = makeState();
374
+
375
+ await setTaskServerUrl(state, mockDb, "http://tasks.lan:9999");
376
+
377
+ expect(state.taskServerUrl).toBe("http://tasks.lan:9999");
378
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
379
+ mockDb,
380
+ CONFIG_KEYS.TASK_SERVER_URL,
381
+ "http://tasks.lan:9999",
382
+ );
247
383
  });
248
384
  });
249
385
 
250
386
  describe("setUseProxy", () => {
251
- it("normalizes and persists boolean", async () => {
387
+ it("normalizes and persists boolean with custom setConfig", async () => {
252
388
  const state = makeState();
253
- const { calls, mockFn } = mockSetConfig();
389
+ const { calls, mockFn } = mockCustomSetConfig();
254
390
 
255
- await setUseProxy(state, {} as any, true, mockFn);
391
+ await setUseProxy(state, mockDb, true, mockFn);
256
392
 
257
393
  expect(state.useProxy).toBe(true);
258
394
  expect(calls[0].value).toBe("true");
395
+ expect(mockSyncProxyConfigToServiceWorker).toHaveBeenCalledWith(state);
396
+ });
397
+
398
+ it("normalizes and persists false with default setConfig", async () => {
399
+ const state = makeState({ useProxy: true });
400
+
401
+ await setUseProxy(state, mockDb, false);
402
+
403
+ expect(state.useProxy).toBe(false);
404
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
405
+ mockDb,
406
+ CONFIG_KEYS.USE_PROXY,
407
+ "false",
408
+ );
409
+ expect(mockSyncProxyConfigToServiceWorker).toHaveBeenCalledWith(state);
259
410
  });
260
411
  });
261
412
 
262
413
  describe("setVMBashFullInternetAccess", () => {
263
- it("normalizes and persists boolean", async () => {
414
+ it("normalizes and persists boolean with custom setConfig", async () => {
264
415
  const state = makeState();
265
- const { calls, mockFn } = mockSetConfig();
416
+ const { calls, mockFn } = mockCustomSetConfig();
266
417
 
267
- await setVMBashFullInternetAccess(state, {} as any, true, mockFn);
418
+ await setVMBashFullInternetAccess(state, mockDb, true, mockFn);
268
419
 
269
420
  expect(state.vmBashFullInternetAccess).toBe(true);
270
421
  expect(calls[0].value).toBe("true");
271
422
  });
423
+
424
+ it("normalizes and persists false with default setConfig", async () => {
425
+ const state = makeState({ vmBashFullInternetAccess: true });
426
+
427
+ await setVMBashFullInternetAccess(state, mockDb, false);
428
+
429
+ expect(state.vmBashFullInternetAccess).toBe(false);
430
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
431
+ mockDb,
432
+ CONFIG_KEYS.VM_BASH_FULL_INTERNET_ACCESS,
433
+ "false",
434
+ );
435
+ });
272
436
  });
273
437
 
274
438
  describe("setVMBashTimeout", () => {
275
- it("clamps to [1, 1800]", async () => {
276
- const { calls, mockFn } = mockSetConfig();
439
+ it("clamps to [1, 1800] with custom setConfig", async () => {
440
+ const { calls, mockFn } = mockCustomSetConfig();
277
441
  const state = makeState();
278
442
 
279
- await setVMBashTimeout(state, {} as any, 0, mockFn);
443
+ await setVMBashTimeout(state, mockDb, 0, mockFn);
280
444
  expect(calls[0].value).toBe("1");
281
445
 
282
- await setVMBashTimeout(state, {} as any, 99999, mockFn);
446
+ await setVMBashTimeout(state, mockDb, 99999, mockFn);
283
447
  expect(calls[1].value).toBe("1800");
284
448
 
285
- await setVMBashTimeout(state, {} as any, 120, mockFn);
449
+ await setVMBashTimeout(state, mockDb, 120, mockFn);
286
450
  expect(calls[2].value).toBe("120");
287
451
  });
288
452
 
289
- it("floors fractional seconds", async () => {
453
+ it("floors fractional seconds and persists with default setConfig", async () => {
290
454
  const state = makeState();
291
- const { calls, mockFn } = mockSetConfig();
292
455
 
293
- await setVMBashTimeout(state, {} as any, 45.9, mockFn);
456
+ await setVMBashTimeout(state, mockDb, 45.9);
294
457
 
295
- expect(calls[0].value).toBe("45");
458
+ expect(mockDefaultSetConfig).toHaveBeenCalledWith(
459
+ mockDb,
460
+ CONFIG_KEYS.VM_BASH_TIMEOUT_SEC,
461
+ "45",
462
+ );
296
463
  });
297
464
  });
298
465
  });