@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.
Files changed (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. 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
- });