openpalm 0.10.2 → 0.11.0-beta.2
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/README.md +11 -19
- package/package.json +4 -2
- package/src/commands/addon.ts +5 -4
- package/src/commands/automations.ts +63 -0
- package/src/commands/install.ts +98 -280
- package/src/commands/logs.ts +1 -1
- package/src/commands/restart.ts +5 -4
- package/src/commands/rollback.ts +4 -3
- package/src/commands/scan.ts +66 -32
- package/src/commands/service.ts +5 -4
- package/src/commands/start.ts +5 -4
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +2 -4
- package/src/commands/uninstall.ts +3 -5
- package/src/commands/update.ts +19 -2
- package/src/commands/validate.ts +16 -34
- package/src/install-flow.test.ts +153 -154
- package/src/lib/admin-skills/index.test.ts +70 -0
- package/src/lib/admin-skills/index.ts +113 -0
- package/src/lib/browser.ts +20 -0
- package/src/lib/cli-compose.ts +2 -20
- package/src/lib/cli-state.ts +1 -1
- package/src/lib/docker.ts +8 -214
- package/src/lib/env.ts +12 -83
- package/src/lib/io.ts +130 -0
- package/src/lib/opencode-subprocess.ts +14 -6
- package/src/lib/paths.ts +2 -2
- package/src/lib/ui-server.ts +150 -0
- package/src/main.test.ts +76 -173
- package/src/main.ts +131 -7
- package/e2e/start-wizard-server.ts +0 -59
- package/src/commands/admin.ts +0 -43
- package/src/commands/install-services.test.ts +0 -13
- package/src/commands/install-services.ts +0 -9
- package/src/commands/upgrade.ts +0 -12
- package/src/lib/embedded-assets.ts +0 -115
- package/src/lib/varlock.ts +0 -126
- package/src/setup-wizard/index.html +0 -321
- package/src/setup-wizard/server-errors.test.ts +0 -418
- package/src/setup-wizard/server-integration.test.ts +0 -511
- package/src/setup-wizard/server.test.ts +0 -508
- package/src/setup-wizard/server.ts +0 -342
- package/src/setup-wizard/wizard-renderers.js +0 -1294
- package/src/setup-wizard/wizard-state.js +0 -346
- package/src/setup-wizard/wizard-validators.js +0 -81
- package/src/setup-wizard/wizard.css +0 -1611
- package/src/setup-wizard/wizard.js +0 -613
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { createSetupServer } from "./server.ts";
|
|
6
|
-
|
|
7
|
-
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
let tempBase: string;
|
|
10
|
-
let homeDir: string;
|
|
11
|
-
let configDir: string;
|
|
12
|
-
let vaultDir: string;
|
|
13
|
-
let dataDir: string;
|
|
14
|
-
let logsDir: string;
|
|
15
|
-
|
|
16
|
-
const savedEnv: Record<string, string | undefined> = {};
|
|
17
|
-
|
|
18
|
-
/** Seed minimal asset files so performSetup() can read them at OP_HOME. */
|
|
19
|
-
function seedRequiredAssets(homeDir: string): void {
|
|
20
|
-
mkdirSync(join(homeDir, "stack"), { recursive: true });
|
|
21
|
-
writeFileSync(join(homeDir, "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
|
|
22
|
-
mkdirSync(join(homeDir, "data", "assistant"), { recursive: true });
|
|
23
|
-
writeFileSync(join(homeDir, "data", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
|
|
24
|
-
writeFileSync(join(homeDir, "data", "assistant", "AGENTS.md"), "# Agents\n");
|
|
25
|
-
writeFileSync(join(homeDir, "vault", "user", "user.env.schema"), "OP_ADMIN_TOKEN=string\n");
|
|
26
|
-
writeFileSync(join(homeDir, "vault", "stack", "stack.env.schema"), "OP_IMAGE_TAG=string\n");
|
|
27
|
-
mkdirSync(join(homeDir, "config", "automations"), { recursive: true });
|
|
28
|
-
writeFileSync(join(homeDir, "config", "automations", "cleanup-logs.yml"), "name: cleanup-logs\nschedule: daily\n");
|
|
29
|
-
writeFileSync(join(homeDir, "config", "automations", "cleanup-data.yml"), "name: cleanup-data\nschedule: weekly\n");
|
|
30
|
-
writeFileSync(join(homeDir, "config", "automations", "validate-config.yml"), "name: validate-config\nschedule: hourly\n");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function makeSetupDirs(): void {
|
|
34
|
-
tempBase = mkdtempSync(join(tmpdir(), "openpalm-server-err-test-"));
|
|
35
|
-
homeDir = tempBase;
|
|
36
|
-
configDir = join(homeDir, "config");
|
|
37
|
-
vaultDir = join(homeDir, "vault");
|
|
38
|
-
dataDir = join(homeDir, "data");
|
|
39
|
-
logsDir = join(homeDir, "logs");
|
|
40
|
-
|
|
41
|
-
for (const dir of [
|
|
42
|
-
configDir,
|
|
43
|
-
join(configDir, "components"),
|
|
44
|
-
join(configDir, "capabilities"),
|
|
45
|
-
join(configDir, "assistant"),
|
|
46
|
-
join(configDir, "automations"),
|
|
47
|
-
vaultDir,
|
|
48
|
-
dataDir,
|
|
49
|
-
join(dataDir, "admin"),
|
|
50
|
-
join(dataDir, "memory"),
|
|
51
|
-
join(dataDir, "assistant"),
|
|
52
|
-
join(dataDir, "guardian"),
|
|
53
|
-
join(dataDir, "stash"),
|
|
54
|
-
join(dataDir, "workspace"),
|
|
55
|
-
logsDir,
|
|
56
|
-
join(logsDir, "opencode"),
|
|
57
|
-
]) {
|
|
58
|
-
mkdirSync(dir, { recursive: true });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
62
|
-
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
63
|
-
writeFileSync(
|
|
64
|
-
join(vaultDir, "stack", "stack.env"),
|
|
65
|
-
[
|
|
66
|
-
"OP_SETUP_COMPLETE=false",
|
|
67
|
-
"OP_ADMIN_TOKEN=",
|
|
68
|
-
"OPENAI_API_KEY=",
|
|
69
|
-
"OPENAI_BASE_URL=",
|
|
70
|
-
"ANTHROPIC_API_KEY=",
|
|
71
|
-
"GROQ_API_KEY=",
|
|
72
|
-
"MISTRAL_API_KEY=",
|
|
73
|
-
"GOOGLE_API_KEY=",
|
|
74
|
-
"OWNER_NAME=",
|
|
75
|
-
"OWNER_EMAIL=",
|
|
76
|
-
"",
|
|
77
|
-
].join("\n")
|
|
78
|
-
);
|
|
79
|
-
writeFileSync(
|
|
80
|
-
join(vaultDir, "user", "user.env"),
|
|
81
|
-
[
|
|
82
|
-
"# OpenPalm — User Extensions",
|
|
83
|
-
"# Add any custom environment variables here.",
|
|
84
|
-
"# These are loaded by compose alongside stack.env.",
|
|
85
|
-
"",
|
|
86
|
-
].join("\n")
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Seed asset files for performSetup() reads
|
|
90
|
-
seedRequiredAssets(homeDir);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Incrementing port counter to avoid conflicts
|
|
94
|
-
let nextPort = 19200;
|
|
95
|
-
|
|
96
|
-
describe("setup wizard server error scenarios", () => {
|
|
97
|
-
let serverPort: number;
|
|
98
|
-
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
makeSetupDirs();
|
|
101
|
-
|
|
102
|
-
savedEnv.OP_HOME = process.env.OP_HOME;
|
|
103
|
-
process.env.OP_HOME = homeDir;
|
|
104
|
-
|
|
105
|
-
serverPort = nextPort++;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
afterEach(() => {
|
|
109
|
-
process.env.OP_HOME = savedEnv.OP_HOME;
|
|
110
|
-
if (tempBase) rmSync(tempBase, { recursive: true, force: true });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── POST /api/setup/complete validation errors ────────────────────────
|
|
114
|
-
|
|
115
|
-
it("returns 400 when adminToken is missing", async () => {
|
|
116
|
-
const { stop } = createSetupServer(serverPort, {
|
|
117
|
-
configDir,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "Content-Type": "application/json" },
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
// no security.adminToken
|
|
126
|
-
version: 2,
|
|
127
|
-
capabilities: {
|
|
128
|
-
llm: "openai/gpt-4o",
|
|
129
|
-
embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
|
|
130
|
-
memory: { userId: "user1", customInstructions: "" },
|
|
131
|
-
},
|
|
132
|
-
connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
|
|
133
|
-
}),
|
|
134
|
-
});
|
|
135
|
-
expect(res.status).toBe(400);
|
|
136
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
137
|
-
expect(data.ok).toBe(false);
|
|
138
|
-
expect(data.error).toContain("security");
|
|
139
|
-
} finally {
|
|
140
|
-
stop();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("returns 400 when connections array is not an array", async () => {
|
|
145
|
-
const { stop } = createSetupServer(serverPort, {
|
|
146
|
-
configDir,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
|
|
151
|
-
method: "POST",
|
|
152
|
-
headers: { "Content-Type": "application/json" },
|
|
153
|
-
body: JSON.stringify({
|
|
154
|
-
version: 2,
|
|
155
|
-
capabilities: {
|
|
156
|
-
llm: "openai/gpt-4o",
|
|
157
|
-
embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
|
|
158
|
-
memory: { userId: "user1", customInstructions: "" },
|
|
159
|
-
},
|
|
160
|
-
security: { adminToken: "valid-token-12345" },
|
|
161
|
-
owner: { name: "Test User", email: "test@example.com" },
|
|
162
|
-
connections: "not-an-array",
|
|
163
|
-
}),
|
|
164
|
-
});
|
|
165
|
-
expect(res.status).toBe(400);
|
|
166
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
167
|
-
expect(data.ok).toBe(false);
|
|
168
|
-
expect(data.error).toContain("connections");
|
|
169
|
-
} finally {
|
|
170
|
-
stop();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("returns 400 when capabilities config is missing", async () => {
|
|
175
|
-
const { stop } = createSetupServer(serverPort, {
|
|
176
|
-
configDir,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
|
|
181
|
-
method: "POST",
|
|
182
|
-
headers: { "Content-Type": "application/json" },
|
|
183
|
-
body: JSON.stringify({
|
|
184
|
-
version: 2,
|
|
185
|
-
security: { adminToken: "valid-token-12345" },
|
|
186
|
-
owner: { name: "Test User", email: "test@example.com" },
|
|
187
|
-
connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
|
|
188
|
-
// no capabilities
|
|
189
|
-
}),
|
|
190
|
-
});
|
|
191
|
-
expect(res.status).toBe(400);
|
|
192
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
193
|
-
expect(data.ok).toBe(false);
|
|
194
|
-
expect(data.error).toContain("capabilities");
|
|
195
|
-
} finally {
|
|
196
|
-
stop();
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("succeeds even when capability provider does not match embeddings provider", async () => {
|
|
201
|
-
const { stop } = createSetupServer(serverPort, {
|
|
202
|
-
configDir,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
|
|
207
|
-
method: "POST",
|
|
208
|
-
headers: { "Content-Type": "application/json" },
|
|
209
|
-
body: JSON.stringify({
|
|
210
|
-
version: 2,
|
|
211
|
-
capabilities: {
|
|
212
|
-
llm: "fakeprovider/gpt-4o",
|
|
213
|
-
embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
|
|
214
|
-
memory: { userId: "user1", customInstructions: "" },
|
|
215
|
-
},
|
|
216
|
-
security: { adminToken: "valid-token-12345" },
|
|
217
|
-
owner: { name: "Test User", email: "test@example.com" },
|
|
218
|
-
connections: [{ id: "c1", name: "C1", provider: "fakeprovider", baseUrl: "", apiKey: "sk-test" }],
|
|
219
|
-
}),
|
|
220
|
-
});
|
|
221
|
-
// Connection-provider matching is no longer validated; setup succeeds
|
|
222
|
-
expect(res.status).toBe(200);
|
|
223
|
-
const data = (await res.json()) as { ok: boolean };
|
|
224
|
-
expect(data.ok).toBe(true);
|
|
225
|
-
} finally {
|
|
226
|
-
stop();
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("succeeds even when no capability matches LLM provider", async () => {
|
|
231
|
-
const { stop } = createSetupServer(serverPort, {
|
|
232
|
-
configDir,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`, {
|
|
237
|
-
method: "POST",
|
|
238
|
-
headers: { "Content-Type": "application/json" },
|
|
239
|
-
body: JSON.stringify({
|
|
240
|
-
version: 2,
|
|
241
|
-
capabilities: {
|
|
242
|
-
llm: "anthropic/claude-3-opus", // No anthropic connection provided
|
|
243
|
-
embeddings: { provider: "openai", model: "text-embedding-3-small", dims: 1536 },
|
|
244
|
-
memory: { userId: "user1", customInstructions: "" },
|
|
245
|
-
},
|
|
246
|
-
security: { adminToken: "valid-token-12345" },
|
|
247
|
-
owner: { name: "Test User", email: "test@example.com" },
|
|
248
|
-
connections: [{ id: "c1", name: "C1", provider: "openai", baseUrl: "", apiKey: "sk-test" }],
|
|
249
|
-
}),
|
|
250
|
-
});
|
|
251
|
-
// Provider matching is no longer validated; setup succeeds
|
|
252
|
-
expect(res.status).toBe(200);
|
|
253
|
-
const data = (await res.json()) as { ok: boolean };
|
|
254
|
-
expect(data.ok).toBe(true);
|
|
255
|
-
} finally {
|
|
256
|
-
stop();
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ── POST /api/setup/models/:provider errors ───────────────────────────
|
|
261
|
-
|
|
262
|
-
it("returns 400 for invalid JSON on model fetch", async () => {
|
|
263
|
-
const { stop } = createSetupServer(serverPort, {
|
|
264
|
-
configDir,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`, {
|
|
269
|
-
method: "POST",
|
|
270
|
-
headers: { "Content-Type": "application/json" },
|
|
271
|
-
body: "not-json",
|
|
272
|
-
});
|
|
273
|
-
expect(res.status).toBe(400);
|
|
274
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
275
|
-
expect(data.ok).toBe(false);
|
|
276
|
-
expect(data.error).toBe("invalid_json");
|
|
277
|
-
} finally {
|
|
278
|
-
stop();
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// lmstudio fetch to 127.0.0.1:1234 can take >5s to fail when nothing listens
|
|
283
|
-
it("returns empty model list when provider has no base URL", async () => {
|
|
284
|
-
const { stop } = createSetupServer(serverPort, {
|
|
285
|
-
configDir,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
// lmstudio without a base URL should return 502 with recoverable_error
|
|
290
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/models/lmstudio`, {
|
|
291
|
-
method: "POST",
|
|
292
|
-
headers: { "Content-Type": "application/json" },
|
|
293
|
-
body: JSON.stringify({ apiKey: "", baseUrl: "" }),
|
|
294
|
-
});
|
|
295
|
-
expect(res.status).toBe(502);
|
|
296
|
-
const data = (await res.json()) as { ok: boolean; models: string[]; status: string; reason: string };
|
|
297
|
-
expect(data.ok).toBe(false);
|
|
298
|
-
expect(Array.isArray(data.models)).toBe(true);
|
|
299
|
-
expect(data.status).toBe("recoverable_error");
|
|
300
|
-
} finally {
|
|
301
|
-
stop();
|
|
302
|
-
}
|
|
303
|
-
}, 15000);
|
|
304
|
-
|
|
305
|
-
it("returns recoverable error when model fetch hits unreachable server", async () => {
|
|
306
|
-
const { stop } = createSetupServer(serverPort, {
|
|
307
|
-
configDir,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
// Use a baseUrl that definitely will not connect — should return 502
|
|
312
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`, {
|
|
313
|
-
method: "POST",
|
|
314
|
-
headers: { "Content-Type": "application/json" },
|
|
315
|
-
body: JSON.stringify({ apiKey: "sk-fake", baseUrl: "http://127.0.0.1:1" }),
|
|
316
|
-
});
|
|
317
|
-
expect(res.status).toBe(502);
|
|
318
|
-
const data = (await res.json()) as {
|
|
319
|
-
ok: boolean;
|
|
320
|
-
models: string[];
|
|
321
|
-
status: string;
|
|
322
|
-
reason: string;
|
|
323
|
-
error?: string;
|
|
324
|
-
};
|
|
325
|
-
expect(data.ok).toBe(false);
|
|
326
|
-
expect(data.models).toEqual([]);
|
|
327
|
-
expect(data.status).toBe("recoverable_error");
|
|
328
|
-
expect(data.reason).toBe("network");
|
|
329
|
-
expect(data.error).toBeDefined();
|
|
330
|
-
} finally {
|
|
331
|
-
stop();
|
|
332
|
-
}
|
|
333
|
-
}, 10000);
|
|
334
|
-
|
|
335
|
-
it("returns static model list for anthropic (no network call needed)", async () => {
|
|
336
|
-
const { stop } = createSetupServer(serverPort, {
|
|
337
|
-
configDir,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/models/anthropic`, {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/json" },
|
|
344
|
-
body: JSON.stringify({ apiKey: "sk-ant-test", baseUrl: "" }),
|
|
345
|
-
});
|
|
346
|
-
expect(res.status).toBe(200);
|
|
347
|
-
const data = (await res.json()) as { ok: boolean; models: string[]; status: string; reason: string };
|
|
348
|
-
expect(data.ok).toBe(true);
|
|
349
|
-
expect(data.models.length).toBeGreaterThan(0);
|
|
350
|
-
expect(data.status).toBe("ok");
|
|
351
|
-
expect(data.reason).toBe("provider_static");
|
|
352
|
-
} finally {
|
|
353
|
-
stop();
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// ── Deploy status with error state ────────────────────────────────────
|
|
358
|
-
|
|
359
|
-
it("reports deploy error via deploy-status endpoint", async () => {
|
|
360
|
-
const { stop, updateDeployStatus, setDeployError } = createSetupServer(serverPort, {
|
|
361
|
-
configDir,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
updateDeployStatus([
|
|
366
|
-
{ service: "assistant", status: "running", label: "Assistant" },
|
|
367
|
-
{ service: "memory", status: "error", label: "Failed to pull" },
|
|
368
|
-
]);
|
|
369
|
-
setDeployError("memory container failed to start");
|
|
370
|
-
|
|
371
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/deploy-status`);
|
|
372
|
-
expect(res.status).toBe(200);
|
|
373
|
-
const data = (await res.json()) as {
|
|
374
|
-
ok: boolean;
|
|
375
|
-
deployStatus: Array<{ service: string; status: string; label: string }>;
|
|
376
|
-
deployError: string | null;
|
|
377
|
-
};
|
|
378
|
-
expect(data.ok).toBe(true);
|
|
379
|
-
expect(data.deployError).toBe("memory container failed to start");
|
|
380
|
-
const memEntry = data.deployStatus.find((e) => e.service === "memory");
|
|
381
|
-
expect(memEntry?.status).toBe("error");
|
|
382
|
-
} finally {
|
|
383
|
-
stop();
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// ── HTTP method mismatches ────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
it("returns 404 for GET on model endpoint (requires POST)", async () => {
|
|
390
|
-
const { stop } = createSetupServer(serverPort, {
|
|
391
|
-
configDir,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/models/openai`);
|
|
396
|
-
expect(res.status).toBe(404);
|
|
397
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
398
|
-
expect(data.ok).toBe(false);
|
|
399
|
-
} finally {
|
|
400
|
-
stop();
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("returns 404 for GET on /api/setup/complete (requires POST)", async () => {
|
|
405
|
-
const { stop } = createSetupServer(serverPort, {
|
|
406
|
-
configDir,
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const res = await fetch(`http://localhost:${serverPort}/api/setup/complete`);
|
|
411
|
-
expect(res.status).toBe(404);
|
|
412
|
-
const data = (await res.json()) as { ok: boolean; error: string };
|
|
413
|
-
expect(data.ok).toBe(false);
|
|
414
|
-
} finally {
|
|
415
|
-
stop();
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
});
|