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.
- package/.well-known/mcp/server-card.json +1 -1
- package/.well-known/mcp.json +1 -1
- package/README.md +9 -9
- package/dist/cli/build/build.js +1 -1
- package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
- package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
- package/dist/cli/cli.js +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
- package/dist/public/.well-known/mcp/server-card.json +1 -1
- package/dist/public/.well-known/mcp.json +1 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/README.md +9 -9
- package/dist/public/docs/README.md +1 -1
- package/dist/public/docs/architecture/orchestrator.md +2 -2
- package/dist/public/docs/architecture/overview.md +3 -3
- package/dist/public/docs/architecture/worker-protocol.md +29 -29
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/example/article.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/publishing.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/skill-creator.html +1 -1
- package/dist/public/docs/subsystems/local-models.md +1 -1
- package/dist/public/docs/subsystems/providers.md +11 -4
- package/dist/public/e2e/README.md +2 -2
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/main/memory.html +1 -1
- package/dist/public/main.html +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
- package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
- package/package.json +1 -1
- package/src/cli/build/build.test.ts +95 -0
- package/src/cli/build/build.ts +5 -1
- package/src/cli/commands/agent.test.ts +134 -0
- package/src/cli/commands/backup.test.ts +85 -0
- package/src/cli/commands/clients.test.ts +32 -0
- package/src/cli/commands/mcp.test.ts +260 -1
- package/src/cli/commands/send-file.test.ts +73 -0
- package/src/cli/commands/send.test.ts +58 -0
- package/src/cli/commands/tasks.test.ts +78 -0
- package/src/cli/commands/webrtc-listen.test.ts +74 -0
- package/src/cli/tools/assert-version-bump.test.ts +30 -1
- package/src/cli/tools/file-search-replace.test.ts +33 -1
- package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
- package/src/cli/tools/touch-nojekyll.test.ts +12 -0
- package/src/cli/tools/version.test.ts +109 -5
- package/src/cli/utils/agent-core.test.ts +40 -0
- package/src/cli/utils/control-client.test.ts +239 -2
- package/src/cli/utils/load-workspace-config.test.ts +63 -0
- package/src/cli/utils/local-models.test.ts +98 -0
- package/src/cli/utils/progress-bar.test.ts +41 -2
- package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
- package/src/cli/utils/resolve-project-root.test.ts +17 -0
- package/src/cli/utils/stdin.test.ts +38 -0
- package/src/cli/utils/stdio.test.ts +51 -1
- package/src/cli/utils/suppress-warnings.test.ts +9 -0
- package/src/cli/utils/webrtc-control-client.test.ts +569 -0
- package/src/common/utils/frontmatter.test.ts +117 -0
- package/src/components/common/help/providers.test.ts +67 -0
- package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
- package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
- package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
- package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
- package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
- package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
- package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
- package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
- package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
- package/src/server/logger.test.ts +57 -0
- package/src/server/peer.test.ts +47 -0
- package/src/server/routes/gemini.test.ts +244 -0
- package/src/server/server-peer.test.ts +245 -11
- package/src/server/services/llamafile-manager.test.ts +668 -0
- package/src/share-target/pending-shares.test.ts +90 -0
- package/src/shell/fs.test.ts +125 -0
- package/src/shell/shell.test.ts +8 -0
- package/src/subsystems/git/credentials.test.ts +260 -0
- package/src/subsystems/git/git.test.ts +64 -0
- package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
- package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
- package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
- package/src/worker/tools/email/email.test.ts +358 -0
- package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
- package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
- package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
- 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 {
|
|
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 {
|
|
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("
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
});
|