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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createFrontmatterDetailsElement,
|
|
3
|
+
createFrontmatterElement,
|
|
3
4
|
PostFrontmatter,
|
|
4
5
|
renderFrontmatterMarkup,
|
|
5
6
|
splitFrontmatter,
|
|
@@ -24,6 +25,60 @@ describe("frontmatter utilities", () => {
|
|
|
24
25
|
expect(parsed.content.trim()).toBe("# Heading");
|
|
25
26
|
});
|
|
26
27
|
|
|
28
|
+
it("handles markdown with no frontmatter or empty input in splitFrontmatter", () => {
|
|
29
|
+
const emptyParsed = splitFrontmatter("");
|
|
30
|
+
expect(emptyParsed.data).toEqual({});
|
|
31
|
+
expect(emptyParsed.content).toBe("");
|
|
32
|
+
|
|
33
|
+
const plainParsed = splitFrontmatter("Just plain markdown");
|
|
34
|
+
expect(plainParsed.data).toEqual({});
|
|
35
|
+
expect(plainParsed.content).toBe("Just plain markdown");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("createFrontmatterElement", () => {
|
|
39
|
+
it("renders header with title and updated timestamp", () => {
|
|
40
|
+
const element = createFrontmatterElement(
|
|
41
|
+
{
|
|
42
|
+
title: "My Article",
|
|
43
|
+
updated: "2026-03-15T10:00:00Z",
|
|
44
|
+
created: "2026-01-01T10:00:00Z",
|
|
45
|
+
} as PostFrontmatter,
|
|
46
|
+
document,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(element.tagName.toLowerCase()).toBe("header");
|
|
50
|
+
expect(element.getAttribute("class")).toBe("markdown-frontmatter");
|
|
51
|
+
|
|
52
|
+
const title = element.querySelector("h1");
|
|
53
|
+
expect(title?.textContent).toBe("My Article");
|
|
54
|
+
expect(title?.getAttribute("class")).toBe("markdown-frontmatter__title");
|
|
55
|
+
|
|
56
|
+
const time = element.querySelector("time");
|
|
57
|
+
expect(time?.getAttribute("datetime")).toBe("2026-03-15T10:00:00Z");
|
|
58
|
+
expect(time?.textContent).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("falls back to created timestamp when updated is missing", () => {
|
|
62
|
+
const element = createFrontmatterElement(
|
|
63
|
+
{
|
|
64
|
+
created: "2026-01-01T10:00:00Z",
|
|
65
|
+
} as PostFrontmatter,
|
|
66
|
+
document,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const title = element.querySelector("h1");
|
|
70
|
+
expect(title).toBeNull();
|
|
71
|
+
|
|
72
|
+
const time = element.querySelector("time");
|
|
73
|
+
expect(time?.getAttribute("datetime")).toBe("2026-01-01T10:00:00Z");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders empty header container when no title or timestamp is provided", () => {
|
|
77
|
+
const element = createFrontmatterElement({} as PostFrontmatter, document);
|
|
78
|
+
expect(element.children.length).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
27
82
|
it("builds frontmatter DOM element using createElement semantics", () => {
|
|
28
83
|
const element = createFrontmatterDetailsElement(
|
|
29
84
|
{ title: "Post", slug: "post" } as PostFrontmatter,
|
|
@@ -43,6 +98,16 @@ describe("frontmatter utilities", () => {
|
|
|
43
98
|
expect(code?.textContent).toContain('"slug": "post"');
|
|
44
99
|
});
|
|
45
100
|
|
|
101
|
+
it("throws in createFrontmatterDetailsElement if documentInstance is invalid", () => {
|
|
102
|
+
expect(() =>
|
|
103
|
+
createFrontmatterDetailsElement({} as PostFrontmatter, null as any),
|
|
104
|
+
).toThrow("A valid documentInstance is required");
|
|
105
|
+
|
|
106
|
+
expect(() =>
|
|
107
|
+
createFrontmatterDetailsElement({} as PostFrontmatter, {} as any),
|
|
108
|
+
).toThrow("A valid documentInstance is required");
|
|
109
|
+
});
|
|
110
|
+
|
|
46
111
|
it("serializes markup in browser via DOM outerHTML", () => {
|
|
47
112
|
const html = renderFrontmatterMarkup(
|
|
48
113
|
{ title: "Browser Title" } as PostFrontmatter,
|
|
@@ -58,6 +123,58 @@ describe("frontmatter utilities", () => {
|
|
|
58
123
|
expect(html).toContain(""title": "Browser Title"");
|
|
59
124
|
});
|
|
60
125
|
|
|
126
|
+
it("uses global document by default in renderFrontmatterMarkup", () => {
|
|
127
|
+
const html = renderFrontmatterMarkup(
|
|
128
|
+
{ title: "Default Document" } as PostFrontmatter,
|
|
129
|
+
createFrontmatterElement,
|
|
130
|
+
);
|
|
131
|
+
expect(html).toContain("markdown-frontmatter");
|
|
132
|
+
expect(html).toContain("Default Document");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("falls back to XMLSerializer when outerHTML is not available", () => {
|
|
136
|
+
const customRenderer = (_data: any, doc: Document) => {
|
|
137
|
+
const el = doc.createElement("section");
|
|
138
|
+
el.setAttribute("class", "xml-fallback");
|
|
139
|
+
Object.defineProperty(el, "outerHTML", {
|
|
140
|
+
value: undefined,
|
|
141
|
+
configurable: true,
|
|
142
|
+
});
|
|
143
|
+
return el;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const html = renderFrontmatterMarkup(
|
|
147
|
+
{} as PostFrontmatter,
|
|
148
|
+
customRenderer,
|
|
149
|
+
{ documentInstance: document },
|
|
150
|
+
);
|
|
151
|
+
expect(html).toContain('class="xml-fallback"');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("throws when no serializer is available", () => {
|
|
155
|
+
const customRenderer = () => {
|
|
156
|
+
const el = { outerHTML: undefined };
|
|
157
|
+
return el as any;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const fakeDoc = {
|
|
161
|
+
createElement: () => ({}),
|
|
162
|
+
defaultView: {},
|
|
163
|
+
} as any;
|
|
164
|
+
|
|
165
|
+
const originalXml = (globalThis as any).XMLSerializer;
|
|
166
|
+
try {
|
|
167
|
+
delete (globalThis as any).XMLSerializer;
|
|
168
|
+
expect(() =>
|
|
169
|
+
renderFrontmatterMarkup({} as PostFrontmatter, customRenderer, {
|
|
170
|
+
documentInstance: fakeDoc,
|
|
171
|
+
}),
|
|
172
|
+
).toThrow("No serializer available for frontmatter DOM element.");
|
|
173
|
+
} finally {
|
|
174
|
+
(globalThis as any).XMLSerializer = originalXml;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
61
178
|
it("serializes markup via injected serializer for xmldom-style usage", () => {
|
|
62
179
|
const fakeDocument = {
|
|
63
180
|
createTextNode(text: string) {
|
|
@@ -89,4 +89,71 @@ describe("provider help", () => {
|
|
|
89
89
|
dialog.details?.some((line) => line.includes("Runtime missing")),
|
|
90
90
|
).toBe(true);
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
it("checks isLlamafileResolutionError", async () => {
|
|
94
|
+
const { isLlamafileResolutionError } = await import("./llamafile.js");
|
|
95
|
+
expect(isLlamafileResolutionError("assets/cache/llamafile not found")).toBe(
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
isLlamafileResolutionError(
|
|
100
|
+
"missing or invalid 'model' parameter for cli mode",
|
|
101
|
+
),
|
|
102
|
+
).toBe(true);
|
|
103
|
+
expect(
|
|
104
|
+
isLlamafileResolutionError("failed to list llamafile binaries"),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
expect(isLlamafileResolutionError("some random error")).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("checks isTransformersJsResolutionError", async () => {
|
|
110
|
+
const { isTransformersJsResolutionError } =
|
|
111
|
+
await import("./transformers.js");
|
|
112
|
+
expect(
|
|
113
|
+
isTransformersJsResolutionError(
|
|
114
|
+
"transformers.js runtime is not installed",
|
|
115
|
+
),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
expect(
|
|
118
|
+
isTransformersJsResolutionError("transformers.js runtime is unavailable"),
|
|
119
|
+
).toBe(true);
|
|
120
|
+
expect(
|
|
121
|
+
isTransformersJsResolutionError(
|
|
122
|
+
"no supported transformers.js model loader",
|
|
123
|
+
),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
expect(
|
|
126
|
+
isTransformersJsResolutionError(
|
|
127
|
+
"cannot find package @huggingface/transformers",
|
|
128
|
+
),
|
|
129
|
+
).toBe(true);
|
|
130
|
+
expect(isTransformersJsResolutionError("network error")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("builds dialogs for api-key-invalid and provider-unreachable", () => {
|
|
134
|
+
const invalidDialog = buildProviderHelpDialogOptions(
|
|
135
|
+
"openrouter",
|
|
136
|
+
"api-key-invalid",
|
|
137
|
+
);
|
|
138
|
+
expect(invalidDialog.title).toContain("Authentication Failed");
|
|
139
|
+
|
|
140
|
+
const localUnreachable = buildProviderHelpDialogOptions(
|
|
141
|
+
"transformers_js_local",
|
|
142
|
+
"provider-unreachable",
|
|
143
|
+
);
|
|
144
|
+
expect(localUnreachable.title).toContain("Not Reachable");
|
|
145
|
+
expect(localUnreachable.details?.some((d) => d.includes("npm start"))).toBe(
|
|
146
|
+
true,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const remoteUnreachable = buildProviderHelpDialogOptions(
|
|
150
|
+
"openrouter",
|
|
151
|
+
"provider-unreachable",
|
|
152
|
+
);
|
|
153
|
+
expect(
|
|
154
|
+
remoteUnreachable.details?.some((d) =>
|
|
155
|
+
d.includes("internet connectivity"),
|
|
156
|
+
),
|
|
157
|
+
).toBe(true);
|
|
158
|
+
});
|
|
92
159
|
});
|
|
@@ -158,4 +158,125 @@ describe("shadow-claw-provider-model-picker", () => {
|
|
|
158
158
|
|
|
159
159
|
el.remove();
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
it("handles getValue, setLabels, and invalidateProviderModels", async () => {
|
|
163
|
+
const el = new ShadowClawProviderModelPicker();
|
|
164
|
+
document.body.appendChild(el);
|
|
165
|
+
|
|
166
|
+
el.setValue({ providerId: "prov-1", modelId: "mod-1" });
|
|
167
|
+
expect(el.getValue()).toEqual({ providerId: "prov-1", modelId: "mod-1" });
|
|
168
|
+
|
|
169
|
+
el.setLabels({
|
|
170
|
+
providerLabel: "Custom Provider Label",
|
|
171
|
+
modelLabel: "Custom Model Label",
|
|
172
|
+
});
|
|
173
|
+
const root = el.shadowRoot;
|
|
174
|
+
expect(root?.textContent).toContain("Custom Provider Label");
|
|
175
|
+
|
|
176
|
+
el.invalidateProviderModels("prov-1");
|
|
177
|
+
el.invalidateProviderModels();
|
|
178
|
+
|
|
179
|
+
el.remove();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles various model tool badges and context window structures", async () => {
|
|
183
|
+
const el = new ShadowClawProviderModelPicker();
|
|
184
|
+
document.body.appendChild(el);
|
|
185
|
+
|
|
186
|
+
el.setProviders([
|
|
187
|
+
{
|
|
188
|
+
id: "multi-models",
|
|
189
|
+
name: "Multi Models",
|
|
190
|
+
models: [
|
|
191
|
+
{
|
|
192
|
+
id: "no-tools",
|
|
193
|
+
name: "No Tools",
|
|
194
|
+
supports_tools: false,
|
|
195
|
+
context_window: 4096,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "supported-params",
|
|
199
|
+
name: "Params",
|
|
200
|
+
supported_parameters: ["tools"],
|
|
201
|
+
context_length: 8192,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "no-params",
|
|
205
|
+
name: "No Params",
|
|
206
|
+
supported_parameters: ["temperature"],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "provider-list",
|
|
210
|
+
name: "With Provider List",
|
|
211
|
+
providers: [{ supports_tools: false, context_length: 16384 }],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "provider-tools",
|
|
215
|
+
name: "With Provider Tools",
|
|
216
|
+
providers: [{ supports_tools: true, context_length: 32768 }],
|
|
217
|
+
},
|
|
218
|
+
{ name: "Only Name" },
|
|
219
|
+
],
|
|
220
|
+
} as any,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
el.setValue({ providerId: "multi-models", modelId: "no-tools" });
|
|
224
|
+
await el.render();
|
|
225
|
+
|
|
226
|
+
const root = el.shadowRoot;
|
|
227
|
+
const modelSelect = root?.querySelector(
|
|
228
|
+
'[data-role="model-select"]',
|
|
229
|
+
) as HTMLSelectElement;
|
|
230
|
+
expect(modelSelect).not.toBeNull();
|
|
231
|
+
const options = Array.from(modelSelect.options).map((o) => o.textContent);
|
|
232
|
+
expect(options.some((t) => t?.includes("🚫🛠️"))).toBe(true);
|
|
233
|
+
expect(options.some((t) => t?.includes("🛠️"))).toBe(true);
|
|
234
|
+
|
|
235
|
+
el.remove();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("handles loader failure and custom model input events", async () => {
|
|
239
|
+
const el = new ShadowClawProviderModelPicker();
|
|
240
|
+
document.body.appendChild(el);
|
|
241
|
+
|
|
242
|
+
const failingLoader = jest
|
|
243
|
+
.fn<() => Promise<any[]>>()
|
|
244
|
+
.mockRejectedValue(new Error("Network fail"));
|
|
245
|
+
el.setModelLoader(failingLoader as any);
|
|
246
|
+
el.setProviders([
|
|
247
|
+
{ id: "dynamic", name: "Dynamic", modelsUrl: "https://fail.test" } as any,
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
el.setValue({ providerId: "dynamic", modelId: null });
|
|
251
|
+
await el.render();
|
|
252
|
+
await Promise.resolve();
|
|
253
|
+
await Promise.resolve();
|
|
254
|
+
|
|
255
|
+
const root = el.shadowRoot;
|
|
256
|
+
const modelSelect = root?.querySelector(
|
|
257
|
+
'[data-role="model-select"]',
|
|
258
|
+
) as HTMLSelectElement;
|
|
259
|
+
if (modelSelect) {
|
|
260
|
+
modelSelect.value = "__custom__";
|
|
261
|
+
modelSelect.dispatchEvent(new Event("change"));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const customInput = root?.querySelector(
|
|
265
|
+
'[data-role="custom-model-input"]',
|
|
266
|
+
) as HTMLInputElement;
|
|
267
|
+
const listener = jest.fn();
|
|
268
|
+
el.addEventListener("provider-model-change", listener as EventListener);
|
|
269
|
+
|
|
270
|
+
if (customInput) {
|
|
271
|
+
customInput.value = "typed-model";
|
|
272
|
+
customInput.dispatchEvent(new Event("input"));
|
|
273
|
+
expect(listener).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
detail: { providerId: "dynamic", modelId: "typed-model" },
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
el.remove();
|
|
281
|
+
});
|
|
161
282
|
});
|
|
@@ -423,4 +423,135 @@ describe("shadow-claw-accounts", () => {
|
|
|
423
423
|
|
|
424
424
|
document.body.removeChild(el);
|
|
425
425
|
});
|
|
426
|
+
|
|
427
|
+
it("updates an existing account and supports basic auth", async () => {
|
|
428
|
+
const el = new ShadowClawAccounts();
|
|
429
|
+
document.body.appendChild(el);
|
|
430
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
431
|
+
await el.render();
|
|
432
|
+
el.db = {} as any;
|
|
433
|
+
|
|
434
|
+
el.accounts = [
|
|
435
|
+
{
|
|
436
|
+
id: "acct-existing",
|
|
437
|
+
label: "Old Label",
|
|
438
|
+
service: "GitHub",
|
|
439
|
+
hostPattern: "api.github.com",
|
|
440
|
+
authMode: "token",
|
|
441
|
+
token: "old-token",
|
|
442
|
+
} as any,
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
el.showAccountForm("acct-existing");
|
|
446
|
+
|
|
447
|
+
const root = el.shadowRoot!;
|
|
448
|
+
(
|
|
449
|
+
root.querySelector('[data-field="acct-label"]') as HTMLInputElement
|
|
450
|
+
).value = "Updated Label";
|
|
451
|
+
(
|
|
452
|
+
root.querySelector('[data-field="acct-auth-mode"]') as HTMLSelectElement
|
|
453
|
+
).value = "basic";
|
|
454
|
+
(
|
|
455
|
+
root.querySelector(
|
|
456
|
+
'[data-field="acct-basic-username"]',
|
|
457
|
+
) as HTMLInputElement
|
|
458
|
+
).value = "admin";
|
|
459
|
+
(
|
|
460
|
+
root.querySelector('[data-field="acct-token"]') as HTMLInputElement
|
|
461
|
+
).value = "secret-pass";
|
|
462
|
+
|
|
463
|
+
await el.saveAccountForm();
|
|
464
|
+
|
|
465
|
+
expect(showSuccess).toHaveBeenCalledWith("Account updated", 3000);
|
|
466
|
+
const updated = el.accounts.find((a) => a.id === "acct-existing");
|
|
467
|
+
expect(updated?.label).toBe("Updated Label");
|
|
468
|
+
expect(updated?.authMode).toBe("basic");
|
|
469
|
+
expect(updated?.basicUsername).toBe("admin");
|
|
470
|
+
|
|
471
|
+
document.body.removeChild(el);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("handles validation errors when saving account", async () => {
|
|
475
|
+
const el = new ShadowClawAccounts();
|
|
476
|
+
document.body.appendChild(el);
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
478
|
+
await el.render();
|
|
479
|
+
el.db = {} as any;
|
|
480
|
+
|
|
481
|
+
el.showAccountForm("new");
|
|
482
|
+
const root = el.shadowRoot!;
|
|
483
|
+
|
|
484
|
+
// 1. Missing label/service/host
|
|
485
|
+
(
|
|
486
|
+
root.querySelector('[data-field="acct-label"]') as HTMLInputElement
|
|
487
|
+
).value = "";
|
|
488
|
+
await el.saveAccountForm();
|
|
489
|
+
expect(showError).toHaveBeenCalledWith(
|
|
490
|
+
"Label, Service, and Host Pattern are required.",
|
|
491
|
+
4000,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// Fill required base fields
|
|
495
|
+
(
|
|
496
|
+
root.querySelector('[data-field="acct-label"]') as HTMLInputElement
|
|
497
|
+
).value = "Test";
|
|
498
|
+
(
|
|
499
|
+
root.querySelector('[data-field="acct-service"]') as HTMLInputElement
|
|
500
|
+
).value = "Service";
|
|
501
|
+
(root.querySelector('[data-field="acct-host"]') as HTMLInputElement).value =
|
|
502
|
+
"example.com";
|
|
503
|
+
(
|
|
504
|
+
root.querySelector('[data-field="acct-auth-mode"]') as HTMLSelectElement
|
|
505
|
+
).value = "oauth";
|
|
506
|
+
|
|
507
|
+
// 2. OAuth mode missing client ID
|
|
508
|
+
(
|
|
509
|
+
root.querySelector(
|
|
510
|
+
'[data-field="acct-oauth-provider"]',
|
|
511
|
+
) as HTMLSelectElement
|
|
512
|
+
).value = "github";
|
|
513
|
+
(
|
|
514
|
+
root.querySelector(
|
|
515
|
+
'[data-field="acct-oauth-client-id"]',
|
|
516
|
+
) as HTMLInputElement
|
|
517
|
+
).value = "";
|
|
518
|
+
await el.saveAccountForm();
|
|
519
|
+
expect(showError).toHaveBeenCalledWith(
|
|
520
|
+
"OAuth client ID is required for OAuth mode.",
|
|
521
|
+
4000,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
document.body.removeChild(el);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("handles error when setConfig fails during saveAccountForm", async () => {
|
|
528
|
+
const el = new ShadowClawAccounts();
|
|
529
|
+
document.body.appendChild(el);
|
|
530
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
531
|
+
await el.render();
|
|
532
|
+
el.db = {} as any;
|
|
533
|
+
|
|
534
|
+
el.showAccountForm("new");
|
|
535
|
+
const root = el.shadowRoot!;
|
|
536
|
+
(
|
|
537
|
+
root.querySelector('[data-field="acct-label"]') as HTMLInputElement
|
|
538
|
+
).value = "Faulty";
|
|
539
|
+
(
|
|
540
|
+
root.querySelector('[data-field="acct-service"]') as HTMLInputElement
|
|
541
|
+
).value = "Service";
|
|
542
|
+
(root.querySelector('[data-field="acct-host"]') as HTMLInputElement).value =
|
|
543
|
+
"example.com";
|
|
544
|
+
|
|
545
|
+
(setConfig as jest.Mock).mockRejectedValueOnce(
|
|
546
|
+
new Error("Database write error"),
|
|
547
|
+
);
|
|
548
|
+
await el.saveAccountForm();
|
|
549
|
+
|
|
550
|
+
expect(showError).toHaveBeenCalledWith(
|
|
551
|
+
expect.stringContaining("Database write error"),
|
|
552
|
+
6000,
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
document.body.removeChild(el);
|
|
556
|
+
});
|
|
426
557
|
});
|
|
@@ -7,6 +7,9 @@ jest.unstable_mockModule("../../../config/config.js", () => ({
|
|
|
7
7
|
GIT_CORS_PROXY: "git_cors_proxy",
|
|
8
8
|
GIT_ACCOUNTS: "git_accounts",
|
|
9
9
|
GIT_DEFAULT_ACCOUNT: "git_default_account",
|
|
10
|
+
GIT_TOKEN: "git_token",
|
|
11
|
+
GIT_USERNAME: "git_username",
|
|
12
|
+
GIT_PASSWORD: "git_password",
|
|
10
13
|
},
|
|
11
14
|
getProviderTokenAuthScheme: (
|
|
12
15
|
providerId: string,
|
|
@@ -454,4 +457,131 @@ describe("shadow-claw-git", () => {
|
|
|
454
457
|
|
|
455
458
|
document.body.removeChild(el);
|
|
456
459
|
});
|
|
460
|
+
|
|
461
|
+
it("handles saveAccountForm validation, edit existing account, and oauth missing token", async () => {
|
|
462
|
+
const el = new ShadowClawGit();
|
|
463
|
+
document.body.appendChild(el);
|
|
464
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
465
|
+
await el.render();
|
|
466
|
+
el.db = {} as any;
|
|
467
|
+
|
|
468
|
+
// 1. Missing label and hostPattern
|
|
469
|
+
el.showAccountForm("new");
|
|
470
|
+
const slot = el.shadowRoot?.querySelector(
|
|
471
|
+
'[data-region="account-form-slot"]',
|
|
472
|
+
) as HTMLElement;
|
|
473
|
+
expect(slot).toBeDefined();
|
|
474
|
+
|
|
475
|
+
const labelInput = slot.querySelector(
|
|
476
|
+
'[data-field="acct-label"]',
|
|
477
|
+
) as HTMLInputElement;
|
|
478
|
+
const hostInput = slot.querySelector(
|
|
479
|
+
'[data-field="acct-host"]',
|
|
480
|
+
) as HTMLInputElement;
|
|
481
|
+
const authModeSelect = slot.querySelector(
|
|
482
|
+
'[data-field="acct-auth-mode"]',
|
|
483
|
+
) as HTMLSelectElement;
|
|
484
|
+
|
|
485
|
+
labelInput.value = "";
|
|
486
|
+
hostInput.value = "";
|
|
487
|
+
await el.saveAccountForm();
|
|
488
|
+
|
|
489
|
+
// 2. OAuth mode with no token
|
|
490
|
+
labelInput.value = "OAuth Acc";
|
|
491
|
+
hostInput.value = "github.com";
|
|
492
|
+
authModeSelect.value = "oauth";
|
|
493
|
+
await el.saveAccountForm();
|
|
494
|
+
|
|
495
|
+
el.accounts = [
|
|
496
|
+
{
|
|
497
|
+
id: "acc-edit",
|
|
498
|
+
label: "Old Label",
|
|
499
|
+
hostPattern: "github.com",
|
|
500
|
+
authMode: "token",
|
|
501
|
+
token: "old-token",
|
|
502
|
+
} as any,
|
|
503
|
+
];
|
|
504
|
+
el.showAccountForm("acc-edit");
|
|
505
|
+
|
|
506
|
+
const editLabelInput = slot.querySelector(
|
|
507
|
+
'[data-field="acct-label"]',
|
|
508
|
+
) as HTMLInputElement;
|
|
509
|
+
const editHostInput = slot.querySelector(
|
|
510
|
+
'[data-field="acct-host"]',
|
|
511
|
+
) as HTMLInputElement;
|
|
512
|
+
const editAuthModeSelect = slot.querySelector(
|
|
513
|
+
'[data-field="acct-auth-mode"]',
|
|
514
|
+
) as HTMLSelectElement;
|
|
515
|
+
const editTokenInput = slot.querySelector(
|
|
516
|
+
'[data-field="acct-token"]',
|
|
517
|
+
) as HTMLInputElement;
|
|
518
|
+
const editPassInput = slot.querySelector(
|
|
519
|
+
'[data-field="acct-password"]',
|
|
520
|
+
) as HTMLInputElement;
|
|
521
|
+
const editUserInput = slot.querySelector(
|
|
522
|
+
'[data-field="acct-username"]',
|
|
523
|
+
) as HTMLInputElement;
|
|
524
|
+
|
|
525
|
+
editLabelInput.value = "New Label";
|
|
526
|
+
editHostInput.value = "github.com/org";
|
|
527
|
+
editAuthModeSelect.value = "token";
|
|
528
|
+
editTokenInput.value = "new-token-raw";
|
|
529
|
+
editPassInput.value = "new-pass-raw";
|
|
530
|
+
editUserInput.value = "new-user";
|
|
531
|
+
|
|
532
|
+
await el.saveAccountForm();
|
|
533
|
+
|
|
534
|
+
const edited = el.accounts.find((a) => a.id === "acc-edit");
|
|
535
|
+
expect(edited).toBeDefined();
|
|
536
|
+
expect(edited?.label).toBe("New Label");
|
|
537
|
+
expect(edited?.hostPattern).toBe("github.com/org");
|
|
538
|
+
expect(edited?.username).toBe("new-user");
|
|
539
|
+
|
|
540
|
+
document.body.removeChild(el);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("migrates legacy account configuration when accounts list is empty", async () => {
|
|
544
|
+
const { getConfig } = await import("../../../db/getConfig.js");
|
|
545
|
+
const { setConfig } = await import("../../../db/setConfig.js");
|
|
546
|
+
|
|
547
|
+
(getConfig as any).mockImplementation(async (_db: any, key: string) => {
|
|
548
|
+
if (key === "git_token") return "legacy-enc-token";
|
|
549
|
+
if (key === "git_username") return "legacy-user";
|
|
550
|
+
if (key === "git_password") return "legacy-enc-pass";
|
|
551
|
+
return undefined;
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const el = new ShadowClawGit();
|
|
555
|
+
document.body.appendChild(el);
|
|
556
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
557
|
+
el.db = {} as any;
|
|
558
|
+
el.accounts = [];
|
|
559
|
+
|
|
560
|
+
await el.migrateLegacyAccount();
|
|
561
|
+
|
|
562
|
+
expect(el.accounts).toHaveLength(1);
|
|
563
|
+
expect(el.accounts[0].token).toBe("legacy-enc-token");
|
|
564
|
+
expect(el.accounts[0].username).toBe("legacy-user");
|
|
565
|
+
expect(el.defaultAccountId).toBe("legacy-migrated");
|
|
566
|
+
expect(setConfig).toHaveBeenCalledWith(el.db, "git_accounts", el.accounts);
|
|
567
|
+
|
|
568
|
+
document.body.removeChild(el);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("handles deleteAccount when removing the only account", async () => {
|
|
572
|
+
const el = new ShadowClawGit();
|
|
573
|
+
document.body.appendChild(el);
|
|
574
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
575
|
+
el.db = {} as any;
|
|
576
|
+
el.accounts = [
|
|
577
|
+
{ id: "single-acc", label: "Single", hostPattern: "gh.com" } as any,
|
|
578
|
+
];
|
|
579
|
+
el.defaultAccountId = "single-acc";
|
|
580
|
+
|
|
581
|
+
await el.deleteAccount("single-acc");
|
|
582
|
+
expect(el.accounts).toHaveLength(0);
|
|
583
|
+
expect(el.defaultAccountId).toBe("");
|
|
584
|
+
|
|
585
|
+
document.body.removeChild(el);
|
|
586
|
+
});
|
|
457
587
|
});
|
|
@@ -42,4 +42,16 @@ describe("resolveConnectionTestAuth", () => {
|
|
|
42
42
|
});
|
|
43
43
|
expect(result.error).toContain("Password/app password is missing");
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it("handles undefined pendingOauthAccessToken and passwordInput safely", () => {
|
|
47
|
+
const resOAuth: any = resolveConnectionTestAuth({
|
|
48
|
+
authMode: "oauth",
|
|
49
|
+
} as any);
|
|
50
|
+
expect(resOAuth.error).toContain("OAuth access token is missing");
|
|
51
|
+
|
|
52
|
+
const resPass: any = resolveConnectionTestAuth({
|
|
53
|
+
authMode: "password",
|
|
54
|
+
} as any);
|
|
55
|
+
expect(resPass.error).toContain("Password/app password is missing");
|
|
56
|
+
});
|
|
45
57
|
});
|