@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,534 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for POST /v1/migrations/import-from-gcs.
|
|
3
|
-
*
|
|
4
|
-
* The endpoint kicks off an async import job that fetches a .vbundle from
|
|
5
|
-
* a signed GCS URL and streams it through the importer. The handler returns
|
|
6
|
-
* 202 with a `job_id`; the test then polls the in-process
|
|
7
|
-
* `migrationJobs` registry directly (no separate status endpoint yet — PR 4
|
|
8
|
-
* adds that) to assert the job progresses through `pending`/`running` to a
|
|
9
|
-
* terminal `complete` or `failed` state with the expected result / error
|
|
10
|
-
* mapping.
|
|
11
|
-
*
|
|
12
|
-
* Covered cases:
|
|
13
|
-
* - Happy path: 202 → poll → `complete` with `result.report.summary`.
|
|
14
|
-
* - Upstream fetch failure (HTTP 500): job ends `failed` with
|
|
15
|
-
* `error.code === "fetch_failed"` and `error.upstreamStatus === 500`.
|
|
16
|
-
* - Validation failure (corrupt bundle): job ends `failed` with
|
|
17
|
-
* `error.code === "validation_failed"`.
|
|
18
|
-
* - Concurrency: second import while the first is in flight → 409 with
|
|
19
|
-
* `error.code === "import_in_progress"`.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
createReadStream,
|
|
24
|
-
existsSync,
|
|
25
|
-
mkdirSync,
|
|
26
|
-
mkdtempSync,
|
|
27
|
-
realpathSync,
|
|
28
|
-
rmSync,
|
|
29
|
-
writeFileSync,
|
|
30
|
-
} from "node:fs";
|
|
31
|
-
import { createServer, type Server } from "node:http";
|
|
32
|
-
import { tmpdir } from "node:os";
|
|
33
|
-
import { join } from "node:path";
|
|
34
|
-
import {
|
|
35
|
-
afterAll,
|
|
36
|
-
afterEach,
|
|
37
|
-
beforeAll,
|
|
38
|
-
beforeEach,
|
|
39
|
-
describe,
|
|
40
|
-
expect,
|
|
41
|
-
test,
|
|
42
|
-
} from "bun:test";
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Test isolation: per-file workspace root.
|
|
46
|
-
//
|
|
47
|
-
// The streaming importer renames the workspace dir itself, so each test
|
|
48
|
-
// needs a fresh tmp dir that the importer can swap into place.
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
52
|
-
|
|
53
|
-
function freshWorkspaceRoot(): string {
|
|
54
|
-
const parent = realpathSync(
|
|
55
|
-
mkdtempSync(join(tmpdir(), "migration-import-from-gcs-")),
|
|
56
|
-
);
|
|
57
|
-
const workspaceDir = join(parent, "workspace");
|
|
58
|
-
mkdirSync(workspaceDir, { recursive: true });
|
|
59
|
-
return workspaceDir;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function setWorkspaceDir(dir: string): void {
|
|
63
|
-
process.env.CADET_WORKSPACE_DIR = dir;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
process.env.DISABLE_HTTP_AUTH = "true";
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Imports (after mocks so module-level code picks up the stubs)
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
import { defaultV1Options } from "../runtime/migrations/__tests__/v1-test-helpers.js";
|
|
73
|
-
import {
|
|
74
|
-
type MigrationJob,
|
|
75
|
-
migrationJobs,
|
|
76
|
-
} from "../runtime/migrations/job-registry.js";
|
|
77
|
-
import { buildVBundle } from "../runtime/migrations/vbundle-builder.js";
|
|
78
|
-
import {
|
|
79
|
-
_setUrlImportValidatorOptionsForTests,
|
|
80
|
-
handleMigrationImportFromGcs,
|
|
81
|
-
} from "../runtime/routes/migration-routes.js";
|
|
82
|
-
import { callHandler } from "./helpers/call-route-handler.js";
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Local http fixture server
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
interface FixtureServer {
|
|
88
|
-
server: Server;
|
|
89
|
-
port: number;
|
|
90
|
-
close: () => Promise<void>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function startFixtureServer(
|
|
94
|
-
handler: (
|
|
95
|
-
req: import("node:http").IncomingMessage,
|
|
96
|
-
res: import("node:http").ServerResponse,
|
|
97
|
-
) => void,
|
|
98
|
-
): Promise<FixtureServer> {
|
|
99
|
-
const server = createServer(handler);
|
|
100
|
-
// Track every connection so `close()` can forcibly tear them down. Tests
|
|
101
|
-
// that intentionally hang the response body (to simulate an in-flight
|
|
102
|
-
// upstream fetch) would otherwise deadlock `server.close()`, which waits
|
|
103
|
-
// for clients to disconnect on their own.
|
|
104
|
-
const sockets = new Set<import("node:net").Socket>();
|
|
105
|
-
server.on("connection", (sock) => {
|
|
106
|
-
sockets.add(sock);
|
|
107
|
-
sock.on("close", () => sockets.delete(sock));
|
|
108
|
-
});
|
|
109
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
110
|
-
const address = server.address();
|
|
111
|
-
if (!address || typeof address === "string") {
|
|
112
|
-
throw new Error("fixture server bound to unexpected address");
|
|
113
|
-
}
|
|
114
|
-
const port = address.port;
|
|
115
|
-
return {
|
|
116
|
-
server,
|
|
117
|
-
port,
|
|
118
|
-
close: async () => {
|
|
119
|
-
for (const sock of sockets) {
|
|
120
|
-
try {
|
|
121
|
-
sock.destroy();
|
|
122
|
-
} catch {
|
|
123
|
-
/* best effort */
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function makeFakeSignedUrl(port: number): string {
|
|
132
|
-
// `validateGcsSignedUrl` requires a signature query param; pin a dummy.
|
|
133
|
-
return `http://127.0.0.1:${port}/bundle?X-Goog-Signature=fake`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Fixture helpers
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
function makeSmallValidBundlePath(parent: string): string {
|
|
141
|
-
const { archive } = buildVBundle({
|
|
142
|
-
files: [
|
|
143
|
-
{
|
|
144
|
-
path: "workspace/data/db/assistant.db",
|
|
145
|
-
data: new TextEncoder().encode("SQLite format 3\0"),
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
path: "workspace/config.json",
|
|
149
|
-
data: new TextEncoder().encode(
|
|
150
|
-
JSON.stringify({ provider: "anthropic", model: "test-model" }),
|
|
151
|
-
),
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
...defaultV1Options(),
|
|
155
|
-
});
|
|
156
|
-
const bundlePath = join(parent, "fixture-small.vbundle");
|
|
157
|
-
writeFileSync(bundlePath, archive);
|
|
158
|
-
return bundlePath;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function makeCorruptBundlePath(parent: string): string {
|
|
162
|
-
// Random bytes — not a gzip stream, not a tar, nothing the importer can
|
|
163
|
-
// parse. Drives `validation_failed` inside `streamCommitImport`.
|
|
164
|
-
const payload = new Uint8Array(4096);
|
|
165
|
-
for (let i = 0; i < payload.length; i += 1) {
|
|
166
|
-
payload[i] = Math.floor(Math.random() * 256);
|
|
167
|
-
}
|
|
168
|
-
const bundlePath = join(parent, "fixture-corrupt.vbundle");
|
|
169
|
-
writeFileSync(bundlePath, payload);
|
|
170
|
-
return bundlePath;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Poll helpers — the registry is in-process so we peek at `getJob` directly
|
|
175
|
-
// until the job leaves the pending/running states.
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
async function pollJobUntilDone(
|
|
179
|
-
jobId: string,
|
|
180
|
-
timeoutMs = 30_000,
|
|
181
|
-
): Promise<MigrationJob> {
|
|
182
|
-
const startedAt = Date.now();
|
|
183
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
184
|
-
const job = migrationJobs.getJob(jobId);
|
|
185
|
-
if (!job) {
|
|
186
|
-
throw new Error(`Job ${jobId} disappeared from the registry`);
|
|
187
|
-
}
|
|
188
|
-
if (job.status === "complete" || job.status === "failed") {
|
|
189
|
-
return job;
|
|
190
|
-
}
|
|
191
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
192
|
-
}
|
|
193
|
-
throw new Error(`Job ${jobId} did not finish within ${timeoutMs}ms`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// Global test-only allowlist: widen the URL validator to accept 127.0.0.1.
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
beforeAll(() => {
|
|
201
|
-
_setUrlImportValidatorOptionsForTests({
|
|
202
|
-
allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
afterAll(() => {
|
|
207
|
-
_setUrlImportValidatorOptionsForTests(undefined);
|
|
208
|
-
if (originalWorkspaceDir !== undefined) {
|
|
209
|
-
process.env.CADET_WORKSPACE_DIR = originalWorkspaceDir;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Each test gets its own workspace dir so the streaming importer's atomic
|
|
214
|
-
// swap doesn't leak state across tests.
|
|
215
|
-
let testWorkspaceRoot: string;
|
|
216
|
-
let testParent: string;
|
|
217
|
-
|
|
218
|
-
beforeEach(() => {
|
|
219
|
-
testWorkspaceRoot = freshWorkspaceRoot();
|
|
220
|
-
testParent = join(testWorkspaceRoot, "..");
|
|
221
|
-
setWorkspaceDir(testWorkspaceRoot);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
afterEach(() => {
|
|
225
|
-
try {
|
|
226
|
-
rmSync(testParent, { recursive: true, force: true });
|
|
227
|
-
} catch {
|
|
228
|
-
/* best effort */
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// Response shape types
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
interface AcceptedResponse {
|
|
237
|
-
job_id: string;
|
|
238
|
-
status: "pending";
|
|
239
|
-
type: "import";
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
interface ConflictResponse {
|
|
243
|
-
error: { code: "import_in_progress"; message: string };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
interface BadRequestResponse {
|
|
247
|
-
error: { code: string; message: string };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
interface InvalidBundleUrlResponse {
|
|
251
|
-
error: { code: "invalid_bundle_url"; message: string };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// Tests
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
|
|
258
|
-
describe("POST /v1/migrations/import-from-gcs", () => {
|
|
259
|
-
test("happy path: 202 → job completes with flat result.summary", async () => {
|
|
260
|
-
const bundlePath = makeSmallValidBundlePath(testParent);
|
|
261
|
-
|
|
262
|
-
const fixture = await startFixtureServer((_req, res) => {
|
|
263
|
-
res.writeHead(200, { "Content-Type": "application/octet-stream" });
|
|
264
|
-
createReadStream(bundlePath).pipe(res);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const req = new Request(
|
|
269
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
270
|
-
{
|
|
271
|
-
method: "POST",
|
|
272
|
-
headers: { "Content-Type": "application/json" },
|
|
273
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const res = await callHandler(
|
|
278
|
-
handleMigrationImportFromGcs,
|
|
279
|
-
req,
|
|
280
|
-
undefined,
|
|
281
|
-
202,
|
|
282
|
-
);
|
|
283
|
-
expect(res.status).toBe(202);
|
|
284
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
285
|
-
expect(body.type).toBe("import");
|
|
286
|
-
expect(body.status).toBe("pending");
|
|
287
|
-
expect(typeof body.job_id).toBe("string");
|
|
288
|
-
expect(body.job_id.length).toBeGreaterThan(0);
|
|
289
|
-
|
|
290
|
-
const finalJob = await pollJobUntilDone(body.job_id);
|
|
291
|
-
expect(finalJob.status).toBe("complete");
|
|
292
|
-
|
|
293
|
-
// The job result must match the wire shape the CLI expects when it
|
|
294
|
-
// casts `terminal.result` to `ImportResponse`: `success` and `summary`
|
|
295
|
-
// live at the top level, NOT under a nested `report` field. Asserting
|
|
296
|
-
// here is what locks in the fix for the async-vs-sync shape drift
|
|
297
|
-
// that was misreporting successful imports as failed.
|
|
298
|
-
const result = finalJob.result as {
|
|
299
|
-
success: boolean;
|
|
300
|
-
summary: {
|
|
301
|
-
total_files: number;
|
|
302
|
-
files_created: number;
|
|
303
|
-
};
|
|
304
|
-
};
|
|
305
|
-
expect(result).toBeDefined();
|
|
306
|
-
expect(result.success).toBe(true);
|
|
307
|
-
expect(result.summary).toBeDefined();
|
|
308
|
-
expect(result.summary.total_files).toBeGreaterThan(0);
|
|
309
|
-
expect(result.summary.files_created).toBeGreaterThan(0);
|
|
310
|
-
|
|
311
|
-
// Workspace was swapped into place.
|
|
312
|
-
expect(
|
|
313
|
-
existsSync(join(testWorkspaceRoot, "data", "db", "assistant.db")),
|
|
314
|
-
).toBe(true);
|
|
315
|
-
expect(existsSync(join(testWorkspaceRoot, "config.json"))).toBe(true);
|
|
316
|
-
} finally {
|
|
317
|
-
await fixture.close();
|
|
318
|
-
}
|
|
319
|
-
}, 30_000);
|
|
320
|
-
|
|
321
|
-
test("upstream 500: job ends failed with fetch_failed + upstreamStatus 500", async () => {
|
|
322
|
-
const fixture = await startFixtureServer((_req, res) => {
|
|
323
|
-
res.writeHead(500);
|
|
324
|
-
res.end("oh no");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
const req = new Request(
|
|
329
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
330
|
-
{
|
|
331
|
-
method: "POST",
|
|
332
|
-
headers: { "Content-Type": "application/json" },
|
|
333
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
334
|
-
},
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const res = await callHandler(
|
|
338
|
-
handleMigrationImportFromGcs,
|
|
339
|
-
req,
|
|
340
|
-
undefined,
|
|
341
|
-
202,
|
|
342
|
-
);
|
|
343
|
-
expect(res.status).toBe(202);
|
|
344
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
345
|
-
|
|
346
|
-
const finalJob = await pollJobUntilDone(body.job_id);
|
|
347
|
-
expect(finalJob.status).toBe("failed");
|
|
348
|
-
expect(finalJob.error?.code).toBe("fetch_failed");
|
|
349
|
-
expect(finalJob.error?.upstreamStatus).toBe(500);
|
|
350
|
-
} finally {
|
|
351
|
-
await fixture.close();
|
|
352
|
-
}
|
|
353
|
-
}, 30_000);
|
|
354
|
-
|
|
355
|
-
test("corrupt bundle: job ends failed with validation_failed", async () => {
|
|
356
|
-
const bundlePath = makeCorruptBundlePath(testParent);
|
|
357
|
-
|
|
358
|
-
const fixture = await startFixtureServer((_req, res) => {
|
|
359
|
-
res.writeHead(200, { "Content-Type": "application/octet-stream" });
|
|
360
|
-
createReadStream(bundlePath).pipe(res);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
const req = new Request(
|
|
365
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
366
|
-
{
|
|
367
|
-
method: "POST",
|
|
368
|
-
headers: { "Content-Type": "application/json" },
|
|
369
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
370
|
-
},
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
const res = await callHandler(
|
|
374
|
-
handleMigrationImportFromGcs,
|
|
375
|
-
req,
|
|
376
|
-
undefined,
|
|
377
|
-
202,
|
|
378
|
-
);
|
|
379
|
-
expect(res.status).toBe(202);
|
|
380
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
381
|
-
|
|
382
|
-
const finalJob = await pollJobUntilDone(body.job_id);
|
|
383
|
-
expect(finalJob.status).toBe("failed");
|
|
384
|
-
// The streaming importer maps a mangled archive to
|
|
385
|
-
// `extraction_failed` (gunzip throws on the random payload before the
|
|
386
|
-
// tar layer even sees it). Parity with the URL-body path: that path
|
|
387
|
-
// returns 500 `{ reason: "extraction_failed" }` for the same input,
|
|
388
|
-
// and `runGcsImport` re-throws it as a `GcsImportError` carrying the
|
|
389
|
-
// same code.
|
|
390
|
-
expect(finalJob.error?.code).toBe("extraction_failed");
|
|
391
|
-
} finally {
|
|
392
|
-
await fixture.close();
|
|
393
|
-
}
|
|
394
|
-
}, 30_000);
|
|
395
|
-
|
|
396
|
-
test("second import while first is in flight returns 409", async () => {
|
|
397
|
-
// A fixture that never responds keeps the first job stuck in `pending`
|
|
398
|
-
// (the runner is scheduled on a microtask, but the handler's `fetch`
|
|
399
|
-
// hangs on the open socket). Even `pending` counts as in-flight for
|
|
400
|
-
// the concurrency check, so the 409 is deterministic regardless of
|
|
401
|
-
// event-loop timing.
|
|
402
|
-
const fixture = await startFixtureServer((_req, _res) => {
|
|
403
|
-
// Intentionally no response — hold the socket open.
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
let firstJobId: string | undefined;
|
|
407
|
-
try {
|
|
408
|
-
const firstReq = new Request(
|
|
409
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
410
|
-
{
|
|
411
|
-
method: "POST",
|
|
412
|
-
headers: { "Content-Type": "application/json" },
|
|
413
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
414
|
-
},
|
|
415
|
-
);
|
|
416
|
-
const firstRes = await callHandler(
|
|
417
|
-
handleMigrationImportFromGcs,
|
|
418
|
-
firstReq,
|
|
419
|
-
undefined,
|
|
420
|
-
202,
|
|
421
|
-
);
|
|
422
|
-
expect(firstRes.status).toBe(202);
|
|
423
|
-
const firstBody = (await firstRes.json()) as AcceptedResponse;
|
|
424
|
-
firstJobId = firstBody.job_id;
|
|
425
|
-
|
|
426
|
-
const secondReq = new Request(
|
|
427
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
428
|
-
{
|
|
429
|
-
method: "POST",
|
|
430
|
-
headers: { "Content-Type": "application/json" },
|
|
431
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
432
|
-
},
|
|
433
|
-
);
|
|
434
|
-
const secondRes = await callHandler(
|
|
435
|
-
handleMigrationImportFromGcs,
|
|
436
|
-
secondReq,
|
|
437
|
-
undefined,
|
|
438
|
-
202,
|
|
439
|
-
);
|
|
440
|
-
expect(secondRes.status).toBe(409);
|
|
441
|
-
const secondBody = (await secondRes.json()) as ConflictResponse;
|
|
442
|
-
expect(secondBody.error.code).toBe("import_in_progress");
|
|
443
|
-
expect(secondBody.error.message).toContain(firstBody.job_id);
|
|
444
|
-
} finally {
|
|
445
|
-
// Close the server — this destroys every tracked socket, which
|
|
446
|
-
// unblocks the first job's `fetch` and lets the runner settle into
|
|
447
|
-
// `failed`. Wait for that before returning so subsequent tests can
|
|
448
|
-
// start their own `"import"` job without tripping the registry's
|
|
449
|
-
// single-in-flight invariant.
|
|
450
|
-
await fixture.close();
|
|
451
|
-
if (firstJobId !== undefined) {
|
|
452
|
-
await pollJobUntilDone(firstJobId);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}, 30_000);
|
|
456
|
-
|
|
457
|
-
test("invalid bundle_url (disallowed host) returns 400 without consuming the in-flight slot", async () => {
|
|
458
|
-
// Disallowed host — syntactically a valid URL (passes zod), but
|
|
459
|
-
// `validateGcsSignedUrl` must reject it synchronously so the single
|
|
460
|
-
// in-flight import slot is NOT taken. Without the preflight, this
|
|
461
|
-
// would return 202 + job_id and then only fail inside the async
|
|
462
|
-
// runner, blocking a correct retry until the doomed job cleared.
|
|
463
|
-
const badReq = new Request(
|
|
464
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
465
|
-
{
|
|
466
|
-
method: "POST",
|
|
467
|
-
headers: { "Content-Type": "application/json" },
|
|
468
|
-
body: JSON.stringify({
|
|
469
|
-
bundle_url: "http://evil.example.com/bundle?X-Goog-Signature=fake",
|
|
470
|
-
}),
|
|
471
|
-
},
|
|
472
|
-
);
|
|
473
|
-
const badRes = await callHandler(
|
|
474
|
-
handleMigrationImportFromGcs,
|
|
475
|
-
badReq,
|
|
476
|
-
undefined,
|
|
477
|
-
202,
|
|
478
|
-
);
|
|
479
|
-
expect(badRes.status).toBe(400);
|
|
480
|
-
const badBody = (await badRes.json()) as InvalidBundleUrlResponse;
|
|
481
|
-
expect(badBody.error.code).toBe("invalid_bundle_url");
|
|
482
|
-
expect(typeof badBody.error.message).toBe("string");
|
|
483
|
-
expect(badBody.error.message.length).toBeGreaterThan(0);
|
|
484
|
-
|
|
485
|
-
// A follow-up valid request must still be able to start a job — proving
|
|
486
|
-
// the doomed request did not occupy the concurrency slot.
|
|
487
|
-
const bundlePath = makeSmallValidBundlePath(testParent);
|
|
488
|
-
const fixture = await startFixtureServer((_req, res) => {
|
|
489
|
-
res.writeHead(200, { "Content-Type": "application/octet-stream" });
|
|
490
|
-
createReadStream(bundlePath).pipe(res);
|
|
491
|
-
});
|
|
492
|
-
try {
|
|
493
|
-
const goodReq = new Request(
|
|
494
|
-
"http://localhost/v1/migrations/import-from-gcs",
|
|
495
|
-
{
|
|
496
|
-
method: "POST",
|
|
497
|
-
headers: { "Content-Type": "application/json" },
|
|
498
|
-
body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
|
|
499
|
-
},
|
|
500
|
-
);
|
|
501
|
-
const goodRes = await callHandler(
|
|
502
|
-
handleMigrationImportFromGcs,
|
|
503
|
-
goodReq,
|
|
504
|
-
undefined,
|
|
505
|
-
202,
|
|
506
|
-
);
|
|
507
|
-
expect(goodRes.status).toBe(202);
|
|
508
|
-
const goodBody = (await goodRes.json()) as AcceptedResponse;
|
|
509
|
-
expect(goodBody.type).toBe("import");
|
|
510
|
-
|
|
511
|
-
// Drain the job so subsequent tests start from a clean registry.
|
|
512
|
-
await pollJobUntilDone(goodBody.job_id);
|
|
513
|
-
} finally {
|
|
514
|
-
await fixture.close();
|
|
515
|
-
}
|
|
516
|
-
}, 30_000);
|
|
517
|
-
|
|
518
|
-
test("malformed body (missing bundle_url) returns 400", async () => {
|
|
519
|
-
const req = new Request("http://localhost/v1/migrations/import-from-gcs", {
|
|
520
|
-
method: "POST",
|
|
521
|
-
headers: { "Content-Type": "application/json" },
|
|
522
|
-
body: JSON.stringify({}),
|
|
523
|
-
});
|
|
524
|
-
const res = await callHandler(
|
|
525
|
-
handleMigrationImportFromGcs,
|
|
526
|
-
req,
|
|
527
|
-
undefined,
|
|
528
|
-
202,
|
|
529
|
-
);
|
|
530
|
-
expect(res.status).toBe(400);
|
|
531
|
-
const body = (await res.json()) as BadRequestResponse;
|
|
532
|
-
expect(body.error.code).toBe("BAD_REQUEST");
|
|
533
|
-
});
|
|
534
|
-
});
|