@mycadet/assistant 0.12.10 → 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 +10 -10
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- 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 +41 -5
- 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/llm.ts +3 -5
- 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__/conversation-query-routes.test.ts +13 -11
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/conversation-query-routes.ts +0 -96
- 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,1395 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the migration wizard state machine.
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* - State creation and initial values
|
|
6
|
-
* - Direction selection (both directions)
|
|
7
|
-
* - Step transitions: forward, backward, skip prevention
|
|
8
|
-
* - Step status tracking: idle, loading, success, error
|
|
9
|
-
* - Validate step execution: success, validation failure, transport error
|
|
10
|
-
* - Preflight step execution: success, validation failure, transport error
|
|
11
|
-
* - Transfer step execution: runtime export+import, managed export+poll+import
|
|
12
|
-
* - Rebind-secrets completion
|
|
13
|
-
* - Error handling: retryable vs non-retryable, retry reset
|
|
14
|
-
* - Persistence: serialize, deserialize, resume, corrupted data
|
|
15
|
-
* - Query helpers: step order, accessibility, completion
|
|
16
|
-
* - Full wizard flow end-to-end
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { describe, expect, test } from "bun:test";
|
|
20
|
-
|
|
21
|
-
import type {
|
|
22
|
-
ImportCommitResponse,
|
|
23
|
-
ImportPreflightResponse,
|
|
24
|
-
TransportConfig,
|
|
25
|
-
ValidateResponse,
|
|
26
|
-
} from "../runtime/migrations/migration-transport.js";
|
|
27
|
-
import type {
|
|
28
|
-
MigrationWizardState,
|
|
29
|
-
StepExecutorOptions,
|
|
30
|
-
WizardStep,
|
|
31
|
-
} from "../runtime/migrations/migration-wizard.js";
|
|
32
|
-
import {
|
|
33
|
-
canRetryCurrentStep,
|
|
34
|
-
completeRebindSecrets,
|
|
35
|
-
createWizardState,
|
|
36
|
-
deserializeWizardState,
|
|
37
|
-
executePreflightStep,
|
|
38
|
-
executeTransferStep,
|
|
39
|
-
executeValidateStep,
|
|
40
|
-
getCurrentStepIndex,
|
|
41
|
-
getStepOrder,
|
|
42
|
-
getTotalSteps,
|
|
43
|
-
goBackTo,
|
|
44
|
-
isResumable,
|
|
45
|
-
isStepAccessible,
|
|
46
|
-
isWizardComplete,
|
|
47
|
-
prepareForResume,
|
|
48
|
-
resetStepForRetry,
|
|
49
|
-
selectDirection,
|
|
50
|
-
serializeWizardState,
|
|
51
|
-
setBundleUploaded,
|
|
52
|
-
validateWizardTransition,
|
|
53
|
-
} from "../runtime/migrations/migration-wizard.js";
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Test helpers
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
function mockFetch(
|
|
60
|
-
status: number,
|
|
61
|
-
body: unknown,
|
|
62
|
-
headers?: Record<string, string>,
|
|
63
|
-
): typeof fetch {
|
|
64
|
-
return (async () => {
|
|
65
|
-
const responseHeaders = new Headers(headers);
|
|
66
|
-
if (
|
|
67
|
-
typeof body === "object" &&
|
|
68
|
-
body !== undefined &&
|
|
69
|
-
!(body instanceof ArrayBuffer)
|
|
70
|
-
) {
|
|
71
|
-
responseHeaders.set("Content-Type", "application/json");
|
|
72
|
-
return new Response(JSON.stringify(body), {
|
|
73
|
-
status,
|
|
74
|
-
headers: responseHeaders,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (body instanceof ArrayBuffer) {
|
|
78
|
-
return new Response(body, { status, headers: responseHeaders });
|
|
79
|
-
}
|
|
80
|
-
return new Response(String(body), { status, headers: responseHeaders });
|
|
81
|
-
}) as unknown as typeof fetch;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function runtimeConfig(overrides?: Partial<TransportConfig>): TransportConfig {
|
|
85
|
-
return {
|
|
86
|
-
baseURL: "http://localhost:7821",
|
|
87
|
-
target: "runtime",
|
|
88
|
-
authHeader: "Bearer test-jwt",
|
|
89
|
-
fetchFn: mockFetch(200, {}),
|
|
90
|
-
...overrides,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function managedConfig(overrides?: Partial<TransportConfig>): TransportConfig {
|
|
95
|
-
return {
|
|
96
|
-
baseURL: "https://cloud.mycadet.ai",
|
|
97
|
-
target: "managed",
|
|
98
|
-
authHeader: "test-session-token",
|
|
99
|
-
fetchFn: mockFetch(200, {}),
|
|
100
|
-
...overrides,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function makeExecutorOptions(
|
|
105
|
-
overrides?: Partial<StepExecutorOptions>,
|
|
106
|
-
): StepExecutorOptions {
|
|
107
|
-
return {
|
|
108
|
-
sourceConfig: runtimeConfig(),
|
|
109
|
-
destConfig: runtimeConfig(),
|
|
110
|
-
bundleData: new ArrayBuffer(16),
|
|
111
|
-
...overrides,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Advance state to a specific step for testing. */
|
|
116
|
-
function advanceTo(step: WizardStep): MigrationWizardState {
|
|
117
|
-
let state = createWizardState();
|
|
118
|
-
const stepOrder: WizardStep[] = [
|
|
119
|
-
"select-direction",
|
|
120
|
-
"upload-bundle",
|
|
121
|
-
"validate",
|
|
122
|
-
"preflight-review",
|
|
123
|
-
"transfer",
|
|
124
|
-
"rebind-secrets",
|
|
125
|
-
"complete",
|
|
126
|
-
];
|
|
127
|
-
const targetIdx = stepOrder.indexOf(step);
|
|
128
|
-
|
|
129
|
-
if (targetIdx >= 1) {
|
|
130
|
-
state = selectDirection(state, "managed-to-self-hosted");
|
|
131
|
-
}
|
|
132
|
-
if (targetIdx >= 2) {
|
|
133
|
-
state = setBundleUploaded(state);
|
|
134
|
-
}
|
|
135
|
-
if (targetIdx >= 3) {
|
|
136
|
-
state = {
|
|
137
|
-
...state,
|
|
138
|
-
steps: { ...state.steps, validate: { status: "success" } },
|
|
139
|
-
currentStep: "preflight-review",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (targetIdx >= 4) {
|
|
143
|
-
state = {
|
|
144
|
-
...state,
|
|
145
|
-
steps: { ...state.steps, "preflight-review": { status: "success" } },
|
|
146
|
-
currentStep: "transfer",
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (targetIdx >= 5) {
|
|
150
|
-
state = {
|
|
151
|
-
...state,
|
|
152
|
-
steps: { ...state.steps, transfer: { status: "success" } },
|
|
153
|
-
currentStep: "rebind-secrets",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (targetIdx >= 6) {
|
|
157
|
-
state = completeRebindSecrets(state);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return state;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// State creation
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
describe("createWizardState", () => {
|
|
168
|
-
test("creates initial state with select-direction as first step", () => {
|
|
169
|
-
const state = createWizardState();
|
|
170
|
-
expect(state.currentStep).toBe("select-direction");
|
|
171
|
-
expect(state.direction).toBeUndefined();
|
|
172
|
-
expect(state.hasBundleData).toBe(false);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("all steps start as idle", () => {
|
|
176
|
-
const state = createWizardState();
|
|
177
|
-
const allSteps: WizardStep[] = [
|
|
178
|
-
"select-direction",
|
|
179
|
-
"upload-bundle",
|
|
180
|
-
"validate",
|
|
181
|
-
"preflight-review",
|
|
182
|
-
"transfer",
|
|
183
|
-
"rebind-secrets",
|
|
184
|
-
"complete",
|
|
185
|
-
];
|
|
186
|
-
for (const step of allSteps) {
|
|
187
|
-
expect(state.steps[step].status).toBe("idle");
|
|
188
|
-
expect(state.steps[step].error).toBeUndefined();
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("sets timestamps", () => {
|
|
193
|
-
const before = new Date().toISOString();
|
|
194
|
-
const state = createWizardState();
|
|
195
|
-
const after = new Date().toISOString();
|
|
196
|
-
expect(state.createdAt >= before).toBe(true);
|
|
197
|
-
expect(state.createdAt <= after).toBe(true);
|
|
198
|
-
expect(state.updatedAt).toBe(state.createdAt);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("no results are set initially", () => {
|
|
202
|
-
const state = createWizardState();
|
|
203
|
-
expect(state.validateResult).toBeUndefined();
|
|
204
|
-
expect(state.preflightResult).toBeUndefined();
|
|
205
|
-
expect(state.exportResult).toBeUndefined();
|
|
206
|
-
expect(state.importResult).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// Direction selection
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
describe("selectDirection", () => {
|
|
215
|
-
test("managed-to-self-hosted direction advances to upload-bundle", () => {
|
|
216
|
-
const state = createWizardState();
|
|
217
|
-
const next = selectDirection(state, "managed-to-self-hosted");
|
|
218
|
-
expect(next.direction).toBe("managed-to-self-hosted");
|
|
219
|
-
expect(next.currentStep).toBe("upload-bundle");
|
|
220
|
-
expect(next.steps["select-direction"].status).toBe("success");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("self-hosted-to-managed direction advances to upload-bundle", () => {
|
|
224
|
-
const state = createWizardState();
|
|
225
|
-
const next = selectDirection(state, "self-hosted-to-managed");
|
|
226
|
-
expect(next.direction).toBe("self-hosted-to-managed");
|
|
227
|
-
expect(next.currentStep).toBe("upload-bundle");
|
|
228
|
-
expect(next.steps["select-direction"].status).toBe("success");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("throws when called from wrong step", () => {
|
|
232
|
-
const state = advanceTo("validate");
|
|
233
|
-
expect(() => selectDirection(state, "managed-to-self-hosted")).toThrow(
|
|
234
|
-
"wrong step",
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
// Bundle upload
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
|
|
243
|
-
describe("setBundleUploaded", () => {
|
|
244
|
-
test("advances to validate step", () => {
|
|
245
|
-
const state = advanceTo("upload-bundle");
|
|
246
|
-
const next = setBundleUploaded(state);
|
|
247
|
-
expect(next.currentStep).toBe("validate");
|
|
248
|
-
expect(next.hasBundleData).toBe(true);
|
|
249
|
-
expect(next.steps["upload-bundle"].status).toBe("success");
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("throws when called from wrong step", () => {
|
|
253
|
-
const state = advanceTo("validate");
|
|
254
|
-
expect(() => setBundleUploaded(state)).toThrow("wrong step");
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
// Transition validation
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
describe("validateWizardTransition", () => {
|
|
263
|
-
test("same-step transition is always valid", () => {
|
|
264
|
-
const state = advanceTo("validate");
|
|
265
|
-
expect(validateWizardTransition(state, "validate").valid).toBe(true);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("forward transition requires current step to be in success", () => {
|
|
269
|
-
let state = advanceTo("validate");
|
|
270
|
-
// validate is currently idle
|
|
271
|
-
const result = validateWizardTransition(state, "preflight-review");
|
|
272
|
-
expect(result.valid).toBe(false);
|
|
273
|
-
expect(result.reason).toContain("idle");
|
|
274
|
-
|
|
275
|
-
// Set validate to success
|
|
276
|
-
state = {
|
|
277
|
-
...state,
|
|
278
|
-
steps: { ...state.steps, validate: { status: "success" } },
|
|
279
|
-
};
|
|
280
|
-
expect(validateWizardTransition(state, "preflight-review").valid).toBe(
|
|
281
|
-
true,
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("cannot skip steps forward", () => {
|
|
286
|
-
const state = advanceTo("validate");
|
|
287
|
-
const result = validateWizardTransition(state, "transfer");
|
|
288
|
-
expect(result.valid).toBe(false);
|
|
289
|
-
expect(result.reason).toContain("skip");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("backward transition is always allowed", () => {
|
|
293
|
-
const state = advanceTo("transfer");
|
|
294
|
-
expect(validateWizardTransition(state, "select-direction").valid).toBe(
|
|
295
|
-
true,
|
|
296
|
-
);
|
|
297
|
-
expect(validateWizardTransition(state, "upload-bundle").valid).toBe(true);
|
|
298
|
-
expect(validateWizardTransition(state, "validate").valid).toBe(true);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("cannot transition from complete", () => {
|
|
302
|
-
const state = advanceTo("complete");
|
|
303
|
-
const result = validateWizardTransition(state, "select-direction");
|
|
304
|
-
expect(result.valid).toBe(false);
|
|
305
|
-
expect(result.reason).toContain("terminal");
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// ---------------------------------------------------------------------------
|
|
310
|
-
// goBackTo
|
|
311
|
-
// ---------------------------------------------------------------------------
|
|
312
|
-
|
|
313
|
-
describe("goBackTo", () => {
|
|
314
|
-
test("going back resets target and subsequent steps to idle", () => {
|
|
315
|
-
const state = advanceTo("transfer");
|
|
316
|
-
const backed = goBackTo(state, "upload-bundle");
|
|
317
|
-
expect(backed.currentStep).toBe("upload-bundle");
|
|
318
|
-
// Target step is reset
|
|
319
|
-
expect(backed.steps["upload-bundle"].status).toBe("idle");
|
|
320
|
-
// Steps after target are reset
|
|
321
|
-
expect(backed.steps["validate"].status).toBe("idle");
|
|
322
|
-
expect(backed.steps["preflight-review"].status).toBe("idle");
|
|
323
|
-
expect(backed.steps["transfer"].status).toBe("idle");
|
|
324
|
-
// Earlier steps retain their status
|
|
325
|
-
expect(backed.steps["select-direction"].status).toBe("success");
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("going back clears results for reset steps", () => {
|
|
329
|
-
let state = advanceTo("transfer");
|
|
330
|
-
state = {
|
|
331
|
-
...state,
|
|
332
|
-
validateResult: { is_valid: true, errors: [], manifest: {} as never },
|
|
333
|
-
};
|
|
334
|
-
state = { ...state, preflightResult: { can_import: true } as never };
|
|
335
|
-
const backed = goBackTo(state, "upload-bundle");
|
|
336
|
-
expect(backed.validateResult).toBeUndefined();
|
|
337
|
-
expect(backed.preflightResult).toBeUndefined();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("throws for invalid back-navigation", () => {
|
|
341
|
-
const state = advanceTo("complete");
|
|
342
|
-
expect(() => goBackTo(state, "select-direction")).toThrow(
|
|
343
|
-
"Invalid back-navigation",
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
// executeValidateStep
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
describe("executeValidateStep", () => {
|
|
353
|
-
test("success — sets status to success and advances to preflight-review", async () => {
|
|
354
|
-
const validateResponse: ValidateResponse = {
|
|
355
|
-
is_valid: true,
|
|
356
|
-
errors: [],
|
|
357
|
-
manifest: {
|
|
358
|
-
schema_version: 1,
|
|
359
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
360
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
361
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
362
|
-
origin: { mode: "self-hosted-local" },
|
|
363
|
-
compatibility: {
|
|
364
|
-
min_runtime_version: "0.0.0-test",
|
|
365
|
-
max_runtime_version: null,
|
|
366
|
-
},
|
|
367
|
-
contents: [
|
|
368
|
-
{ path: "data/db/assistant.db", sha256: "abc", size_bytes: 100 },
|
|
369
|
-
],
|
|
370
|
-
checksum: "def",
|
|
371
|
-
secrets_redacted: false,
|
|
372
|
-
export_options: {
|
|
373
|
-
include_logs: false,
|
|
374
|
-
include_browser_state: false,
|
|
375
|
-
include_memory_vectors: false,
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const state = advanceTo("validate");
|
|
381
|
-
const options = makeExecutorOptions({
|
|
382
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(200, validateResponse) }),
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const result = await executeValidateStep(state, options);
|
|
386
|
-
expect(result.steps.validate.status).toBe("success");
|
|
387
|
-
expect(result.currentStep).toBe("preflight-review");
|
|
388
|
-
expect(result.validateResult).toBeDefined();
|
|
389
|
-
if (result.validateResult && result.validateResult.is_valid) {
|
|
390
|
-
expect(result.validateResult.manifest.schema_version).toBe(1);
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test("validation failure — sets status to error with retryable flag", async () => {
|
|
395
|
-
const validateResponse: ValidateResponse = {
|
|
396
|
-
is_valid: false,
|
|
397
|
-
errors: [{ code: "INVALID_GZIP", message: "Not a valid gzip file" }],
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
const state = advanceTo("validate");
|
|
401
|
-
const options = makeExecutorOptions({
|
|
402
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(200, validateResponse) }),
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
const result = await executeValidateStep(state, options);
|
|
406
|
-
expect(result.steps.validate.status).toBe("error");
|
|
407
|
-
expect(result.steps.validate.error?.retryable).toBe(true);
|
|
408
|
-
expect(result.steps.validate.error?.message).toContain(
|
|
409
|
-
"Not a valid gzip file",
|
|
410
|
-
);
|
|
411
|
-
expect(result.currentStep).toBe("validate"); // Does not advance
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("transport error — maps HTTP error to step error", async () => {
|
|
415
|
-
const state = advanceTo("validate");
|
|
416
|
-
const options = makeExecutorOptions({
|
|
417
|
-
destConfig: runtimeConfig({
|
|
418
|
-
fetchFn: mockFetch(500, "Internal Server Error"),
|
|
419
|
-
}),
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const result = await executeValidateStep(state, options);
|
|
423
|
-
expect(result.steps.validate.status).toBe("error");
|
|
424
|
-
expect(result.steps.validate.error?.code).toBe("HTTP_500");
|
|
425
|
-
expect(result.steps.validate.error?.retryable).toBe(true);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("non-retryable error — 400 is not retryable", async () => {
|
|
429
|
-
const state = advanceTo("validate");
|
|
430
|
-
const options = makeExecutorOptions({
|
|
431
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(400, "Bad Request") }),
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const result = await executeValidateStep(state, options);
|
|
435
|
-
expect(result.steps.validate.status).toBe("error");
|
|
436
|
-
expect(result.steps.validate.error?.retryable).toBe(false);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("429 rate limit error is retryable", async () => {
|
|
440
|
-
const state = advanceTo("validate");
|
|
441
|
-
const options = makeExecutorOptions({
|
|
442
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(429, "Rate Limited") }),
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
const result = await executeValidateStep(state, options);
|
|
446
|
-
expect(result.steps.validate.status).toBe("error");
|
|
447
|
-
expect(result.steps.validate.error?.retryable).toBe(true);
|
|
448
|
-
expect(result.steps.validate.error?.code).toBe("HTTP_429");
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
test("loading state is set before async operation", async () => {
|
|
452
|
-
const stateChanges: MigrationWizardState[] = [];
|
|
453
|
-
const state = advanceTo("validate");
|
|
454
|
-
const options = makeExecutorOptions({
|
|
455
|
-
destConfig: runtimeConfig({
|
|
456
|
-
fetchFn: mockFetch(200, { is_valid: true, errors: [], manifest: {} }),
|
|
457
|
-
}),
|
|
458
|
-
onStateChange: (s) => stateChanges.push(s),
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
await executeValidateStep(state, options);
|
|
462
|
-
expect(stateChanges.length).toBeGreaterThanOrEqual(2);
|
|
463
|
-
expect(stateChanges[0].steps.validate.status).toBe("loading");
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test("throws when called from wrong step", async () => {
|
|
467
|
-
const state = advanceTo("upload-bundle");
|
|
468
|
-
const options = makeExecutorOptions();
|
|
469
|
-
try {
|
|
470
|
-
await executeValidateStep(state, options);
|
|
471
|
-
expect(true).toBe(false);
|
|
472
|
-
} catch (err) {
|
|
473
|
-
expect((err as Error).message).toContain("wrong step");
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// ---------------------------------------------------------------------------
|
|
479
|
-
// executePreflightStep
|
|
480
|
-
// ---------------------------------------------------------------------------
|
|
481
|
-
|
|
482
|
-
describe("executePreflightStep", () => {
|
|
483
|
-
test("success — sets status to success and advances to transfer", async () => {
|
|
484
|
-
const preflightResponse: ImportPreflightResponse = {
|
|
485
|
-
can_import: true,
|
|
486
|
-
summary: {
|
|
487
|
-
total_files: 2,
|
|
488
|
-
files_to_create: 1,
|
|
489
|
-
files_to_overwrite: 1,
|
|
490
|
-
files_unchanged: 0,
|
|
491
|
-
files_to_skip: 0,
|
|
492
|
-
},
|
|
493
|
-
files: [
|
|
494
|
-
{
|
|
495
|
-
path: "data/db/assistant.db",
|
|
496
|
-
action: "overwrite",
|
|
497
|
-
bundle_size: 1024,
|
|
498
|
-
current_size: 512,
|
|
499
|
-
bundle_sha256: "abc",
|
|
500
|
-
current_sha256: "def",
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
conflicts: [],
|
|
504
|
-
manifest: {
|
|
505
|
-
schema_version: 1,
|
|
506
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
507
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
508
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
509
|
-
origin: { mode: "self-hosted-local" },
|
|
510
|
-
compatibility: {
|
|
511
|
-
min_runtime_version: "0.0.0-test",
|
|
512
|
-
max_runtime_version: null,
|
|
513
|
-
},
|
|
514
|
-
contents: [],
|
|
515
|
-
checksum: "ghi",
|
|
516
|
-
secrets_redacted: false,
|
|
517
|
-
export_options: {
|
|
518
|
-
include_logs: false,
|
|
519
|
-
include_browser_state: false,
|
|
520
|
-
include_memory_vectors: false,
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const state = advanceTo("preflight-review");
|
|
526
|
-
const options = makeExecutorOptions({
|
|
527
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(200, preflightResponse) }),
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
const result = await executePreflightStep(state, options);
|
|
531
|
-
expect(result.steps["preflight-review"].status).toBe("success");
|
|
532
|
-
expect(result.currentStep).toBe("transfer");
|
|
533
|
-
expect(result.preflightResult).toBeDefined();
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test("validation failure — sets error state", async () => {
|
|
537
|
-
const preflightResponse: ImportPreflightResponse = {
|
|
538
|
-
can_import: false,
|
|
539
|
-
validation: {
|
|
540
|
-
is_valid: false,
|
|
541
|
-
errors: [{ code: "SCHEMA_MISMATCH", message: "Incompatible schema" }],
|
|
542
|
-
},
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
const state = advanceTo("preflight-review");
|
|
546
|
-
const options = makeExecutorOptions({
|
|
547
|
-
destConfig: runtimeConfig({ fetchFn: mockFetch(200, preflightResponse) }),
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
const result = await executePreflightStep(state, options);
|
|
551
|
-
expect(result.steps["preflight-review"].status).toBe("error");
|
|
552
|
-
expect(result.steps["preflight-review"].error?.retryable).toBe(true);
|
|
553
|
-
expect(result.currentStep).toBe("preflight-review");
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
test("transport error is handled", async () => {
|
|
557
|
-
const state = advanceTo("preflight-review");
|
|
558
|
-
const options = makeExecutorOptions({
|
|
559
|
-
destConfig: runtimeConfig({
|
|
560
|
-
fetchFn: mockFetch(503, "Service Unavailable"),
|
|
561
|
-
}),
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
const result = await executePreflightStep(state, options);
|
|
565
|
-
expect(result.steps["preflight-review"].status).toBe("error");
|
|
566
|
-
expect(result.steps["preflight-review"].error?.retryable).toBe(true);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("throws when called from wrong step", async () => {
|
|
570
|
-
const state = advanceTo("validate");
|
|
571
|
-
const options = makeExecutorOptions();
|
|
572
|
-
try {
|
|
573
|
-
await executePreflightStep(state, options);
|
|
574
|
-
expect(true).toBe(false);
|
|
575
|
-
} catch (err) {
|
|
576
|
-
expect((err as Error).message).toContain("wrong step");
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// ---------------------------------------------------------------------------
|
|
582
|
-
// executeTransferStep
|
|
583
|
-
// ---------------------------------------------------------------------------
|
|
584
|
-
|
|
585
|
-
describe("executeTransferStep", () => {
|
|
586
|
-
test("import-only — success", async () => {
|
|
587
|
-
const importResponse: ImportCommitResponse = {
|
|
588
|
-
success: true,
|
|
589
|
-
summary: {
|
|
590
|
-
total_files: 1,
|
|
591
|
-
files_created: 1,
|
|
592
|
-
files_overwritten: 0,
|
|
593
|
-
files_skipped: 0,
|
|
594
|
-
backups_created: 0,
|
|
595
|
-
},
|
|
596
|
-
files: [
|
|
597
|
-
{
|
|
598
|
-
path: "data/db/assistant.db",
|
|
599
|
-
disk_path: "/home/.cadet/data/db/assistant.db",
|
|
600
|
-
action: "created",
|
|
601
|
-
size: 64,
|
|
602
|
-
sha256: "xyz",
|
|
603
|
-
backup_path: null,
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
manifest: {
|
|
607
|
-
schema_version: 1,
|
|
608
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
609
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
610
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
611
|
-
origin: { mode: "self-hosted-local" },
|
|
612
|
-
compatibility: {
|
|
613
|
-
min_runtime_version: "0.0.0-test",
|
|
614
|
-
max_runtime_version: null,
|
|
615
|
-
},
|
|
616
|
-
contents: [],
|
|
617
|
-
checksum: "abc",
|
|
618
|
-
secrets_redacted: false,
|
|
619
|
-
export_options: {
|
|
620
|
-
include_logs: false,
|
|
621
|
-
include_browser_state: false,
|
|
622
|
-
include_memory_vectors: false,
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
warnings: [],
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
let importCalled = false;
|
|
629
|
-
|
|
630
|
-
const destFetch = (async () => {
|
|
631
|
-
importCalled = true;
|
|
632
|
-
return new Response(JSON.stringify(importResponse), {
|
|
633
|
-
status: 200,
|
|
634
|
-
headers: { "Content-Type": "application/json" },
|
|
635
|
-
});
|
|
636
|
-
}) as unknown as typeof fetch;
|
|
637
|
-
|
|
638
|
-
const state = advanceTo("transfer");
|
|
639
|
-
const options = makeExecutorOptions({
|
|
640
|
-
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
const result = await executeTransferStep(state, options);
|
|
644
|
-
expect(importCalled).toBe(true);
|
|
645
|
-
expect(result.steps.transfer.status).toBe("success");
|
|
646
|
-
expect(result.currentStep).toBe("rebind-secrets");
|
|
647
|
-
expect(result.importResult).toBeDefined();
|
|
648
|
-
if (result.importResult && result.importResult.success) {
|
|
649
|
-
expect(result.importResult.summary.files_created).toBe(1);
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("managed export + poll + import — success", async () => {
|
|
654
|
-
let fetchCallCount = 0;
|
|
655
|
-
const importResponse: ImportCommitResponse = {
|
|
656
|
-
success: true,
|
|
657
|
-
summary: {
|
|
658
|
-
total_files: 1,
|
|
659
|
-
files_created: 1,
|
|
660
|
-
files_overwritten: 0,
|
|
661
|
-
files_skipped: 0,
|
|
662
|
-
backups_created: 0,
|
|
663
|
-
},
|
|
664
|
-
files: [],
|
|
665
|
-
manifest: {
|
|
666
|
-
schema_version: 1,
|
|
667
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
668
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
669
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
670
|
-
origin: { mode: "self-hosted-local" },
|
|
671
|
-
compatibility: {
|
|
672
|
-
min_runtime_version: "0.0.0-test",
|
|
673
|
-
max_runtime_version: null,
|
|
674
|
-
},
|
|
675
|
-
contents: [],
|
|
676
|
-
checksum: "abc",
|
|
677
|
-
secrets_redacted: false,
|
|
678
|
-
export_options: {
|
|
679
|
-
include_logs: false,
|
|
680
|
-
include_browser_state: false,
|
|
681
|
-
include_memory_vectors: false,
|
|
682
|
-
},
|
|
683
|
-
},
|
|
684
|
-
warnings: [],
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
const sourceFetch = (async (url: string | URL | Request) => {
|
|
688
|
-
fetchCallCount++;
|
|
689
|
-
const urlStr = String(url);
|
|
690
|
-
if (urlStr.endsWith("/export/")) {
|
|
691
|
-
return new Response(
|
|
692
|
-
JSON.stringify({ job_id: "exp-1", status: "pending" }),
|
|
693
|
-
{
|
|
694
|
-
status: 200,
|
|
695
|
-
headers: { "Content-Type": "application/json" },
|
|
696
|
-
},
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
if (urlStr.includes("/export/") && urlStr.includes("/status/")) {
|
|
700
|
-
if (fetchCallCount <= 3) {
|
|
701
|
-
return new Response(
|
|
702
|
-
JSON.stringify({ status: "processing", job_id: "exp-1" }),
|
|
703
|
-
{
|
|
704
|
-
status: 200,
|
|
705
|
-
headers: { "Content-Type": "application/json" },
|
|
706
|
-
},
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
return new Response(
|
|
710
|
-
JSON.stringify({
|
|
711
|
-
status: "complete",
|
|
712
|
-
job_id: "exp-1",
|
|
713
|
-
download_url: "https://cdn.example.com/export.vbundle",
|
|
714
|
-
}),
|
|
715
|
-
{
|
|
716
|
-
status: 200,
|
|
717
|
-
headers: { "Content-Type": "application/json" },
|
|
718
|
-
},
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
if (urlStr === "https://cdn.example.com/export.vbundle") {
|
|
722
|
-
return new Response(new ArrayBuffer(32), { status: 200 });
|
|
723
|
-
}
|
|
724
|
-
return new Response("Not found", { status: 404 });
|
|
725
|
-
}) as unknown as typeof fetch;
|
|
726
|
-
|
|
727
|
-
const destFetch = (async () => {
|
|
728
|
-
return new Response(JSON.stringify(importResponse), {
|
|
729
|
-
status: 200,
|
|
730
|
-
headers: { "Content-Type": "application/json" },
|
|
731
|
-
});
|
|
732
|
-
}) as unknown as typeof fetch;
|
|
733
|
-
|
|
734
|
-
const state = advanceTo("transfer");
|
|
735
|
-
const options = makeExecutorOptions({
|
|
736
|
-
sourceConfig: managedConfig({ fetchFn: sourceFetch }),
|
|
737
|
-
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const result = await executeTransferStep(state, options);
|
|
741
|
-
expect(result.steps.transfer.status).toBe("success");
|
|
742
|
-
expect(result.currentStep).toBe("rebind-secrets");
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
test("export failure — sets error", async () => {
|
|
746
|
-
const state = advanceTo("transfer");
|
|
747
|
-
const options = makeExecutorOptions({
|
|
748
|
-
sourceConfig: runtimeConfig({ fetchFn: mockFetch(500, "Server Error") }),
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
const result = await executeTransferStep(state, options);
|
|
752
|
-
expect(result.steps.transfer.status).toBe("error");
|
|
753
|
-
expect(result.steps.transfer.error?.retryable).toBe(true);
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
test("import failure — sets error with reason", async () => {
|
|
757
|
-
const archiveBytes = new ArrayBuffer(32);
|
|
758
|
-
const importResponse: ImportCommitResponse = {
|
|
759
|
-
success: false,
|
|
760
|
-
reason: "write_failed",
|
|
761
|
-
message: "Disk full",
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
const sourceFetch = (async () => {
|
|
765
|
-
return new Response(archiveBytes, {
|
|
766
|
-
status: 200,
|
|
767
|
-
headers: {
|
|
768
|
-
"Content-Disposition": 'attachment; filename="export.vbundle"',
|
|
769
|
-
"X-Vbundle-Schema-Version": "1",
|
|
770
|
-
"X-Vbundle-Manifest-Sha256": "abc",
|
|
771
|
-
},
|
|
772
|
-
});
|
|
773
|
-
}) as unknown as typeof fetch;
|
|
774
|
-
|
|
775
|
-
const destFetch = (async () => {
|
|
776
|
-
return new Response(JSON.stringify(importResponse), {
|
|
777
|
-
status: 200,
|
|
778
|
-
headers: { "Content-Type": "application/json" },
|
|
779
|
-
});
|
|
780
|
-
}) as unknown as typeof fetch;
|
|
781
|
-
|
|
782
|
-
const state = advanceTo("transfer");
|
|
783
|
-
const options = makeExecutorOptions({
|
|
784
|
-
sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
|
|
785
|
-
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
const result = await executeTransferStep(state, options);
|
|
789
|
-
expect(result.steps.transfer.status).toBe("error");
|
|
790
|
-
expect(result.steps.transfer.error?.message).toContain("Disk full");
|
|
791
|
-
expect(result.steps.transfer.error?.retryable).toBe(true);
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
test("import failure — sets error with validation_failed reason", async () => {
|
|
795
|
-
const failResponse: ImportCommitResponse = {
|
|
796
|
-
success: false,
|
|
797
|
-
reason: "validation_failed",
|
|
798
|
-
message: "Manifest SHA mismatch",
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
const destFetch = (async () => {
|
|
802
|
-
return new Response(JSON.stringify(failResponse), {
|
|
803
|
-
status: 200,
|
|
804
|
-
headers: { "Content-Type": "application/json" },
|
|
805
|
-
});
|
|
806
|
-
}) as unknown as typeof fetch;
|
|
807
|
-
|
|
808
|
-
const state = advanceTo("transfer");
|
|
809
|
-
const options = makeExecutorOptions({
|
|
810
|
-
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const result = await executeTransferStep(state, options);
|
|
814
|
-
expect(result.steps.transfer.status).toBe("error");
|
|
815
|
-
expect(result.steps.transfer.error?.code).toBe("validation_failed");
|
|
816
|
-
expect(result.steps.transfer.error?.retryable).toBe(true);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test("throws when called from wrong step", async () => {
|
|
820
|
-
const state = advanceTo("validate");
|
|
821
|
-
const options = makeExecutorOptions();
|
|
822
|
-
try {
|
|
823
|
-
await executeTransferStep(state, options);
|
|
824
|
-
expect(true).toBe(false);
|
|
825
|
-
} catch (err) {
|
|
826
|
-
expect((err as Error).message).toContain("wrong step");
|
|
827
|
-
}
|
|
828
|
-
});
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
// ---------------------------------------------------------------------------
|
|
832
|
-
// completeRebindSecrets
|
|
833
|
-
// ---------------------------------------------------------------------------
|
|
834
|
-
|
|
835
|
-
describe("completeRebindSecrets", () => {
|
|
836
|
-
test("advances to complete", () => {
|
|
837
|
-
const state = advanceTo("rebind-secrets");
|
|
838
|
-
const result = completeRebindSecrets(state);
|
|
839
|
-
expect(result.currentStep).toBe("complete");
|
|
840
|
-
expect(result.steps["rebind-secrets"].status).toBe("success");
|
|
841
|
-
expect(result.steps.complete.status).toBe("success");
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test("throws when called from wrong step", () => {
|
|
845
|
-
const state = advanceTo("transfer");
|
|
846
|
-
expect(() => completeRebindSecrets(state)).toThrow("wrong step");
|
|
847
|
-
});
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
// ---------------------------------------------------------------------------
|
|
851
|
-
// Retry
|
|
852
|
-
// ---------------------------------------------------------------------------
|
|
853
|
-
|
|
854
|
-
describe("retry", () => {
|
|
855
|
-
test("canRetryCurrentStep returns true for retryable errors", () => {
|
|
856
|
-
let state = advanceTo("validate");
|
|
857
|
-
state = {
|
|
858
|
-
...state,
|
|
859
|
-
steps: {
|
|
860
|
-
...state.steps,
|
|
861
|
-
validate: {
|
|
862
|
-
status: "error",
|
|
863
|
-
error: { message: "Server error", code: "HTTP_500", retryable: true },
|
|
864
|
-
},
|
|
865
|
-
},
|
|
866
|
-
};
|
|
867
|
-
expect(canRetryCurrentStep(state)).toBe(true);
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
test("canRetryCurrentStep returns false for non-retryable errors", () => {
|
|
871
|
-
let state = advanceTo("validate");
|
|
872
|
-
state = {
|
|
873
|
-
...state,
|
|
874
|
-
steps: {
|
|
875
|
-
...state.steps,
|
|
876
|
-
validate: {
|
|
877
|
-
status: "error",
|
|
878
|
-
error: { message: "Bad request", code: "HTTP_400", retryable: false },
|
|
879
|
-
},
|
|
880
|
-
},
|
|
881
|
-
};
|
|
882
|
-
expect(canRetryCurrentStep(state)).toBe(false);
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
test("canRetryCurrentStep returns false for non-error states", () => {
|
|
886
|
-
const state = advanceTo("validate");
|
|
887
|
-
expect(canRetryCurrentStep(state)).toBe(false);
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
test("resetStepForRetry sets step back to idle", () => {
|
|
891
|
-
let state = advanceTo("validate");
|
|
892
|
-
state = {
|
|
893
|
-
...state,
|
|
894
|
-
steps: {
|
|
895
|
-
...state.steps,
|
|
896
|
-
validate: {
|
|
897
|
-
status: "error",
|
|
898
|
-
error: { message: "Server error", retryable: true },
|
|
899
|
-
},
|
|
900
|
-
},
|
|
901
|
-
};
|
|
902
|
-
const reset = resetStepForRetry(state);
|
|
903
|
-
expect(reset.steps.validate.status).toBe("idle");
|
|
904
|
-
expect(reset.steps.validate.error).toBeUndefined();
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
test("resetStepForRetry throws for non-retryable step", () => {
|
|
908
|
-
const state = advanceTo("validate");
|
|
909
|
-
expect(() => resetStepForRetry(state)).toThrow(
|
|
910
|
-
"not in a retryable error state",
|
|
911
|
-
);
|
|
912
|
-
});
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
// ---------------------------------------------------------------------------
|
|
916
|
-
// Persistence
|
|
917
|
-
// ---------------------------------------------------------------------------
|
|
918
|
-
|
|
919
|
-
describe("persistence", () => {
|
|
920
|
-
describe("serializeWizardState", () => {
|
|
921
|
-
test("produces valid JSON", () => {
|
|
922
|
-
const state = advanceTo("validate");
|
|
923
|
-
const json = serializeWizardState(state);
|
|
924
|
-
const parsed = JSON.parse(json);
|
|
925
|
-
expect(parsed.currentStep).toBe("validate");
|
|
926
|
-
expect(parsed.direction).toBe("managed-to-self-hosted");
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
test("round-trips through serialize/deserialize", () => {
|
|
930
|
-
const state = advanceTo("transfer");
|
|
931
|
-
const json = serializeWizardState(state);
|
|
932
|
-
const restored = deserializeWizardState(json);
|
|
933
|
-
expect(restored).toBeDefined();
|
|
934
|
-
expect(restored!.currentStep).toBe("transfer");
|
|
935
|
-
expect(restored!.direction).toBe("managed-to-self-hosted");
|
|
936
|
-
expect(restored!.steps["select-direction"].status).toBe("success");
|
|
937
|
-
});
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
describe("deserializeWizardState", () => {
|
|
941
|
-
test("returns undefined for invalid JSON", () => {
|
|
942
|
-
expect(deserializeWizardState("not json")).toBeUndefined();
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
test("returns undefined for missing required fields", () => {
|
|
946
|
-
expect(deserializeWizardState("{}")).toBeUndefined();
|
|
947
|
-
expect(
|
|
948
|
-
deserializeWizardState('{"currentStep":"validate"}'),
|
|
949
|
-
).toBeUndefined();
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
test("returns undefined for unknown step", () => {
|
|
953
|
-
const state = createWizardState();
|
|
954
|
-
const json = serializeWizardState(state);
|
|
955
|
-
const modified = JSON.parse(json);
|
|
956
|
-
modified.currentStep = "unknown-step";
|
|
957
|
-
expect(deserializeWizardState(JSON.stringify(modified))).toBeUndefined();
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
test("returns undefined when a step is missing from steps record", () => {
|
|
961
|
-
const state = createWizardState();
|
|
962
|
-
const json = serializeWizardState(state);
|
|
963
|
-
const modified = JSON.parse(json);
|
|
964
|
-
delete modified.steps["validate"];
|
|
965
|
-
expect(deserializeWizardState(JSON.stringify(modified))).toBeUndefined();
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
test("clears hasBundleData on deserialization", () => {
|
|
969
|
-
let state = advanceTo("validate");
|
|
970
|
-
state = { ...state, hasBundleData: true };
|
|
971
|
-
const json = serializeWizardState(state);
|
|
972
|
-
const restored = deserializeWizardState(json);
|
|
973
|
-
expect(restored!.hasBundleData).toBe(false);
|
|
974
|
-
});
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
describe("isResumable", () => {
|
|
978
|
-
test("not resumable on first step", () => {
|
|
979
|
-
const state = createWizardState();
|
|
980
|
-
expect(isResumable(state)).toBe(false);
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
test("not resumable without direction", () => {
|
|
984
|
-
const state = createWizardState();
|
|
985
|
-
expect(isResumable(state)).toBe(false);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
test("not resumable when complete", () => {
|
|
989
|
-
const state = advanceTo("complete");
|
|
990
|
-
expect(isResumable(state)).toBe(false);
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
test("resumable at validate step with direction set", () => {
|
|
994
|
-
const state = advanceTo("validate");
|
|
995
|
-
expect(isResumable(state)).toBe(true);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
test("resumable at transfer step", () => {
|
|
999
|
-
const state = advanceTo("transfer");
|
|
1000
|
-
expect(isResumable(state)).toBe(true);
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
describe("prepareForResume", () => {
|
|
1005
|
-
test("resets loading step to idle", () => {
|
|
1006
|
-
let state = advanceTo("validate");
|
|
1007
|
-
state = {
|
|
1008
|
-
...state,
|
|
1009
|
-
steps: { ...state.steps, validate: { status: "loading" } },
|
|
1010
|
-
};
|
|
1011
|
-
const prepared = prepareForResume(state);
|
|
1012
|
-
expect(prepared.steps.validate.status).toBe("idle");
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
test("goes back to upload-bundle when bundle data is missing", () => {
|
|
1016
|
-
let state = advanceTo("validate");
|
|
1017
|
-
state = { ...state, hasBundleData: false };
|
|
1018
|
-
const prepared = prepareForResume(state);
|
|
1019
|
-
expect(prepared.currentStep).toBe("upload-bundle");
|
|
1020
|
-
expect(prepared.steps["upload-bundle"].status).toBe("idle");
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
test("preserves step when bundle data is available", () => {
|
|
1024
|
-
let state = advanceTo("validate");
|
|
1025
|
-
state = { ...state, hasBundleData: true };
|
|
1026
|
-
const prepared = prepareForResume(state);
|
|
1027
|
-
expect(prepared.currentStep).toBe("validate");
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test("does not change error steps", () => {
|
|
1031
|
-
let state = advanceTo("validate");
|
|
1032
|
-
state = {
|
|
1033
|
-
...state,
|
|
1034
|
-
hasBundleData: true,
|
|
1035
|
-
steps: {
|
|
1036
|
-
...state.steps,
|
|
1037
|
-
validate: {
|
|
1038
|
-
status: "error",
|
|
1039
|
-
error: { message: "Failed", retryable: true },
|
|
1040
|
-
},
|
|
1041
|
-
},
|
|
1042
|
-
};
|
|
1043
|
-
const prepared = prepareForResume(state);
|
|
1044
|
-
expect(prepared.steps.validate.status).toBe("error");
|
|
1045
|
-
expect(prepared.currentStep).toBe("validate");
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// ---------------------------------------------------------------------------
|
|
1051
|
-
// Query helpers
|
|
1052
|
-
// ---------------------------------------------------------------------------
|
|
1053
|
-
|
|
1054
|
-
describe("query helpers", () => {
|
|
1055
|
-
test("getStepOrder returns all steps", () => {
|
|
1056
|
-
const state = advanceTo("validate");
|
|
1057
|
-
const order = getStepOrder(state);
|
|
1058
|
-
expect(order).toHaveLength(7);
|
|
1059
|
-
expect(order[0]).toBe("select-direction");
|
|
1060
|
-
expect(order[6]).toBe("complete");
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
test("getCurrentStepIndex returns correct index", () => {
|
|
1064
|
-
expect(getCurrentStepIndex(createWizardState())).toBe(0);
|
|
1065
|
-
expect(getCurrentStepIndex(advanceTo("validate"))).toBe(2);
|
|
1066
|
-
expect(getCurrentStepIndex(advanceTo("transfer"))).toBe(4);
|
|
1067
|
-
expect(getCurrentStepIndex(advanceTo("complete"))).toBe(6);
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
test("getTotalSteps returns 7", () => {
|
|
1071
|
-
expect(getTotalSteps()).toBe(7);
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
test("isStepAccessible checks transition validity", () => {
|
|
1075
|
-
const state = advanceTo("validate");
|
|
1076
|
-
expect(isStepAccessible(state, "select-direction")).toBe(true);
|
|
1077
|
-
expect(isStepAccessible(state, "upload-bundle")).toBe(true);
|
|
1078
|
-
expect(isStepAccessible(state, "validate")).toBe(true);
|
|
1079
|
-
// Cannot advance because validate is not success
|
|
1080
|
-
expect(isStepAccessible(state, "preflight-review")).toBe(false);
|
|
1081
|
-
expect(isStepAccessible(state, "transfer")).toBe(false);
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
test("isWizardComplete returns true only when complete", () => {
|
|
1085
|
-
expect(isWizardComplete(createWizardState())).toBe(false);
|
|
1086
|
-
expect(isWizardComplete(advanceTo("validate"))).toBe(false);
|
|
1087
|
-
expect(isWizardComplete(advanceTo("complete"))).toBe(true);
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
// ---------------------------------------------------------------------------
|
|
1092
|
-
// Full flow end-to-end
|
|
1093
|
-
// ---------------------------------------------------------------------------
|
|
1094
|
-
|
|
1095
|
-
describe("full wizard flow", () => {
|
|
1096
|
-
test("managed-to-self-hosted: select → upload → validate → preflight → transfer → rebind → complete", async () => {
|
|
1097
|
-
// Step 1: Select direction
|
|
1098
|
-
let state = createWizardState();
|
|
1099
|
-
state = selectDirection(state, "managed-to-self-hosted");
|
|
1100
|
-
expect(state.currentStep).toBe("upload-bundle");
|
|
1101
|
-
|
|
1102
|
-
// Step 2: Upload bundle
|
|
1103
|
-
state = setBundleUploaded(state);
|
|
1104
|
-
expect(state.currentStep).toBe("validate");
|
|
1105
|
-
|
|
1106
|
-
// Step 3: Validate
|
|
1107
|
-
const validateResponse: ValidateResponse = {
|
|
1108
|
-
is_valid: true,
|
|
1109
|
-
errors: [],
|
|
1110
|
-
manifest: {
|
|
1111
|
-
schema_version: 1,
|
|
1112
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
1113
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
1114
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
1115
|
-
origin: { mode: "self-hosted-local" },
|
|
1116
|
-
compatibility: {
|
|
1117
|
-
min_runtime_version: "0.0.0-test",
|
|
1118
|
-
max_runtime_version: null,
|
|
1119
|
-
},
|
|
1120
|
-
contents: [],
|
|
1121
|
-
checksum: "abc",
|
|
1122
|
-
secrets_redacted: false,
|
|
1123
|
-
export_options: {
|
|
1124
|
-
include_logs: false,
|
|
1125
|
-
include_browser_state: false,
|
|
1126
|
-
include_memory_vectors: false,
|
|
1127
|
-
},
|
|
1128
|
-
},
|
|
1129
|
-
};
|
|
1130
|
-
state = await executeValidateStep(
|
|
1131
|
-
state,
|
|
1132
|
-
makeExecutorOptions({
|
|
1133
|
-
destConfig: runtimeConfig({
|
|
1134
|
-
fetchFn: mockFetch(200, validateResponse),
|
|
1135
|
-
}),
|
|
1136
|
-
}),
|
|
1137
|
-
);
|
|
1138
|
-
expect(state.currentStep).toBe("preflight-review");
|
|
1139
|
-
|
|
1140
|
-
// Step 4: Preflight review
|
|
1141
|
-
const preflightResponse: ImportPreflightResponse = {
|
|
1142
|
-
can_import: true,
|
|
1143
|
-
summary: {
|
|
1144
|
-
total_files: 1,
|
|
1145
|
-
files_to_create: 1,
|
|
1146
|
-
files_to_overwrite: 0,
|
|
1147
|
-
files_unchanged: 0,
|
|
1148
|
-
files_to_skip: 0,
|
|
1149
|
-
},
|
|
1150
|
-
files: [],
|
|
1151
|
-
conflicts: [],
|
|
1152
|
-
manifest: {
|
|
1153
|
-
schema_version: 1,
|
|
1154
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
1155
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
1156
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
1157
|
-
origin: { mode: "self-hosted-local" },
|
|
1158
|
-
compatibility: {
|
|
1159
|
-
min_runtime_version: "0.0.0-test",
|
|
1160
|
-
max_runtime_version: null,
|
|
1161
|
-
},
|
|
1162
|
-
contents: [],
|
|
1163
|
-
checksum: "abc",
|
|
1164
|
-
secrets_redacted: false,
|
|
1165
|
-
export_options: {
|
|
1166
|
-
include_logs: false,
|
|
1167
|
-
include_browser_state: false,
|
|
1168
|
-
include_memory_vectors: false,
|
|
1169
|
-
},
|
|
1170
|
-
},
|
|
1171
|
-
};
|
|
1172
|
-
state = await executePreflightStep(
|
|
1173
|
-
state,
|
|
1174
|
-
makeExecutorOptions({
|
|
1175
|
-
destConfig: runtimeConfig({
|
|
1176
|
-
fetchFn: mockFetch(200, preflightResponse),
|
|
1177
|
-
}),
|
|
1178
|
-
}),
|
|
1179
|
-
);
|
|
1180
|
-
expect(state.currentStep).toBe("transfer");
|
|
1181
|
-
|
|
1182
|
-
// Step 5: Transfer (runtime export + import)
|
|
1183
|
-
const importResponse: ImportCommitResponse = {
|
|
1184
|
-
success: true,
|
|
1185
|
-
summary: {
|
|
1186
|
-
total_files: 1,
|
|
1187
|
-
files_created: 1,
|
|
1188
|
-
files_overwritten: 0,
|
|
1189
|
-
files_skipped: 0,
|
|
1190
|
-
backups_created: 0,
|
|
1191
|
-
},
|
|
1192
|
-
files: [],
|
|
1193
|
-
manifest: {
|
|
1194
|
-
schema_version: 1,
|
|
1195
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
1196
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
1197
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
1198
|
-
origin: { mode: "self-hosted-local" },
|
|
1199
|
-
compatibility: {
|
|
1200
|
-
min_runtime_version: "0.0.0-test",
|
|
1201
|
-
max_runtime_version: null,
|
|
1202
|
-
},
|
|
1203
|
-
contents: [],
|
|
1204
|
-
checksum: "abc",
|
|
1205
|
-
secrets_redacted: false,
|
|
1206
|
-
export_options: {
|
|
1207
|
-
include_logs: false,
|
|
1208
|
-
include_browser_state: false,
|
|
1209
|
-
include_memory_vectors: false,
|
|
1210
|
-
},
|
|
1211
|
-
},
|
|
1212
|
-
warnings: [],
|
|
1213
|
-
};
|
|
1214
|
-
|
|
1215
|
-
const sourceFetch = (async () => {
|
|
1216
|
-
return new Response(new ArrayBuffer(32), {
|
|
1217
|
-
status: 200,
|
|
1218
|
-
headers: {
|
|
1219
|
-
"Content-Disposition": 'attachment; filename="export.vbundle"',
|
|
1220
|
-
"X-Vbundle-Schema-Version": "1",
|
|
1221
|
-
"X-Vbundle-Manifest-Sha256": "abc",
|
|
1222
|
-
},
|
|
1223
|
-
});
|
|
1224
|
-
}) as unknown as typeof fetch;
|
|
1225
|
-
|
|
1226
|
-
const destFetch = (async () => {
|
|
1227
|
-
return new Response(JSON.stringify(importResponse), {
|
|
1228
|
-
status: 200,
|
|
1229
|
-
headers: { "Content-Type": "application/json" },
|
|
1230
|
-
});
|
|
1231
|
-
}) as unknown as typeof fetch;
|
|
1232
|
-
|
|
1233
|
-
state = await executeTransferStep(
|
|
1234
|
-
state,
|
|
1235
|
-
makeExecutorOptions({
|
|
1236
|
-
sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
|
|
1237
|
-
destConfig: runtimeConfig({ fetchFn: destFetch }),
|
|
1238
|
-
}),
|
|
1239
|
-
);
|
|
1240
|
-
expect(state.currentStep).toBe("rebind-secrets");
|
|
1241
|
-
|
|
1242
|
-
// Step 6: Rebind secrets
|
|
1243
|
-
state = completeRebindSecrets(state);
|
|
1244
|
-
expect(state.currentStep).toBe("complete");
|
|
1245
|
-
expect(isWizardComplete(state)).toBe(true);
|
|
1246
|
-
|
|
1247
|
-
// Verify all steps are in success state
|
|
1248
|
-
for (const step of getStepOrder(state)) {
|
|
1249
|
-
expect(state.steps[step].status).toBe("success");
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
test("error → retry → success flow", async () => {
|
|
1254
|
-
let state = advanceTo("validate");
|
|
1255
|
-
|
|
1256
|
-
// First attempt fails
|
|
1257
|
-
state = await executeValidateStep(
|
|
1258
|
-
state,
|
|
1259
|
-
makeExecutorOptions({
|
|
1260
|
-
destConfig: runtimeConfig({
|
|
1261
|
-
fetchFn: mockFetch(503, "Service Unavailable"),
|
|
1262
|
-
}),
|
|
1263
|
-
}),
|
|
1264
|
-
);
|
|
1265
|
-
expect(state.steps.validate.status).toBe("error");
|
|
1266
|
-
expect(canRetryCurrentStep(state)).toBe(true);
|
|
1267
|
-
|
|
1268
|
-
// Reset for retry
|
|
1269
|
-
state = resetStepForRetry(state);
|
|
1270
|
-
expect(state.steps.validate.status).toBe("idle");
|
|
1271
|
-
|
|
1272
|
-
// Second attempt succeeds
|
|
1273
|
-
const validateResponse: ValidateResponse = {
|
|
1274
|
-
is_valid: true,
|
|
1275
|
-
errors: [],
|
|
1276
|
-
manifest: {
|
|
1277
|
-
schema_version: 1,
|
|
1278
|
-
bundle_id: "00000000-0000-4000-8000-000000000000",
|
|
1279
|
-
created_at: "2026-03-01T00:00:00Z",
|
|
1280
|
-
assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
|
|
1281
|
-
origin: { mode: "self-hosted-local" },
|
|
1282
|
-
compatibility: {
|
|
1283
|
-
min_runtime_version: "0.0.0-test",
|
|
1284
|
-
max_runtime_version: null,
|
|
1285
|
-
},
|
|
1286
|
-
contents: [],
|
|
1287
|
-
checksum: "abc",
|
|
1288
|
-
secrets_redacted: false,
|
|
1289
|
-
export_options: {
|
|
1290
|
-
include_logs: false,
|
|
1291
|
-
include_browser_state: false,
|
|
1292
|
-
include_memory_vectors: false,
|
|
1293
|
-
},
|
|
1294
|
-
},
|
|
1295
|
-
};
|
|
1296
|
-
state = await executeValidateStep(
|
|
1297
|
-
state,
|
|
1298
|
-
makeExecutorOptions({
|
|
1299
|
-
destConfig: runtimeConfig({
|
|
1300
|
-
fetchFn: mockFetch(200, validateResponse),
|
|
1301
|
-
}),
|
|
1302
|
-
}),
|
|
1303
|
-
);
|
|
1304
|
-
expect(state.steps.validate.status).toBe("success");
|
|
1305
|
-
expect(state.currentStep).toBe("preflight-review");
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
test("persistence/resume round-trip", () => {
|
|
1309
|
-
// Build up state to transfer step
|
|
1310
|
-
let state = advanceTo("transfer");
|
|
1311
|
-
|
|
1312
|
-
// Simulate interrupted loading state
|
|
1313
|
-
state = {
|
|
1314
|
-
...state,
|
|
1315
|
-
steps: { ...state.steps, transfer: { status: "loading" } },
|
|
1316
|
-
hasBundleData: true,
|
|
1317
|
-
};
|
|
1318
|
-
|
|
1319
|
-
// Persist
|
|
1320
|
-
const json = serializeWizardState(state);
|
|
1321
|
-
|
|
1322
|
-
// Restore
|
|
1323
|
-
const restored = deserializeWizardState(json);
|
|
1324
|
-
expect(restored).toBeDefined();
|
|
1325
|
-
expect(restored!.currentStep).toBe("transfer");
|
|
1326
|
-
// Bundle data cleared on deserialization
|
|
1327
|
-
expect(restored!.hasBundleData).toBe(false);
|
|
1328
|
-
|
|
1329
|
-
// Prepare for resume — loading resets to idle, and since no bundle data,
|
|
1330
|
-
// goes back to upload-bundle
|
|
1331
|
-
const prepared = prepareForResume(restored!);
|
|
1332
|
-
expect(prepared.currentStep).toBe("upload-bundle");
|
|
1333
|
-
expect(prepared.steps.transfer.status).toBe("idle");
|
|
1334
|
-
expect(prepared.steps["upload-bundle"].status).toBe("idle");
|
|
1335
|
-
|
|
1336
|
-
// Earlier completed steps remain
|
|
1337
|
-
expect(prepared.steps["select-direction"].status).toBe("success");
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
test("go back and redo flow", () => {
|
|
1341
|
-
let state = advanceTo("transfer");
|
|
1342
|
-
|
|
1343
|
-
// Go back to validate
|
|
1344
|
-
state = goBackTo(state, "validate");
|
|
1345
|
-
expect(state.currentStep).toBe("validate");
|
|
1346
|
-
expect(state.steps.validate.status).toBe("idle");
|
|
1347
|
-
expect(state.steps["preflight-review"].status).toBe("idle");
|
|
1348
|
-
expect(state.steps.transfer.status).toBe("idle");
|
|
1349
|
-
|
|
1350
|
-
// Earlier steps remain successful
|
|
1351
|
-
expect(state.steps["select-direction"].status).toBe("success");
|
|
1352
|
-
expect(state.steps["upload-bundle"].status).toBe("success");
|
|
1353
|
-
});
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
// ---------------------------------------------------------------------------
|
|
1357
|
-
// isResumable — corrected test for complete state
|
|
1358
|
-
// ---------------------------------------------------------------------------
|
|
1359
|
-
|
|
1360
|
-
describe("isResumable edge cases", () => {
|
|
1361
|
-
test("complete state is not resumable", () => {
|
|
1362
|
-
const state = advanceTo("complete");
|
|
1363
|
-
expect(isResumable(state)).toBe(false);
|
|
1364
|
-
});
|
|
1365
|
-
|
|
1366
|
-
test("upload-bundle with direction is resumable", () => {
|
|
1367
|
-
const state = advanceTo("upload-bundle");
|
|
1368
|
-
expect(isResumable(state)).toBe(true);
|
|
1369
|
-
});
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
// ---------------------------------------------------------------------------
|
|
1373
|
-
// Disabled state (step not yet accessible)
|
|
1374
|
-
// ---------------------------------------------------------------------------
|
|
1375
|
-
|
|
1376
|
-
describe("disabled states", () => {
|
|
1377
|
-
test("steps beyond current are not accessible and effectively disabled", () => {
|
|
1378
|
-
const state = createWizardState();
|
|
1379
|
-
expect(isStepAccessible(state, "upload-bundle")).toBe(false);
|
|
1380
|
-
expect(isStepAccessible(state, "validate")).toBe(false);
|
|
1381
|
-
expect(isStepAccessible(state, "preflight-review")).toBe(false);
|
|
1382
|
-
expect(isStepAccessible(state, "transfer")).toBe(false);
|
|
1383
|
-
expect(isStepAccessible(state, "rebind-secrets")).toBe(false);
|
|
1384
|
-
expect(isStepAccessible(state, "complete")).toBe(false);
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
test("current step and earlier steps are accessible", () => {
|
|
1388
|
-
const state = advanceTo("transfer");
|
|
1389
|
-
expect(isStepAccessible(state, "select-direction")).toBe(true);
|
|
1390
|
-
expect(isStepAccessible(state, "upload-bundle")).toBe(true);
|
|
1391
|
-
expect(isStepAccessible(state, "validate")).toBe(true);
|
|
1392
|
-
expect(isStepAccessible(state, "preflight-review")).toBe(true);
|
|
1393
|
-
expect(isStepAccessible(state, "transfer")).toBe(true);
|
|
1394
|
-
});
|
|
1395
|
-
});
|