shadow-claw 1.52.0 → 1.52.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 (112) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +9 -9
  4. package/dist/cli/build/build.js +1 -1
  5. package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
  6. package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
  7. package/dist/cli/cli.js +1 -1
  8. package/dist/lib/components/index.js +1 -1
  9. package/dist/lib/components/shadow-claw-pages.js +1 -1
  10. package/dist/lib/index.js +1 -1
  11. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
  12. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
  13. package/dist/public/.well-known/mcp/server-card.json +1 -1
  14. package/dist/public/.well-known/mcp.json +1 -1
  15. package/dist/public/AGENTS.md +1 -1
  16. package/dist/public/README.md +9 -9
  17. package/dist/public/docs/README.md +1 -1
  18. package/dist/public/docs/architecture/orchestrator.md +2 -2
  19. package/dist/public/docs/architecture/overview.md +3 -3
  20. package/dist/public/docs/architecture/worker-protocol.md +29 -29
  21. package/dist/public/docs/example/article/index.html +1 -1
  22. package/dist/public/docs/example/article.html +1 -1
  23. package/dist/public/docs/publishing/index.html +1 -1
  24. package/dist/public/docs/publishing.html +1 -1
  25. package/dist/public/docs/skill-creator/index.html +1 -1
  26. package/dist/public/docs/skill-creator.html +1 -1
  27. package/dist/public/docs/subsystems/local-models.md +1 -1
  28. package/dist/public/docs/subsystems/providers.md +11 -4
  29. package/dist/public/e2e/README.md +2 -2
  30. package/dist/public/index.html +1 -1
  31. package/dist/public/index.js +1 -1
  32. package/dist/public/llms.txt +1 -1
  33. package/dist/public/main/index.html +1 -1
  34. package/dist/public/main/memory/index.html +1 -1
  35. package/dist/public/main/memory.html +1 -1
  36. package/dist/public/main.html +1 -1
  37. package/dist/public/service-worker.js +1 -1
  38. package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
  39. package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/build/build.test.ts +95 -0
  42. package/src/cli/build/build.ts +5 -1
  43. package/src/cli/commands/agent.test.ts +134 -0
  44. package/src/cli/commands/backup.test.ts +85 -0
  45. package/src/cli/commands/clients.test.ts +32 -0
  46. package/src/cli/commands/mcp.test.ts +260 -1
  47. package/src/cli/commands/send-file.test.ts +73 -0
  48. package/src/cli/commands/send.test.ts +58 -0
  49. package/src/cli/commands/tasks.test.ts +78 -0
  50. package/src/cli/commands/webrtc-listen.test.ts +74 -0
  51. package/src/cli/tools/assert-version-bump.test.ts +30 -1
  52. package/src/cli/tools/file-search-replace.test.ts +33 -1
  53. package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
  54. package/src/cli/tools/touch-nojekyll.test.ts +12 -0
  55. package/src/cli/tools/version.test.ts +109 -5
  56. package/src/cli/utils/agent-core.test.ts +40 -0
  57. package/src/cli/utils/control-client.test.ts +239 -2
  58. package/src/cli/utils/load-workspace-config.test.ts +63 -0
  59. package/src/cli/utils/local-models.test.ts +98 -0
  60. package/src/cli/utils/progress-bar.test.ts +41 -2
  61. package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
  62. package/src/cli/utils/resolve-project-root.test.ts +17 -0
  63. package/src/cli/utils/stdin.test.ts +38 -0
  64. package/src/cli/utils/stdio.test.ts +51 -1
  65. package/src/cli/utils/suppress-warnings.test.ts +9 -0
  66. package/src/cli/utils/webrtc-control-client.test.ts +569 -0
  67. package/src/common/utils/frontmatter.test.ts +117 -0
  68. package/src/components/common/help/providers.test.ts +67 -0
  69. package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
  70. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
  71. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
  72. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
  73. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
  74. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
  75. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
  76. package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
  77. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
  78. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
  79. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
  80. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
  81. package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
  82. package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
  83. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
  84. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
  85. package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
  86. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
  87. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
  88. package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
  89. package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
  90. package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
  91. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
  92. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
  93. package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
  94. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
  95. package/src/server/logger.test.ts +57 -0
  96. package/src/server/peer.test.ts +47 -0
  97. package/src/server/routes/gemini.test.ts +244 -0
  98. package/src/server/server-peer.test.ts +245 -11
  99. package/src/server/services/llamafile-manager.test.ts +668 -0
  100. package/src/share-target/pending-shares.test.ts +90 -0
  101. package/src/shell/fs.test.ts +125 -0
  102. package/src/shell/shell.test.ts +8 -0
  103. package/src/subsystems/git/credentials.test.ts +260 -0
  104. package/src/subsystems/git/git.test.ts +64 -0
  105. package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
  106. package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
  107. package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
  108. package/src/worker/tools/email/email.test.ts +358 -0
  109. package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
  110. package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
  111. package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
  112. package/src/worker/tools/fetch-url/fetch-url.test.ts +295 -0
@@ -138,4 +138,77 @@ describe("runSendFileCommand", () => {
138
138
  expect.stringContaining("The CSV has 2 columns and 1 row."),
139
139
  );
140
140
  });
141
+
142
+ it("fails when path is a directory", async () => {
143
+ await runSendFileCommand(tempDir, {});
144
+ expect(errorSpy).toHaveBeenCalledWith(
145
+ expect.stringContaining("Cannot send directory"),
146
+ );
147
+ expect(process.exitCode).toBe(1);
148
+ });
149
+
150
+ it("fails when no client specified and no clients connected", async () => {
151
+ const filePath = path.join(tempDir, "file.txt");
152
+ fs.writeFileSync(filePath, "data", "utf8");
153
+
154
+ jest
155
+ .spyOn(CliControlClient.prototype, "listClients")
156
+ .mockResolvedValueOnce([]);
157
+
158
+ await runSendFileCommand(filePath, {});
159
+
160
+ expect(errorSpy).toHaveBeenCalledWith("Error: No clients are connected.");
161
+ expect(process.exitCode).toBe(1);
162
+ });
163
+
164
+ it("handles client transfer failure", async () => {
165
+ const filePath = path.join(tempDir, "fail.txt");
166
+ fs.writeFileSync(filePath, "data", "utf8");
167
+
168
+ jest
169
+ .spyOn(CliControlClient.prototype, "sendFile")
170
+ .mockResolvedValueOnce({ success: false, error: "Disk quota exceeded" });
171
+
172
+ await runSendFileCommand(filePath, { client: "client-1" });
173
+
174
+ expect(errorSpy).toHaveBeenCalledWith(
175
+ "Error from client: Disk quota exceeded",
176
+ );
177
+ expect(process.exitCode).toBe(1);
178
+ });
179
+
180
+ it("handles exception thrown during file send", async () => {
181
+ const filePath = path.join(tempDir, "err.txt");
182
+ fs.writeFileSync(filePath, "data", "utf8");
183
+
184
+ jest
185
+ .spyOn(CliControlClient.prototype, "sendFile")
186
+ .mockRejectedValueOnce(new Error("Pipe broken"));
187
+
188
+ await runSendFileCommand(filePath, { client: "client-1" });
189
+
190
+ expect(errorSpy).toHaveBeenCalledWith("Error sending file: Pipe broken");
191
+ expect(process.exitCode).toBe(1);
192
+ });
193
+
194
+ it("outputs json when --json is passed and no reply text", async () => {
195
+ const filePath = path.join(tempDir, "json.txt");
196
+ fs.writeFileSync(filePath, "data", "utf8");
197
+
198
+ jest.spyOn(CliControlClient.prototype, "sendFile").mockResolvedValueOnce({
199
+ success: true,
200
+ data: { transferredBytes: 4 },
201
+ });
202
+
203
+ await runSendFileCommand(filePath, {
204
+ client: "client-1",
205
+ json: true,
206
+ name: "custom.txt",
207
+ timeout: 15,
208
+ });
209
+
210
+ expect(logSpy).toHaveBeenCalledWith(
211
+ expect.stringContaining('"transferredBytes": 4'),
212
+ );
213
+ });
141
214
  });
@@ -89,4 +89,62 @@ describe("runSendCommand", () => {
89
89
  }),
90
90
  );
91
91
  });
92
+
93
+ it("auto-resolves client when client option omitted", async () => {
94
+ jest
95
+ .spyOn(CliControlClient.prototype, "listClients")
96
+ .mockResolvedValueOnce([
97
+ { clientId: "c-first", deviceLabel: "My Laptop" },
98
+ ]);
99
+ const sendSpy = jest
100
+ .spyOn(CliControlClient.prototype, "sendCommand")
101
+ .mockResolvedValueOnce({ success: true, data: { status: "ok" } });
102
+
103
+ await runSendCommand("Hello first", { timeout: 10, json: true });
104
+
105
+ expect(sendSpy).toHaveBeenCalledWith(
106
+ "c-first",
107
+ "send-message",
108
+ { text: "Hello first", groupId: undefined },
109
+ 10000,
110
+ );
111
+ expect(logSpy).toHaveBeenCalledWith(
112
+ expect.stringContaining("Targeting client: My Laptop (c-first)"),
113
+ );
114
+ });
115
+
116
+ it("errors when client option omitted and no clients connected", async () => {
117
+ jest
118
+ .spyOn(CliControlClient.prototype, "listClients")
119
+ .mockResolvedValueOnce([]);
120
+
121
+ await runSendCommand("Hello nobody", {});
122
+
123
+ expect(errorSpy).toHaveBeenCalledWith("Error: No clients are connected.");
124
+ expect(process.exitCode).toBe(1);
125
+ });
126
+
127
+ it("handles client error response", async () => {
128
+ jest
129
+ .spyOn(CliControlClient.prototype, "sendCommand")
130
+ .mockResolvedValueOnce({ success: false, error: "Model timeout" });
131
+
132
+ await runSendCommand("Hello failure", { client: "client-target" });
133
+
134
+ expect(errorSpy).toHaveBeenCalledWith("Error from client: Model timeout");
135
+ expect(process.exitCode).toBe(1);
136
+ });
137
+
138
+ it("handles exception during sending", async () => {
139
+ jest
140
+ .spyOn(CliControlClient.prototype, "sendCommand")
141
+ .mockRejectedValueOnce(new Error("Socket closed"));
142
+
143
+ await runSendCommand("Hello exception", { client: "client-target" });
144
+
145
+ expect(errorSpy).toHaveBeenCalledWith(
146
+ "Error sending message: Socket closed",
147
+ );
148
+ expect(process.exitCode).toBe(1);
149
+ });
92
150
  });
@@ -61,4 +61,82 @@ describe("runTasksCommand", () => {
61
61
 
62
62
  expect(logSpy).toHaveBeenCalledWith("No tasks configured on client.");
63
63
  });
64
+
65
+ it("prints empty message with group when no tasks exist for group", async () => {
66
+ jest
67
+ .spyOn(CliControlClient.prototype, "sendCommand")
68
+ .mockResolvedValueOnce({
69
+ success: true,
70
+ data: { tasks: [] },
71
+ });
72
+
73
+ await runTasksCommand({ client: "test-client", group: "my-group" });
74
+
75
+ expect(logSpy).toHaveBeenCalledWith(
76
+ 'No tasks configured on client for group "my-group".',
77
+ );
78
+ });
79
+
80
+ it("resolves default client from listClients when client option omitted", async () => {
81
+ jest
82
+ .spyOn(CliControlClient.prototype, "listClients")
83
+ .mockResolvedValueOnce([{ clientId: "first-client" }]);
84
+ const sendSpy = jest
85
+ .spyOn(CliControlClient.prototype, "sendCommand")
86
+ .mockResolvedValueOnce({
87
+ success: true,
88
+ data: {
89
+ tasks: [{ prompt: "Say hello and do things", enabled: true }],
90
+ },
91
+ });
92
+
93
+ await runTasksCommand({});
94
+
95
+ expect(sendSpy).toHaveBeenCalledWith("first-client", "list-tasks", {
96
+ groupId: undefined,
97
+ });
98
+ expect(logSpy).toHaveBeenCalledWith(
99
+ expect.stringContaining("1. [enabled] Say hello and do things (manual)"),
100
+ );
101
+ });
102
+
103
+ it("errors when client option omitted and no clients connected", async () => {
104
+ jest
105
+ .spyOn(CliControlClient.prototype, "listClients")
106
+ .mockResolvedValueOnce([]);
107
+
108
+ await runTasksCommand({});
109
+
110
+ expect(errorSpy).toHaveBeenCalledWith("Error: No clients connected.");
111
+ expect(process.exitCode).toBe(1);
112
+ });
113
+
114
+ it("handles sendCommand failure", async () => {
115
+ jest
116
+ .spyOn(CliControlClient.prototype, "sendCommand")
117
+ .mockResolvedValueOnce({
118
+ success: false,
119
+ error: "Client unreachable",
120
+ });
121
+
122
+ await runTasksCommand({ client: "offline-client" });
123
+
124
+ expect(errorSpy).toHaveBeenCalledWith(
125
+ "Failed to list tasks: Client unreachable",
126
+ );
127
+ expect(process.exitCode).toBe(1);
128
+ });
129
+
130
+ it("handles exception thrown during execution", async () => {
131
+ jest
132
+ .spyOn(CliControlClient.prototype, "sendCommand")
133
+ .mockRejectedValueOnce(new Error("Connection reset"));
134
+
135
+ await runTasksCommand({ client: "error-client" });
136
+
137
+ expect(errorSpy).toHaveBeenCalledWith(
138
+ "Error executing tasks command: Connection reset",
139
+ );
140
+ expect(process.exitCode).toBe(1);
141
+ });
64
142
  });
@@ -400,5 +400,79 @@ describe("webrtc-listen command", () => {
400
400
  expect(res1.path.includes("shadow-claw")).toBe(true);
401
401
  expect(res1.path.includes("workspace-")).toBe(true);
402
402
  });
403
+
404
+ it("handles error during runWebRtcListenCommand startup", async () => {
405
+ const { CliWebRtcListener } =
406
+ await import("../utils/webrtc-control-client.js");
407
+ jest
408
+ .spyOn(CliWebRtcListener.prototype, "start")
409
+ .mockRejectedValueOnce(new Error("Signaling fail"));
410
+ const errorSpy = jest
411
+ .spyOn(console, "error")
412
+ .mockImplementation(() => {});
413
+ await runWebRtcListenCommand({ cacheDir: tmpDir });
414
+ expect(errorSpy).toHaveBeenCalledWith(
415
+ "Error starting WebRTC listener: Signaling fail",
416
+ );
417
+ expect(process.exitCode).toBe(1);
418
+ errorSpy.mockRestore();
419
+ });
420
+
421
+ it("wires up IPC command result handler in _setupConnection during runWebRtcListenCommand", async () => {
422
+ const { CliWebRtcListener } =
423
+ await import("../utils/webrtc-control-client.js");
424
+ jest
425
+ .spyOn(CliWebRtcListener.prototype, "start")
426
+ .mockImplementation(async function (this: any) {
427
+ const listeners: Record<string, Function> = {};
428
+ const mockConn = {
429
+ on: (ev: string, cb: any) => {
430
+ listeners[ev] = cb;
431
+ },
432
+ };
433
+ let receivedPayload: any = null;
434
+ this._pendingIpc.set("cmd-1", (payload: any) => {
435
+ receivedPayload = payload;
436
+ });
437
+
438
+ this._setupConnection(mockConn, "remote-peer-1");
439
+
440
+ // Test non-object/null
441
+ listeners["data"]?.(null);
442
+ // Test invalid JSON string
443
+ listeners["data"]?.("invalid-json");
444
+ // Test valid command result
445
+ listeners["data"]?.(
446
+ JSON.stringify({
447
+ type: "command:result",
448
+ payload: { commandId: "cmd-1", success: true },
449
+ }),
450
+ );
451
+
452
+ expect(receivedPayload).toEqual({
453
+ commandId: "cmd-1",
454
+ success: true,
455
+ });
456
+ throw new Error("Simulated stop");
457
+ });
458
+
459
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
460
+ const errorSpy = jest
461
+ .spyOn(console, "error")
462
+ .mockImplementation(() => {});
463
+
464
+ await runWebRtcListenCommand({
465
+ cacheDir: tmpDir,
466
+ secure: true,
467
+ insecure: true,
468
+ trustedPeer: ["peer-a", "peer-b"],
469
+ });
470
+
471
+ expect(errorSpy).toHaveBeenCalledWith(
472
+ "Error starting WebRTC listener: Simulated stop",
473
+ );
474
+ logSpy.mockRestore();
475
+ errorSpy.mockRestore();
476
+ });
403
477
  });
404
478
  });
@@ -1,8 +1,9 @@
1
- import { describe, it, expect } from "@jest/globals";
1
+ import { describe, it, expect, jest } from "@jest/globals";
2
2
  import {
3
3
  parseSemver,
4
4
  compareSemver,
5
5
  assertVersionBump,
6
+ main,
6
7
  } from "./assert-version-bump.js";
7
8
 
8
9
  describe("assert-version-bump", () => {
@@ -122,6 +123,34 @@ describe("assert-version-bump", () => {
122
123
  }),
123
124
  ).resolves.toBeUndefined();
124
125
  });
126
+
127
+ it("logs message when silent is false and versions pass", async () => {
128
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
129
+ try {
130
+ await assertVersionBump({
131
+ localPkgVersion: "1.23.4",
132
+ localLockVersion: "1.23.4",
133
+ wellKnownMcpVersion: "1.23.4",
134
+ wellKnownServerCardVersion: "1.23.4",
135
+ pkgName: "shadow-claw",
136
+ publishedVersion: "1.23.3",
137
+ silent: false,
138
+ });
139
+ expect(logSpy).toHaveBeenCalledWith(
140
+ expect.stringContaining("Version assertion passed"),
141
+ );
142
+ } finally {
143
+ logSpy.mockRestore();
144
+ }
145
+ });
146
+
147
+ it("runs main() and validates repository version against npm", async () => {
148
+ try {
149
+ await main();
150
+ } catch (err: any) {
151
+ expect(err.message).toMatch(/Version assertion failed/);
152
+ }
153
+ });
125
154
  });
126
155
 
127
156
  describe("repository live version parity", () => {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, jest } from "@jest/globals";
2
2
 
3
- import { fileSearchReplace, readStdin } from "./file-search-replace.js";
3
+ import { fileSearchReplace, readStdin, main } from "./file-search-replace.js";
4
4
 
5
5
  describe("readStdin", () => {
6
6
  it("joins stdin chunks and trims the result", async () => {
@@ -69,4 +69,36 @@ describe("fileSearchReplace", () => {
69
69
  ).rejects.toThrow(SyntaxError);
70
70
  expect(readFileImpl).not.toHaveBeenCalled();
71
71
  });
72
+
73
+ it("joins string chunks in readStdin", async () => {
74
+ const stdin = (async function* () {
75
+ yield " non-buffer ";
76
+ yield "chunk \n";
77
+ })();
78
+
79
+ await expect(readStdin(stdin)).resolves.toBe("non-buffer chunk");
80
+ });
81
+
82
+ it("main() replaces content when valid arguments are passed", async () => {
83
+ const fs = await import("node:fs/promises");
84
+ const os = await import("node:os");
85
+ const path = await import("node:path");
86
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fsr-test-"));
87
+ const tempFile = path.join(tempDir, "replace-target.txt");
88
+ await fs.writeFile(tempFile, "apple and orange and apple", "utf8");
89
+
90
+ const origLength = process.argv.length;
91
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
92
+
93
+ try {
94
+ process.argv.push("apple", tempFile, "", "banana");
95
+ await main();
96
+ const updated = await fs.readFile(tempFile, "utf8");
97
+ expect(updated).toBe("banana and orange and banana");
98
+ } finally {
99
+ process.argv.length = origLength;
100
+ logSpy.mockRestore();
101
+ await fs.rm(tempDir, { recursive: true, force: true });
102
+ }
103
+ });
72
104
  });
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it, jest } from "@jest/globals";
2
2
 
3
- import { patchServiceWorkerTrustedTypesFile } from "./patch-service-worker-trusted-types.js";
3
+ import {
4
+ patchServiceWorkerTrustedTypesFile,
5
+ main,
6
+ } from "./patch-service-worker-trusted-types.js";
4
7
 
5
8
  describe("patchServiceWorkerTrustedTypesFile", () => {
6
9
  it("writes the patched service worker when content changes", async () => {
@@ -69,4 +72,24 @@ describe("patchServiceWorkerTrustedTypesFile", () => {
69
72
  }),
70
73
  ).rejects.toBe(error);
71
74
  });
75
+
76
+ it("main() patches target path specified in process.argv[2]", async () => {
77
+ const fs = await import("node:fs/promises");
78
+ const os = await import("node:os");
79
+ const path = await import("node:path");
80
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "sw-patch-"));
81
+ const tempFile = path.join(tempDir, "service-worker.js");
82
+ await fs.writeFile(tempFile, "/* sw content */", "utf8");
83
+
84
+ const prevArgv = process.argv;
85
+ try {
86
+ process.argv = ["node", "patch.js", tempFile];
87
+ await main();
88
+ const content = await fs.readFile(tempFile, "utf8");
89
+ expect(content).toBeDefined();
90
+ } finally {
91
+ process.argv = prevArgv;
92
+ await fs.rm(tempDir, { recursive: true, force: true });
93
+ }
94
+ });
72
95
  });
@@ -15,4 +15,16 @@ describe("touchNoJekyll", () => {
15
15
  "utf-8",
16
16
  );
17
17
  });
18
+
19
+ it("uses default project root dist/public when targetDir is omitted", async () => {
20
+ const writeFileImpl = jest.fn<any>().mockResolvedValue(undefined);
21
+
22
+ await touchNoJekyll(undefined, { writeFileImpl });
23
+
24
+ expect(writeFileImpl).toHaveBeenCalledWith(
25
+ expect.stringMatching(/dist\/public\/\.nojekyll$/),
26
+ "",
27
+ "utf-8",
28
+ );
29
+ });
18
30
  });
@@ -1,4 +1,11 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ beforeEach,
6
+ afterEach,
7
+ jest,
8
+ } from "@jest/globals";
2
9
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
10
  import os from "node:os";
4
11
  import path from "node:path";
@@ -125,10 +132,27 @@ describe("src/cli/tools/version.ts", () => {
125
132
  expect(result.versions["shadow-claw"]).toBeDefined();
126
133
  });
127
134
 
128
- it("returns help action when --help flag is provided", async () => {
129
- const result = await runVersion(["--help"], { stdio: "pipe" });
130
- expect(result.action).toBe("help");
131
- expect(result.message).toContain(USAGE_NOTICE);
135
+ it("logs version map when stdio is inherit and no arguments provided", async () => {
136
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
137
+ try {
138
+ const result = await runVersion([], { stdio: "inherit" });
139
+ expect(result.action).toBe("list");
140
+ expect(logSpy).toHaveBeenCalled();
141
+ } finally {
142
+ logSpy.mockRestore();
143
+ }
144
+ });
145
+
146
+ it("returns help action and logs USAGE_NOTICE when stdio is inherit", async () => {
147
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
148
+ try {
149
+ const result = await runVersion(["--help"], { stdio: "inherit" });
150
+ expect(result.action).toBe("help");
151
+ expect(result.message).toContain(USAGE_NOTICE);
152
+ expect(logSpy).toHaveBeenCalledWith(USAGE_NOTICE);
153
+ } finally {
154
+ logSpy.mockRestore();
155
+ }
132
156
  });
133
157
 
134
158
  it("throws error when disallowed keyword is provided", async () => {
@@ -136,5 +160,85 @@ describe("src/cli/tools/version.ts", () => {
136
160
  'Keyword command "minor" is not supported',
137
161
  );
138
162
  });
163
+
164
+ it("handles isLifecycle mode with git directory", async () => {
165
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "lifecycle-test-"));
166
+ try {
167
+ await writeFile(
168
+ path.join(tempDir, "package.json"),
169
+ JSON.stringify({ name: "test-pkg", version: "2.0.0" }),
170
+ "utf8",
171
+ );
172
+ await mkdir(path.join(tempDir, ".well-known/mcp"), { recursive: true });
173
+ await writeFile(
174
+ path.join(tempDir, ".well-known/mcp.json"),
175
+ JSON.stringify({ version: "1.0.0" }),
176
+ "utf8",
177
+ );
178
+ await mkdir(path.join(tempDir, ".git"), { recursive: true });
179
+
180
+ const result = await runVersion([], {
181
+ isLifecycle: true,
182
+ rootDir: tempDir,
183
+ });
184
+
185
+ expect(result.version).toBe("2.0.0");
186
+ expect(result.updated).toContain(".well-known/mcp.json");
187
+ } finally {
188
+ await rm(tempDir, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ it("handles syncWellKnownVersions JSON parse error gracefully", async () => {
193
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "corrupt-test-"));
194
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
195
+ try {
196
+ await mkdir(path.join(tempDir, ".well-known"), { recursive: true });
197
+ await writeFile(
198
+ path.join(tempDir, ".well-known/mcp.json"),
199
+ "invalid-json",
200
+ "utf8",
201
+ );
202
+
203
+ const updated = await syncWellKnownVersions("1.2.3", tempDir);
204
+ expect(updated).toEqual([]);
205
+ expect(warnSpy).toHaveBeenCalled();
206
+ } finally {
207
+ warnSpy.mockRestore();
208
+ await rm(tempDir, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ it("executes npm version and syncs well-known when valid semver given", async () => {
213
+ const tempDir = await mkdtemp(
214
+ path.join(os.tmpdir(), "npm-version-test-"),
215
+ );
216
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
217
+ try {
218
+ await writeFile(
219
+ path.join(tempDir, "package.json"),
220
+ JSON.stringify({ name: "version-bump-test", version: "1.0.0" }),
221
+ "utf8",
222
+ );
223
+ await mkdir(path.join(tempDir, ".well-known/mcp"), { recursive: true });
224
+ await writeFile(
225
+ path.join(tempDir, ".well-known/mcp.json"),
226
+ JSON.stringify({ version: "1.0.0" }),
227
+ "utf8",
228
+ );
229
+
230
+ const result = await runVersion(["1.0.1"], {
231
+ rootDir: tempDir,
232
+ stdio: "inherit",
233
+ execOptions: { cwd: tempDir, stdio: "pipe" },
234
+ });
235
+
236
+ expect(result.version).toBe("1.0.1");
237
+ expect(result.updated).toContain(".well-known/mcp.json");
238
+ } finally {
239
+ logSpy.mockRestore();
240
+ await rm(tempDir, { recursive: true, force: true });
241
+ }
242
+ });
139
243
  });
140
244
  });
@@ -0,0 +1,40 @@
1
+ import { jest } from "@jest/globals";
2
+ import { getAgentCore } from "./agent-core.js";
3
+
4
+ describe("agent-core", () => {
5
+ it("loads and caches the headless-agent core module", async () => {
6
+ const core1 = await getAgentCore();
7
+ expect(core1).toBeDefined();
8
+
9
+ // second call should return cached instance
10
+ const core2 = await getAgentCore();
11
+ expect(core2).toBe(core1);
12
+ });
13
+
14
+ it("loads from dist when NODE_ENV is not test", async () => {
15
+ const prevEnv = process.env.NODE_ENV;
16
+ try {
17
+ process.env.NODE_ENV = "production";
18
+ await jest.isolateModulesAsync(async () => {
19
+ const { getAgentCore: isolatedGetAgentCore } =
20
+ await import("./agent-core.js");
21
+ const core = await isolatedGetAgentCore();
22
+ expect(core).toBeDefined();
23
+ });
24
+ } finally {
25
+ process.env.NODE_ENV = prevEnv;
26
+ }
27
+ });
28
+
29
+ it("falls back to dist/headless-agent.js when worker/headless-agent import fails", async () => {
30
+ await jest.isolateModulesAsync(async () => {
31
+ jest.unstable_mockModule("../../worker/headless-agent.js", () => {
32
+ throw new Error("Module failed to load");
33
+ });
34
+ const { getAgentCore: isolatedGetAgentCore } =
35
+ await import("./agent-core.js");
36
+ const core = await isolatedGetAgentCore();
37
+ expect(core).toBeDefined();
38
+ });
39
+ });
40
+ });