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