@mycadet/assistant 0.12.11 → 0.13.0
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/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- package/src/types/tar-stream.d.ts +0 -66
|
@@ -1,1176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP-layer integration tests for POST /v1/migrations/import.
|
|
3
|
-
*
|
|
4
|
-
* Tests cover:
|
|
5
|
-
* - Success: valid .vbundle writes files to disk and returns import report
|
|
6
|
-
* - Validation failures: invalid bundle returns success: false with errors
|
|
7
|
-
* - Request errors: empty body, invalid multipart
|
|
8
|
-
* - Backup creation: existing files are backed up before overwriting
|
|
9
|
-
* - Post-write integrity: written files match expected checksums
|
|
10
|
-
* - Skipped files: unknown archive paths are reported as skipped
|
|
11
|
-
* - Auth: route policy enforcement (settings.write scope required)
|
|
12
|
-
* - Integration: existing routes are unaffected by the new endpoint
|
|
13
|
-
*/
|
|
14
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
15
|
-
import {
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
readdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
unlinkSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from "node:fs";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import { gzipSync } from "node:zlib";
|
|
26
|
-
import {
|
|
27
|
-
afterEach,
|
|
28
|
-
beforeAll,
|
|
29
|
-
beforeEach,
|
|
30
|
-
describe,
|
|
31
|
-
expect,
|
|
32
|
-
test,
|
|
33
|
-
} from "bun:test";
|
|
34
|
-
|
|
35
|
-
/** Convert a Uint8Array to an ArrayBuffer for BodyInit compatibility. */
|
|
36
|
-
function toArrayBuffer(data: Uint8Array): ArrayBuffer {
|
|
37
|
-
return data.buffer.slice(
|
|
38
|
-
data.byteOffset,
|
|
39
|
-
data.byteOffset + data.byteLength,
|
|
40
|
-
) as ArrayBuffer;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const testDir = process.env.CADET_WORKSPACE_DIR!;
|
|
44
|
-
const testDbDir = join(testDir, "data", "db");
|
|
45
|
-
const testDbPath = join(testDbDir, "assistant.db");
|
|
46
|
-
const testConfigPath = join(testDir, "config.json");
|
|
47
|
-
|
|
48
|
-
process.env.DISABLE_HTTP_AUTH = "true";
|
|
49
|
-
|
|
50
|
-
import { DefaultPathResolver } from "../runtime/migrations/vbundle-import-analyzer.js";
|
|
51
|
-
import { commitImport } from "../runtime/migrations/vbundle-importer.js";
|
|
52
|
-
import { handleMigrationImport } from "../runtime/routes/migration-routes.js";
|
|
53
|
-
import { callHandler } from "./helpers/call-route-handler.js";
|
|
54
|
-
|
|
55
|
-
// Test fixture data
|
|
56
|
-
const EXISTING_DB_DATA = new Uint8Array([
|
|
57
|
-
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74,
|
|
58
|
-
0x20, 0x33, 0x00,
|
|
59
|
-
]);
|
|
60
|
-
const EXISTING_CONFIG = { provider: "anthropic", model: "test-model" };
|
|
61
|
-
|
|
62
|
-
beforeAll(() => {
|
|
63
|
-
mkdirSync(testDbDir, { recursive: true });
|
|
64
|
-
writeFileSync(testDbPath, EXISTING_DB_DATA);
|
|
65
|
-
writeFileSync(testConfigPath, JSON.stringify(EXISTING_CONFIG, null, 2));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Restore test files before each test so mutations from previous tests
|
|
69
|
-
// do not leak across test cases.
|
|
70
|
-
beforeEach(() => {
|
|
71
|
-
mkdirSync(testDbDir, { recursive: true });
|
|
72
|
-
writeFileSync(testDbPath, EXISTING_DB_DATA);
|
|
73
|
-
writeFileSync(testConfigPath, JSON.stringify(EXISTING_CONFIG, null, 2));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Clean up backup files after each test
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
try {
|
|
79
|
-
const entries = readdirSync(testDbDir);
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
if (entry.includes(".backup-")) {
|
|
82
|
-
unlinkSync(join(testDbDir, entry));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const parentEntries = readdirSync(testDir);
|
|
86
|
-
for (const entry of parentEntries) {
|
|
87
|
-
if (entry.includes(".backup-")) {
|
|
88
|
-
unlinkSync(join(testDir, entry));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
/* best effort */
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Tar archive builder helpers (mirrors validate/preflight tests)
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
const BLOCK_SIZE = 512;
|
|
101
|
-
|
|
102
|
-
function padToBlock(data: Uint8Array): Uint8Array {
|
|
103
|
-
const remainder = data.length % BLOCK_SIZE;
|
|
104
|
-
if (remainder === 0) {
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
const padded = new Uint8Array(data.length + (BLOCK_SIZE - remainder));
|
|
108
|
-
padded.set(data);
|
|
109
|
-
return padded;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function writeOctal(
|
|
113
|
-
buf: Uint8Array,
|
|
114
|
-
offset: number,
|
|
115
|
-
length: number,
|
|
116
|
-
value: number,
|
|
117
|
-
): void {
|
|
118
|
-
const str = value.toString(8).padStart(length - 1, "0");
|
|
119
|
-
for (let i = 0; i < str.length; i++) {
|
|
120
|
-
buf[offset + i] = str.charCodeAt(i);
|
|
121
|
-
}
|
|
122
|
-
buf[offset + length - 1] = 0;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function computeHeaderChecksum(header: Uint8Array): number {
|
|
126
|
-
let sum = 0;
|
|
127
|
-
for (let i = 0; i < 512; i++) {
|
|
128
|
-
if (i >= 148 && i < 156) {
|
|
129
|
-
sum += 0x20;
|
|
130
|
-
} else {
|
|
131
|
-
sum += header[i];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return sum;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function createTarEntry(name: string, data: Uint8Array): Uint8Array {
|
|
138
|
-
const header = new Uint8Array(BLOCK_SIZE);
|
|
139
|
-
const encoder = new TextEncoder();
|
|
140
|
-
|
|
141
|
-
const nameBytes = encoder.encode(name);
|
|
142
|
-
header.set(nameBytes.subarray(0, 100), 0);
|
|
143
|
-
|
|
144
|
-
writeOctal(header, 100, 8, 0o644);
|
|
145
|
-
writeOctal(header, 108, 8, 0);
|
|
146
|
-
writeOctal(header, 116, 8, 0);
|
|
147
|
-
writeOctal(header, 124, 12, data.length);
|
|
148
|
-
writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
|
|
149
|
-
header[156] = "0".charCodeAt(0);
|
|
150
|
-
|
|
151
|
-
const magic = encoder.encode("ustar\0");
|
|
152
|
-
header.set(magic, 257);
|
|
153
|
-
header[263] = "0".charCodeAt(0);
|
|
154
|
-
header[264] = "0".charCodeAt(0);
|
|
155
|
-
|
|
156
|
-
const checksum = computeHeaderChecksum(header);
|
|
157
|
-
writeOctal(header, 148, 7, checksum);
|
|
158
|
-
header[155] = 0x20;
|
|
159
|
-
|
|
160
|
-
const paddedData = padToBlock(data);
|
|
161
|
-
const result = new Uint8Array(header.length + paddedData.length);
|
|
162
|
-
result.set(header, 0);
|
|
163
|
-
result.set(paddedData, header.length);
|
|
164
|
-
return result;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function createTarArchive(
|
|
168
|
-
entries: Array<{ name: string; data: Uint8Array }>,
|
|
169
|
-
): Uint8Array {
|
|
170
|
-
const parts: Uint8Array[] = [];
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
parts.push(createTarEntry(entry.name, entry.data));
|
|
173
|
-
}
|
|
174
|
-
parts.push(new Uint8Array(BLOCK_SIZE * 2));
|
|
175
|
-
|
|
176
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
177
|
-
const result = new Uint8Array(totalLength);
|
|
178
|
-
let offset = 0;
|
|
179
|
-
for (const part of parts) {
|
|
180
|
-
result.set(part, offset);
|
|
181
|
-
offset += part.length;
|
|
182
|
-
}
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function sha256Hex(data: Uint8Array | string): string {
|
|
187
|
-
return createHash("sha256").update(data).digest("hex");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function canonicalizeJson(obj: unknown): string {
|
|
191
|
-
return JSON.stringify(obj, (_key, value) => {
|
|
192
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
193
|
-
const sorted: Record<string, unknown> = {};
|
|
194
|
-
for (const k of Object.keys(value as Record<string, unknown>).sort()) {
|
|
195
|
-
sorted[k] = (value as Record<string, unknown>)[k];
|
|
196
|
-
}
|
|
197
|
-
return sorted;
|
|
198
|
-
}
|
|
199
|
-
return value;
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
interface VBundleFile {
|
|
204
|
-
path: string;
|
|
205
|
-
data: Uint8Array;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Build a v1-shape vbundle archive for HTTP/import tests.
|
|
210
|
-
*
|
|
211
|
-
* Mirrors the v1 ten-field manifest that `buildVBundle()` produces, so the
|
|
212
|
-
* fixtures here pass the same `ManifestSchema` zod validation the real
|
|
213
|
-
* importer applies. The manifest's `checksum` is computed against the
|
|
214
|
-
* canonical JSON with `checksum` set to "" (matches `computeManifestChecksum`
|
|
215
|
-
* in vbundle-validator.ts).
|
|
216
|
-
*
|
|
217
|
-
* Adds a synthetic `data/db/assistant.db` entry to `contents` when the caller
|
|
218
|
-
* doesn't supply one, satisfying the schema's `.refine()` constraint that
|
|
219
|
-
* every bundle must reference an assistant.db file.
|
|
220
|
-
*/
|
|
221
|
-
function createValidVBundle(
|
|
222
|
-
files?: VBundleFile[],
|
|
223
|
-
overrides?: Partial<{
|
|
224
|
-
bundle_id: string;
|
|
225
|
-
origin_mode: "managed" | "self-hosted-remote" | "self-hosted-local";
|
|
226
|
-
secrets_redacted: boolean;
|
|
227
|
-
min_runtime_version: string;
|
|
228
|
-
max_runtime_version: string | null;
|
|
229
|
-
}>,
|
|
230
|
-
): Uint8Array {
|
|
231
|
-
const dbData = new Uint8Array([0x53, 0x51, 0x4c, 0x69, 0x74, 0x65]);
|
|
232
|
-
const bundleFiles = files ?? [{ path: "data/db/assistant.db", data: dbData }];
|
|
233
|
-
|
|
234
|
-
// v1 schema requires contents to include data/db/assistant.db (legacy) or
|
|
235
|
-
// workspace/data/db/assistant.db (current). If the caller's files don't
|
|
236
|
-
// satisfy that refine, inject a synthetic legacy-path db entry — this
|
|
237
|
-
// matches the pattern used in vbundle-streaming-importer.test.ts.
|
|
238
|
-
const hasDbEntry = bundleFiles.some(
|
|
239
|
-
(f) =>
|
|
240
|
-
f.path === "data/db/assistant.db" ||
|
|
241
|
-
f.path === "workspace/data/db/assistant.db",
|
|
242
|
-
);
|
|
243
|
-
const allFiles: VBundleFile[] = hasDbEntry
|
|
244
|
-
? bundleFiles
|
|
245
|
-
: [
|
|
246
|
-
{ path: "data/db/assistant.db", data: new Uint8Array() },
|
|
247
|
-
...bundleFiles,
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
const contents = allFiles.map((f) => ({
|
|
251
|
-
path: f.path,
|
|
252
|
-
sha256: sha256Hex(f.data),
|
|
253
|
-
size_bytes: f.data.length,
|
|
254
|
-
}));
|
|
255
|
-
|
|
256
|
-
const manifestWithoutChecksum = {
|
|
257
|
-
schema_version: 1 as const,
|
|
258
|
-
bundle_id: overrides?.bundle_id ?? randomUUID(),
|
|
259
|
-
created_at: new Date().toISOString(),
|
|
260
|
-
assistant: {
|
|
261
|
-
id: "self",
|
|
262
|
-
name: "Test",
|
|
263
|
-
runtime_version: "0.0.0-test",
|
|
264
|
-
},
|
|
265
|
-
origin: {
|
|
266
|
-
mode: overrides?.origin_mode ?? "self-hosted-local",
|
|
267
|
-
},
|
|
268
|
-
compatibility: {
|
|
269
|
-
min_runtime_version: overrides?.min_runtime_version ?? "0.0.0-test",
|
|
270
|
-
max_runtime_version:
|
|
271
|
-
overrides?.max_runtime_version === undefined
|
|
272
|
-
? null
|
|
273
|
-
: overrides.max_runtime_version,
|
|
274
|
-
},
|
|
275
|
-
contents,
|
|
276
|
-
checksum: "",
|
|
277
|
-
secrets_redacted: overrides?.secrets_redacted ?? false,
|
|
278
|
-
export_options: {
|
|
279
|
-
include_logs: false,
|
|
280
|
-
include_browser_state: false,
|
|
281
|
-
include_memory_vectors: false,
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const checksum = sha256Hex(canonicalizeJson(manifestWithoutChecksum));
|
|
286
|
-
const manifest = { ...manifestWithoutChecksum, checksum };
|
|
287
|
-
const manifestData = new TextEncoder().encode(JSON.stringify(manifest));
|
|
288
|
-
|
|
289
|
-
const tarEntries = [
|
|
290
|
-
{ name: "manifest.json", data: manifestData },
|
|
291
|
-
...allFiles.map((f) => ({ name: f.path, data: f.data })),
|
|
292
|
-
];
|
|
293
|
-
|
|
294
|
-
const tar = createTarArchive(tarEntries);
|
|
295
|
-
return gzipSync(tar);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// Import commit report types (for type-safe assertions)
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
|
|
302
|
-
interface ImportCommitResponse {
|
|
303
|
-
success: boolean;
|
|
304
|
-
summary: {
|
|
305
|
-
total_files: number;
|
|
306
|
-
files_created: number;
|
|
307
|
-
files_overwritten: number;
|
|
308
|
-
files_skipped: number;
|
|
309
|
-
backups_created: number;
|
|
310
|
-
};
|
|
311
|
-
files: Array<{
|
|
312
|
-
path: string;
|
|
313
|
-
disk_path: string;
|
|
314
|
-
action: string;
|
|
315
|
-
size: number;
|
|
316
|
-
sha256: string;
|
|
317
|
-
backup_path: string | null;
|
|
318
|
-
}>;
|
|
319
|
-
manifest: Record<string, unknown>;
|
|
320
|
-
warnings: string[];
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
interface ImportValidationFailureResponse {
|
|
324
|
-
success: boolean;
|
|
325
|
-
reason: string;
|
|
326
|
-
errors: Array<{ code: string; message: string; path?: string }>;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
// HTTP handler tests: success cases
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
|
|
333
|
-
describe("handleMigrationImport", () => {
|
|
334
|
-
test("POST with valid vbundle returns 200 with import report", async () => {
|
|
335
|
-
const newDbData = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
336
|
-
const vbundle = createValidVBundle([
|
|
337
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
338
|
-
]);
|
|
339
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
340
|
-
method: "POST",
|
|
341
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
342
|
-
body: toArrayBuffer(vbundle),
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
346
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
347
|
-
|
|
348
|
-
expect(res.status).toBe(200);
|
|
349
|
-
expect(body.success).toBe(true);
|
|
350
|
-
expect(body.summary).toBeDefined();
|
|
351
|
-
expect(body.summary.total_files).toBeGreaterThan(0);
|
|
352
|
-
expect(body.files).toBeDefined();
|
|
353
|
-
expect(body.files.length).toBeGreaterThan(0);
|
|
354
|
-
expect(body.manifest).toBeDefined();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("writes bundle data to disk", async () => {
|
|
358
|
-
const newDbData = new Uint8Array([0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad]);
|
|
359
|
-
const vbundle = createValidVBundle([
|
|
360
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
361
|
-
]);
|
|
362
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
363
|
-
method: "POST",
|
|
364
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
365
|
-
body: toArrayBuffer(vbundle),
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
await callHandler(handleMigrationImport, req);
|
|
369
|
-
|
|
370
|
-
// Verify the file was actually written to disk
|
|
371
|
-
const writtenData = new Uint8Array(readFileSync(testDbPath));
|
|
372
|
-
expect(writtenData).toEqual(newDbData);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("workspace is cleared before restore — preserved dirs are overwritten", async () => {
|
|
376
|
-
// New-format bundles (workspace/ prefix) trigger workspace clearing, but
|
|
377
|
-
// data/db/ is preserved during clearing to avoid destroying the database
|
|
378
|
-
// if the import fails partway. The DB file is overwritten (with backup).
|
|
379
|
-
const newDbData = new Uint8Array([0x01, 0x02, 0x03]);
|
|
380
|
-
const vbundle = createValidVBundle([
|
|
381
|
-
{ path: "workspace/data/db/assistant.db", data: newDbData },
|
|
382
|
-
]);
|
|
383
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
384
|
-
method: "POST",
|
|
385
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
386
|
-
body: toArrayBuffer(vbundle),
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
390
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
391
|
-
|
|
392
|
-
expect(body.success).toBe(true);
|
|
393
|
-
expect(body.summary.backups_created).toBe(1);
|
|
394
|
-
|
|
395
|
-
const dbFile = body.files.find(
|
|
396
|
-
(f) => f.path === "workspace/data/db/assistant.db",
|
|
397
|
-
);
|
|
398
|
-
expect(dbFile).toBeDefined();
|
|
399
|
-
expect(dbFile!.action).toBe("overwritten");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("workspace clear: preserved dirs overwritten, cleared dirs created", async () => {
|
|
403
|
-
// data/db/ is preserved during workspace clearing → DB is "overwritten".
|
|
404
|
-
// config.json lives at the workspace root which IS cleared → "created".
|
|
405
|
-
const newDbData = new Uint8Array([0xaa, 0xbb]);
|
|
406
|
-
const newConfigData = new TextEncoder().encode('{"provider":"openai"}');
|
|
407
|
-
const vbundle = createValidVBundle([
|
|
408
|
-
{ path: "workspace/data/db/assistant.db", data: newDbData },
|
|
409
|
-
{ path: "workspace/config.json", data: newConfigData },
|
|
410
|
-
]);
|
|
411
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
412
|
-
method: "POST",
|
|
413
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
414
|
-
body: toArrayBuffer(vbundle),
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
418
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
419
|
-
|
|
420
|
-
expect(body.success).toBe(true);
|
|
421
|
-
|
|
422
|
-
const dbFile = body.files.find(
|
|
423
|
-
(f) => f.path === "workspace/data/db/assistant.db",
|
|
424
|
-
);
|
|
425
|
-
const configFile = body.files.find(
|
|
426
|
-
(f) => f.path === "workspace/config.json",
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
// data/db/ preserved during clearing → overwritten; config.json cleared → created
|
|
430
|
-
expect(dbFile!.action).toBe("overwritten");
|
|
431
|
-
expect(configFile!.action).toBe("created");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test("summary counts match file details", async () => {
|
|
435
|
-
const newDbData = new Uint8Array([0xdd, 0xee, 0xff]);
|
|
436
|
-
const newConfigData = new TextEncoder().encode('{"provider":"test"}');
|
|
437
|
-
const vbundle = createValidVBundle([
|
|
438
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
439
|
-
{ path: "config/settings.json", data: newConfigData },
|
|
440
|
-
]);
|
|
441
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
442
|
-
method: "POST",
|
|
443
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
444
|
-
body: toArrayBuffer(vbundle),
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
448
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
449
|
-
|
|
450
|
-
expect(body.success).toBe(true);
|
|
451
|
-
expect(body.summary.total_files).toBe(body.files.length);
|
|
452
|
-
|
|
453
|
-
const created = body.files.filter((f) => f.action === "created").length;
|
|
454
|
-
const overwritten = body.files.filter(
|
|
455
|
-
(f) => f.action === "overwritten",
|
|
456
|
-
).length;
|
|
457
|
-
const skipped = body.files.filter((f) => f.action === "skipped").length;
|
|
458
|
-
|
|
459
|
-
expect(body.summary.files_created).toBe(created);
|
|
460
|
-
expect(body.summary.files_overwritten).toBe(overwritten);
|
|
461
|
-
expect(body.summary.files_skipped).toBe(skipped);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
test("includes manifest in response", async () => {
|
|
465
|
-
const fixedBundleId = "11111111-2222-4333-8444-555555555555";
|
|
466
|
-
const vbundle = createValidVBundle(undefined, {
|
|
467
|
-
bundle_id: fixedBundleId,
|
|
468
|
-
});
|
|
469
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
470
|
-
method: "POST",
|
|
471
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
472
|
-
body: toArrayBuffer(vbundle),
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
476
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
477
|
-
|
|
478
|
-
expect(body.manifest).toBeDefined();
|
|
479
|
-
expect(body.manifest.schema_version).toBe(1);
|
|
480
|
-
expect(body.manifest.bundle_id).toBe(fixedBundleId);
|
|
481
|
-
expect(body.manifest.assistant).toBeDefined();
|
|
482
|
-
expect(body.manifest.origin).toEqual({ mode: "self-hosted-local" });
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
test("POST with multipart form data works", async () => {
|
|
486
|
-
const vbundle = createValidVBundle();
|
|
487
|
-
const formData = new FormData();
|
|
488
|
-
formData.append("file", new Blob([toArrayBuffer(vbundle)]), "test.vbundle");
|
|
489
|
-
|
|
490
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
491
|
-
method: "POST",
|
|
492
|
-
body: formData,
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
496
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
497
|
-
|
|
498
|
-
expect(res.status).toBe(200);
|
|
499
|
-
expect(body.success).toBe(true);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test("skips files with unknown archive paths", async () => {
|
|
503
|
-
const dbData = new Uint8Array([0x53, 0x51, 0x4c, 0x69, 0x74, 0x65]);
|
|
504
|
-
const extraData = new Uint8Array([0x01, 0x02]);
|
|
505
|
-
const vbundle = createValidVBundle([
|
|
506
|
-
{ path: "data/db/assistant.db", data: dbData },
|
|
507
|
-
{ path: "unknown/extra-file.bin", data: extraData },
|
|
508
|
-
]);
|
|
509
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
510
|
-
method: "POST",
|
|
511
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
512
|
-
body: toArrayBuffer(vbundle),
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
516
|
-
const body = (await res.json()) as ImportCommitResponse;
|
|
517
|
-
|
|
518
|
-
expect(body.success).toBe(true);
|
|
519
|
-
expect(body.summary.files_skipped).toBe(1);
|
|
520
|
-
|
|
521
|
-
const skippedFile = body.files.find(
|
|
522
|
-
(f) => f.path === "unknown/extra-file.bin",
|
|
523
|
-
);
|
|
524
|
-
expect(skippedFile).toBeDefined();
|
|
525
|
-
expect(skippedFile!.action).toBe("skipped");
|
|
526
|
-
expect(body.warnings.length).toBeGreaterThan(0);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
// HTTP handler tests: validation failure cases
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
|
|
534
|
-
describe("handleMigrationImport — validation failures", () => {
|
|
535
|
-
test("invalid gzip returns success: false with validation errors", async () => {
|
|
536
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
537
|
-
method: "POST",
|
|
538
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
539
|
-
body: toArrayBuffer(new Uint8Array([0xde, 0xad, 0xbe, 0xef])),
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
543
|
-
const body = (await res.json()) as ImportValidationFailureResponse;
|
|
544
|
-
|
|
545
|
-
expect(res.status).toBe(200);
|
|
546
|
-
expect(body.success).toBe(false);
|
|
547
|
-
expect(body.reason).toBe("validation_failed");
|
|
548
|
-
expect(body.errors.length).toBeGreaterThan(0);
|
|
549
|
-
expect(body.errors[0].code).toBe("INVALID_GZIP");
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test("missing manifest returns success: false", async () => {
|
|
553
|
-
const dbData = new Uint8Array([0x53, 0x51, 0x4c, 0x69, 0x74, 0x65]);
|
|
554
|
-
const tar = createTarArchive([
|
|
555
|
-
{ name: "data/db/assistant.db", data: dbData },
|
|
556
|
-
]);
|
|
557
|
-
const vbundle = gzipSync(tar);
|
|
558
|
-
|
|
559
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
560
|
-
method: "POST",
|
|
561
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
562
|
-
body: toArrayBuffer(vbundle),
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
566
|
-
const body = (await res.json()) as ImportValidationFailureResponse;
|
|
567
|
-
|
|
568
|
-
expect(res.status).toBe(200);
|
|
569
|
-
expect(body.success).toBe(false);
|
|
570
|
-
expect(body.reason).toBe("validation_failed");
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
test("empty body returns 400", async () => {
|
|
574
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
575
|
-
method: "POST",
|
|
576
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
577
|
-
body: toArrayBuffer(new Uint8Array(0)),
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
581
|
-
const body = (await res.json()) as {
|
|
582
|
-
error: { code: string; message: string };
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
expect(res.status).toBe(400);
|
|
586
|
-
expect(body.error.code).toBe("BAD_REQUEST");
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test("multipart without file field returns 400", async () => {
|
|
590
|
-
const formData = new FormData();
|
|
591
|
-
formData.append("notfile", "some text");
|
|
592
|
-
|
|
593
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
594
|
-
method: "POST",
|
|
595
|
-
body: formData,
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
599
|
-
const body = (await res.json()) as { error: { code: string } };
|
|
600
|
-
|
|
601
|
-
expect(res.status).toBe(400);
|
|
602
|
-
expect(body.error.code).toBe("BAD_REQUEST");
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("does not modify disk when validation fails", async () => {
|
|
606
|
-
// Save original data
|
|
607
|
-
const originalDb = new Uint8Array(readFileSync(testDbPath));
|
|
608
|
-
const originalConfig = readFileSync(testConfigPath, "utf8");
|
|
609
|
-
|
|
610
|
-
// Send invalid bundle
|
|
611
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
612
|
-
method: "POST",
|
|
613
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
614
|
-
body: toArrayBuffer(new Uint8Array([0xde, 0xad, 0xbe, 0xef])),
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
await callHandler(handleMigrationImport, req);
|
|
618
|
-
|
|
619
|
-
// Verify disk was not modified
|
|
620
|
-
const currentDb = new Uint8Array(readFileSync(testDbPath));
|
|
621
|
-
const currentConfig = readFileSync(testConfigPath, "utf8");
|
|
622
|
-
|
|
623
|
-
expect(currentDb).toEqual(originalDb);
|
|
624
|
-
expect(currentConfig).toBe(originalConfig);
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// ---------------------------------------------------------------------------
|
|
629
|
-
// HTTP handler tests: version_incompatible (Codex P2 regression)
|
|
630
|
-
//
|
|
631
|
-
// Pin: a bundle whose min_runtime_version exceeds APP_VERSION must surface as
|
|
632
|
-
// a 4xx user-actionable response (422 Unprocessable Entity), not a 500
|
|
633
|
-
// InternalError. Body mirrors the cloud's PR #5470 response shape so
|
|
634
|
-
// clients can render the same UX regardless of which gate (platform or
|
|
635
|
-
// runtime) rejected the bundle.
|
|
636
|
-
// ---------------------------------------------------------------------------
|
|
637
|
-
|
|
638
|
-
describe("handleMigrationImport — version_incompatible", () => {
|
|
639
|
-
test("incompatible bundle returns 422 with structured body, not 500", async () => {
|
|
640
|
-
const vbundle = createValidVBundle(undefined, {
|
|
641
|
-
min_runtime_version: "99.0.0",
|
|
642
|
-
});
|
|
643
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
644
|
-
method: "POST",
|
|
645
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
646
|
-
body: toArrayBuffer(vbundle),
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
650
|
-
const body = (await res.json()) as {
|
|
651
|
-
error: {
|
|
652
|
-
code: string;
|
|
653
|
-
message: string;
|
|
654
|
-
details?: {
|
|
655
|
-
reason: string;
|
|
656
|
-
bundle_compat: {
|
|
657
|
-
min_runtime_version: string;
|
|
658
|
-
max_runtime_version: string | null;
|
|
659
|
-
};
|
|
660
|
-
runtime_version: string;
|
|
661
|
-
};
|
|
662
|
-
};
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
expect(res.status).toBe(422);
|
|
666
|
-
expect(body.error.code).toBe("UNPROCESSABLE_ENTITY");
|
|
667
|
-
expect(body.error.message).toContain("99.0.0");
|
|
668
|
-
expect(body.error.details).toBeDefined();
|
|
669
|
-
expect(body.error.details!.reason).toBe("version_incompatible");
|
|
670
|
-
expect(body.error.details!.bundle_compat.min_runtime_version).toBe(
|
|
671
|
-
"99.0.0",
|
|
672
|
-
);
|
|
673
|
-
expect(body.error.details!.bundle_compat.max_runtime_version).toBeNull();
|
|
674
|
-
expect(typeof body.error.details!.runtime_version).toBe("string");
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
test("incompatible bundle does not modify disk", async () => {
|
|
678
|
-
const originalDb = new Uint8Array(readFileSync(testDbPath));
|
|
679
|
-
const originalConfig = readFileSync(testConfigPath, "utf8");
|
|
680
|
-
|
|
681
|
-
const vbundle = createValidVBundle(undefined, {
|
|
682
|
-
min_runtime_version: "99.0.0",
|
|
683
|
-
});
|
|
684
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
685
|
-
method: "POST",
|
|
686
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
687
|
-
body: toArrayBuffer(vbundle),
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
await callHandler(handleMigrationImport, req);
|
|
691
|
-
|
|
692
|
-
const currentDb = new Uint8Array(readFileSync(testDbPath));
|
|
693
|
-
const currentConfig = readFileSync(testConfigPath, "utf8");
|
|
694
|
-
|
|
695
|
-
expect(currentDb).toEqual(originalDb);
|
|
696
|
-
expect(currentConfig).toBe(originalConfig);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// Regression: the route handler pre-checks runtime-version compat using
|
|
700
|
-
// `validation.manifest.compatibility` before calling `resetDb()` and
|
|
701
|
-
// `commitImport()`. If a future refactor reorders the close to come
|
|
702
|
-
// before the gate, an incompatible bundle would still 422 (commitImport
|
|
703
|
-
// has its own defense-in-depth gate) but it would unnecessarily close
|
|
704
|
-
// and reopen the live SQLite singleton on every rejected import.
|
|
705
|
-
//
|
|
706
|
-
// We can't easily spy on `resetDb` here without mocking `db-connection.js`
|
|
707
|
-
// module-wide (which other tests in this file rely on for real). The
|
|
708
|
-
// semantic regression — disk unchanged + 422 — is covered by the two
|
|
709
|
-
// tests above. This test is a marker so future readers know the pre-
|
|
710
|
-
// check intent; if it ever starts failing, recheck `handleMigrationImport`
|
|
711
|
-
// ordering against the comment at line 861-862 ("Validate the bundle
|
|
712
|
-
// before closing the DB to avoid an unnecessary close/reopen cycle when
|
|
713
|
-
// the bundle is invalid").
|
|
714
|
-
test("incompatible bundle short-circuits before resetDb (intent marker)", async () => {
|
|
715
|
-
const vbundle = createValidVBundle(undefined, {
|
|
716
|
-
min_runtime_version: "99.0.0",
|
|
717
|
-
});
|
|
718
|
-
const req = new Request("http://localhost/v1/migrations/import", {
|
|
719
|
-
method: "POST",
|
|
720
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
721
|
-
body: toArrayBuffer(vbundle),
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const res = await callHandler(handleMigrationImport, req);
|
|
725
|
-
expect(res.status).toBe(422);
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
// ---------------------------------------------------------------------------
|
|
730
|
-
// commitImport unit tests
|
|
731
|
-
// ---------------------------------------------------------------------------
|
|
732
|
-
|
|
733
|
-
describe("commitImport", () => {
|
|
734
|
-
test("returns ok: true with report on success", () => {
|
|
735
|
-
const newDbData = new Uint8Array([0xfa, 0xce]);
|
|
736
|
-
const vbundle = createValidVBundle([
|
|
737
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
738
|
-
]);
|
|
739
|
-
|
|
740
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
741
|
-
const result = commitImport({
|
|
742
|
-
archiveData: vbundle,
|
|
743
|
-
pathResolver: resolver,
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
expect(result.ok).toBe(true);
|
|
747
|
-
if (result.ok) {
|
|
748
|
-
expect(result.report.success).toBe(true);
|
|
749
|
-
expect(result.report.files.length).toBeGreaterThan(0);
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
test("returns validation_failed for invalid bundles", () => {
|
|
754
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
755
|
-
const result = commitImport({
|
|
756
|
-
archiveData: new Uint8Array([0xba, 0xad]),
|
|
757
|
-
pathResolver: resolver,
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
expect(result.ok).toBe(false);
|
|
761
|
-
if (!result.ok) {
|
|
762
|
-
expect(result.reason).toBe("validation_failed");
|
|
763
|
-
if (result.reason === "validation_failed") {
|
|
764
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test("creates parent directories if they do not exist", () => {
|
|
770
|
-
// Use a workspace that does not exist yet
|
|
771
|
-
const nonexistentWorkspace = join(testDir, "new-workspace");
|
|
772
|
-
const expectedDbPath = join(
|
|
773
|
-
nonexistentWorkspace,
|
|
774
|
-
"data",
|
|
775
|
-
"db",
|
|
776
|
-
"assistant.db",
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
const dbData = new Uint8Array([0x01, 0x02, 0x03]);
|
|
780
|
-
const vbundle = createValidVBundle([
|
|
781
|
-
{ path: "data/db/assistant.db", data: dbData },
|
|
782
|
-
]);
|
|
783
|
-
|
|
784
|
-
const resolver = new DefaultPathResolver(nonexistentWorkspace);
|
|
785
|
-
const result = commitImport({
|
|
786
|
-
archiveData: vbundle,
|
|
787
|
-
pathResolver: resolver,
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
expect(result.ok).toBe(true);
|
|
791
|
-
if (result.ok) {
|
|
792
|
-
expect(result.report.files[0].action).toBe("created");
|
|
793
|
-
expect(existsSync(expectedDbPath)).toBe(true);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Clean up
|
|
797
|
-
rmSync(nonexistentWorkspace, { recursive: true, force: true });
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
test("post-write integrity: written file SHA-256 matches expected", () => {
|
|
801
|
-
const newDbData = new Uint8Array([0xab, 0xcd, 0xef]);
|
|
802
|
-
const expectedSha256 = sha256Hex(newDbData);
|
|
803
|
-
|
|
804
|
-
const vbundle = createValidVBundle([
|
|
805
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
806
|
-
]);
|
|
807
|
-
|
|
808
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
809
|
-
const result = commitImport({
|
|
810
|
-
archiveData: vbundle,
|
|
811
|
-
pathResolver: resolver,
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
expect(result.ok).toBe(true);
|
|
815
|
-
if (result.ok) {
|
|
816
|
-
// No integrity warnings should be present
|
|
817
|
-
const integrityWarnings = result.report.warnings.filter((w) =>
|
|
818
|
-
w.includes("integrity"),
|
|
819
|
-
);
|
|
820
|
-
expect(integrityWarnings).toHaveLength(0);
|
|
821
|
-
|
|
822
|
-
// Verify the file on disk matches
|
|
823
|
-
const writtenData = new Uint8Array(readFileSync(testDbPath));
|
|
824
|
-
const writtenSha256 = sha256Hex(writtenData);
|
|
825
|
-
expect(writtenSha256).toBe(expectedSha256);
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
test("handles multiple files (db + config)", () => {
|
|
830
|
-
const newDbData = new Uint8Array([0x11, 0x22, 0x33]);
|
|
831
|
-
const newConfigData = new TextEncoder().encode('{"model":"claude"}');
|
|
832
|
-
|
|
833
|
-
const vbundle = createValidVBundle([
|
|
834
|
-
{ path: "data/db/assistant.db", data: newDbData },
|
|
835
|
-
{ path: "config/settings.json", data: newConfigData },
|
|
836
|
-
]);
|
|
837
|
-
|
|
838
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
839
|
-
const result = commitImport({
|
|
840
|
-
archiveData: vbundle,
|
|
841
|
-
pathResolver: resolver,
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
expect(result.ok).toBe(true);
|
|
845
|
-
if (result.ok) {
|
|
846
|
-
expect(result.report.summary.total_files).toBe(2);
|
|
847
|
-
|
|
848
|
-
// Verify both files were written
|
|
849
|
-
const writtenDb = new Uint8Array(readFileSync(testDbPath));
|
|
850
|
-
expect(writtenDb).toEqual(newDbData);
|
|
851
|
-
|
|
852
|
-
const writtenConfig = readFileSync(testConfigPath, "utf8");
|
|
853
|
-
expect(writtenConfig).toBe(
|
|
854
|
-
JSON.stringify({ model: "claude" }, null, 2) + "\n",
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
});
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
// ---------------------------------------------------------------------------
|
|
861
|
-
// Workspace clearing tests
|
|
862
|
-
// ---------------------------------------------------------------------------
|
|
863
|
-
|
|
864
|
-
describe("commitImport — workspace clearing", () => {
|
|
865
|
-
const skillsDir = join(testDir, "skills");
|
|
866
|
-
const hooksDir = join(testDir, "hooks");
|
|
867
|
-
|
|
868
|
-
afterEach(() => {
|
|
869
|
-
// Restore test fixture files for subsequent tests
|
|
870
|
-
mkdirSync(testDbDir, { recursive: true });
|
|
871
|
-
writeFileSync(testDbPath, EXISTING_DB_DATA);
|
|
872
|
-
writeFileSync(testConfigPath, JSON.stringify(EXISTING_CONFIG, null, 2));
|
|
873
|
-
// Clean up skills/hooks dirs
|
|
874
|
-
for (const dir of [skillsDir, hooksDir]) {
|
|
875
|
-
if (existsSync(dir)) {
|
|
876
|
-
rmSync(dir, { recursive: true, force: true });
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
test("clears stale files via workspace clearing (new-format workspace/ entries)", () => {
|
|
882
|
-
mkdirSync(join(skillsDir, "stale-skill"), { recursive: true });
|
|
883
|
-
writeFileSync(join(skillsDir, "stale-skill", "SKILL.md"), "stale");
|
|
884
|
-
|
|
885
|
-
const skillData = new TextEncoder().encode("# New Skill");
|
|
886
|
-
const vbundle = createValidVBundle([
|
|
887
|
-
{ path: "workspace/skills/new-skill/SKILL.md", data: skillData },
|
|
888
|
-
]);
|
|
889
|
-
|
|
890
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
891
|
-
const result = commitImport({
|
|
892
|
-
archiveData: vbundle,
|
|
893
|
-
pathResolver: resolver,
|
|
894
|
-
workspaceDir: testDir,
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
expect(result.ok).toBe(true);
|
|
898
|
-
if (!result.ok) {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// New skill written
|
|
903
|
-
expect(existsSync(join(skillsDir, "new-skill", "SKILL.md"))).toBe(true);
|
|
904
|
-
expect(readFileSync(join(skillsDir, "new-skill", "SKILL.md"), "utf8")).toBe(
|
|
905
|
-
"# New Skill",
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
// Stale skill removed by workspace clearing
|
|
909
|
-
expect(existsSync(join(skillsDir, "stale-skill"))).toBe(false);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
test("old-format skills/ entries do not trigger workspace clearing", () => {
|
|
913
|
-
mkdirSync(join(skillsDir, "stale-skill"), { recursive: true });
|
|
914
|
-
writeFileSync(join(skillsDir, "stale-skill", "SKILL.md"), "stale");
|
|
915
|
-
|
|
916
|
-
const skillData = new TextEncoder().encode("# New Skill");
|
|
917
|
-
const vbundle = createValidVBundle([
|
|
918
|
-
{ path: "skills/new-skill/SKILL.md", data: skillData },
|
|
919
|
-
]);
|
|
920
|
-
|
|
921
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
922
|
-
const result = commitImport({
|
|
923
|
-
archiveData: vbundle,
|
|
924
|
-
pathResolver: resolver,
|
|
925
|
-
workspaceDir: testDir,
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
expect(result.ok).toBe(true);
|
|
929
|
-
if (!result.ok) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// New skill written
|
|
934
|
-
expect(existsSync(join(skillsDir, "new-skill", "SKILL.md"))).toBe(true);
|
|
935
|
-
|
|
936
|
-
// Stale skill survives — old-format bundles don't trigger workspace clearing
|
|
937
|
-
expect(existsSync(join(skillsDir, "stale-skill", "SKILL.md"))).toBe(true);
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
test("hooks/ entries import to hooksDir (not workspace/hooks/)", () => {
|
|
941
|
-
// Use a separate hooks dir outside the workspace, like production layout
|
|
942
|
-
const externalHooksDir = join(testDir, ".hooks-external");
|
|
943
|
-
mkdirSync(externalHooksDir, { recursive: true });
|
|
944
|
-
|
|
945
|
-
const hookData = new TextEncoder().encode("#!/bin/sh\necho new");
|
|
946
|
-
const vbundle = createValidVBundle([
|
|
947
|
-
{ path: "hooks/new-hook/hook.sh", data: hookData },
|
|
948
|
-
]);
|
|
949
|
-
|
|
950
|
-
const resolver = new DefaultPathResolver(testDir, externalHooksDir);
|
|
951
|
-
const result = commitImport({
|
|
952
|
-
archiveData: vbundle,
|
|
953
|
-
pathResolver: resolver,
|
|
954
|
-
workspaceDir: testDir,
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
expect(result.ok).toBe(true);
|
|
958
|
-
if (!result.ok) {
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Hook written to the external hooks dir, not workspace/hooks/
|
|
963
|
-
expect(existsSync(join(externalHooksDir, "new-hook", "hook.sh"))).toBe(
|
|
964
|
-
true,
|
|
965
|
-
);
|
|
966
|
-
expect(
|
|
967
|
-
readFileSync(join(externalHooksDir, "new-hook", "hook.sh"), "utf8"),
|
|
968
|
-
).toBe("#!/bin/sh\necho new");
|
|
969
|
-
|
|
970
|
-
// Cleanup
|
|
971
|
-
rmSync(externalHooksDir, { recursive: true, force: true });
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
test("without workspaceDir, no clearing happens", () => {
|
|
975
|
-
mkdirSync(join(skillsDir, "existing-skill"), { recursive: true });
|
|
976
|
-
writeFileSync(
|
|
977
|
-
join(skillsDir, "existing-skill", "SKILL.md"),
|
|
978
|
-
"should survive",
|
|
979
|
-
);
|
|
980
|
-
|
|
981
|
-
const vbundle = createValidVBundle([
|
|
982
|
-
{
|
|
983
|
-
path: "skills/new-skill/SKILL.md",
|
|
984
|
-
data: new TextEncoder().encode("new"),
|
|
985
|
-
},
|
|
986
|
-
]);
|
|
987
|
-
|
|
988
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
989
|
-
// No workspaceDir — no clearing
|
|
990
|
-
const result = commitImport({
|
|
991
|
-
archiveData: vbundle,
|
|
992
|
-
pathResolver: resolver,
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
expect(result.ok).toBe(true);
|
|
996
|
-
|
|
997
|
-
// Existing skill survives (no workspace clearing without workspaceDir)
|
|
998
|
-
expect(existsSync(join(skillsDir, "existing-skill", "SKILL.md"))).toBe(
|
|
999
|
-
true,
|
|
1000
|
-
);
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
// ---------------------------------------------------------------------------
|
|
1005
|
-
// Config sanitization tests
|
|
1006
|
-
// ---------------------------------------------------------------------------
|
|
1007
|
-
|
|
1008
|
-
describe("commitImport — config sanitization", () => {
|
|
1009
|
-
test("imported workspace/config.json has environment-specific fields stripped", () => {
|
|
1010
|
-
const configWithEnvFields = {
|
|
1011
|
-
provider: "anthropic",
|
|
1012
|
-
model: "test-model",
|
|
1013
|
-
ingress: {
|
|
1014
|
-
publicBaseUrl: "https://my-tunnel.example.com",
|
|
1015
|
-
enabled: true,
|
|
1016
|
-
port: 8080,
|
|
1017
|
-
},
|
|
1018
|
-
daemon: {
|
|
1019
|
-
autoStart: true,
|
|
1020
|
-
logLevel: "debug",
|
|
1021
|
-
},
|
|
1022
|
-
skills: {
|
|
1023
|
-
load: {
|
|
1024
|
-
extraDirs: ["/home/user/custom-skills"],
|
|
1025
|
-
autoReload: true,
|
|
1026
|
-
},
|
|
1027
|
-
},
|
|
1028
|
-
memory: { enabled: true },
|
|
1029
|
-
};
|
|
1030
|
-
const configData = new TextEncoder().encode(
|
|
1031
|
-
JSON.stringify(configWithEnvFields, null, 2),
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
const vbundle = createValidVBundle([
|
|
1035
|
-
{ path: "workspace/config.json", data: configData },
|
|
1036
|
-
]);
|
|
1037
|
-
|
|
1038
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
1039
|
-
const result = commitImport({
|
|
1040
|
-
archiveData: vbundle,
|
|
1041
|
-
pathResolver: resolver,
|
|
1042
|
-
workspaceDir: testDir,
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
expect(result.ok).toBe(true);
|
|
1046
|
-
if (!result.ok) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Read the written config from disk and verify sanitization
|
|
1051
|
-
const writtenConfig = JSON.parse(readFileSync(testConfigPath, "utf8"));
|
|
1052
|
-
|
|
1053
|
-
// Environment-specific fields should be stripped/reset
|
|
1054
|
-
expect(writtenConfig.ingress.publicBaseUrl).toBe("");
|
|
1055
|
-
expect(writtenConfig.ingress.enabled).toBeUndefined();
|
|
1056
|
-
expect(writtenConfig.daemon).toBeUndefined();
|
|
1057
|
-
expect(writtenConfig.skills.load.extraDirs).toEqual([]);
|
|
1058
|
-
|
|
1059
|
-
// Non-environment-specific fields should be preserved
|
|
1060
|
-
expect(writtenConfig.provider).toBe("anthropic");
|
|
1061
|
-
expect(writtenConfig.model).toBe("test-model");
|
|
1062
|
-
expect(writtenConfig.ingress.port).toBe(8080);
|
|
1063
|
-
expect(writtenConfig.skills.load.autoReload).toBe(true);
|
|
1064
|
-
expect(writtenConfig.memory.enabled).toBe(true);
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
test("imported config/settings.json (legacy) has environment-specific fields stripped", () => {
|
|
1068
|
-
const configWithEnvFields = {
|
|
1069
|
-
provider: "openai",
|
|
1070
|
-
daemon: { autoStart: false },
|
|
1071
|
-
ingress: {
|
|
1072
|
-
publicBaseUrl: "https://old-tunnel.example.com",
|
|
1073
|
-
enabled: false,
|
|
1074
|
-
},
|
|
1075
|
-
skills: { load: { extraDirs: ["/legacy/skills"] } },
|
|
1076
|
-
};
|
|
1077
|
-
const configData = new TextEncoder().encode(
|
|
1078
|
-
JSON.stringify(configWithEnvFields, null, 2),
|
|
1079
|
-
);
|
|
1080
|
-
|
|
1081
|
-
const vbundle = createValidVBundle([
|
|
1082
|
-
{ path: "config/settings.json", data: configData },
|
|
1083
|
-
]);
|
|
1084
|
-
|
|
1085
|
-
const resolver = new DefaultPathResolver(testDir);
|
|
1086
|
-
const result = commitImport({
|
|
1087
|
-
archiveData: vbundle,
|
|
1088
|
-
pathResolver: resolver,
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
expect(result.ok).toBe(true);
|
|
1092
|
-
if (!result.ok) {
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const writtenConfig = JSON.parse(readFileSync(testConfigPath, "utf8"));
|
|
1097
|
-
|
|
1098
|
-
expect(writtenConfig.ingress.publicBaseUrl).toBe("");
|
|
1099
|
-
expect(writtenConfig.ingress.enabled).toBeUndefined();
|
|
1100
|
-
expect(writtenConfig.daemon).toBeUndefined();
|
|
1101
|
-
expect(writtenConfig.skills.load.extraDirs).toEqual([]);
|
|
1102
|
-
expect(writtenConfig.provider).toBe("openai");
|
|
1103
|
-
});
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
// ---------------------------------------------------------------------------
|
|
1107
|
-
// Auth policy registration tests
|
|
1108
|
-
// ---------------------------------------------------------------------------
|
|
1109
|
-
|
|
1110
|
-
describe("route policy registration", () => {
|
|
1111
|
-
test("migrations/import policy requires settings.write scope", async () => {
|
|
1112
|
-
const { ROUTES } = await import("../runtime/routes/index.js");
|
|
1113
|
-
const policy =
|
|
1114
|
-
ROUTES.find((r) => r.endpoint === "migrations/import")?.policy ?? null;
|
|
1115
|
-
|
|
1116
|
-
expect(policy).not.toBeNull();
|
|
1117
|
-
expect(policy?.requiredScopes).toContain("settings.write");
|
|
1118
|
-
expect(policy?.allowedPrincipalTypes).toContain("actor");
|
|
1119
|
-
expect(policy?.allowedPrincipalTypes).toContain("svc_gateway");
|
|
1120
|
-
expect(policy?.allowedPrincipalTypes).toContain("local");
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
test("import policy matches export/preflight but differs from validate on scopes", async () => {
|
|
1124
|
-
const { ROUTES } = await import("../runtime/routes/index.js");
|
|
1125
|
-
const importPolicy =
|
|
1126
|
-
ROUTES.find((r) => r.endpoint === "migrations/import")?.policy ?? null;
|
|
1127
|
-
const validatePolicy =
|
|
1128
|
-
ROUTES.find((r) => r.endpoint === "migrations/validate")?.policy ?? null;
|
|
1129
|
-
const exportPolicy =
|
|
1130
|
-
ROUTES.find((r) => r.endpoint === "migrations/export")?.policy ?? null;
|
|
1131
|
-
const preflightPolicy =
|
|
1132
|
-
ROUTES.find((r) => r.endpoint === "migrations/import-preflight")
|
|
1133
|
-
?.policy ?? null;
|
|
1134
|
-
|
|
1135
|
-
// import, export, and preflight all require settings.write
|
|
1136
|
-
expect(importPolicy!.requiredScopes).toEqual(exportPolicy!.requiredScopes);
|
|
1137
|
-
expect(importPolicy!.requiredScopes).toEqual(
|
|
1138
|
-
preflightPolicy!.requiredScopes,
|
|
1139
|
-
);
|
|
1140
|
-
expect(importPolicy!.requiredScopes).toEqual(["settings.write"]);
|
|
1141
|
-
// validate is read-only so requires settings.read
|
|
1142
|
-
expect(validatePolicy!.requiredScopes).toEqual(["settings.read"]);
|
|
1143
|
-
// all share the same principal types
|
|
1144
|
-
expect(importPolicy!.allowedPrincipalTypes).toEqual(
|
|
1145
|
-
validatePolicy!.allowedPrincipalTypes,
|
|
1146
|
-
);
|
|
1147
|
-
});
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
// ---------------------------------------------------------------------------
|
|
1151
|
-
// Integration: existing routes unaffected
|
|
1152
|
-
// ---------------------------------------------------------------------------
|
|
1153
|
-
|
|
1154
|
-
describe("integration: existing routes unaffected", () => {
|
|
1155
|
-
test("GET /v1/health still works", async () => {
|
|
1156
|
-
const { handleDetailedHealth } =
|
|
1157
|
-
await import("../runtime/routes/identity-routes.js");
|
|
1158
|
-
const res = await handleDetailedHealth();
|
|
1159
|
-
const body = (await res.json()) as Record<string, unknown>;
|
|
1160
|
-
|
|
1161
|
-
expect(res.status).toBe(200);
|
|
1162
|
-
expect(body.status).toBe("healthy");
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
test("validate, export, and import-preflight handlers still importable", async () => {
|
|
1166
|
-
const {
|
|
1167
|
-
handleMigrationValidate,
|
|
1168
|
-
handleMigrationExport,
|
|
1169
|
-
handleMigrationImportPreflight,
|
|
1170
|
-
} = await import("../runtime/routes/migration-routes.js");
|
|
1171
|
-
|
|
1172
|
-
expect(typeof handleMigrationValidate).toBe("function");
|
|
1173
|
-
expect(typeof handleMigrationExport).toBe("function");
|
|
1174
|
-
expect(typeof handleMigrationImportPreflight).toBe("function");
|
|
1175
|
-
});
|
|
1176
|
-
});
|