@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,1217 +0,0 @@
1
- /**
2
- * Tests for the transfer progress screen view model.
3
- *
4
- * Covers:
5
- * - Disabled state: screen not accessible when earlier steps incomplete
6
- * - Exporting state: export phase in progress
7
- * - Polling state: managed async export job polling
8
- * - Importing state: import phase in progress
9
- * - Success state: full import summary with details
10
- * - Error states: retryable vs non-retryable, failed phase inference
11
- * - Retry behavior: reset and re-execute after error
12
- * - Back navigation: return to preflight-review step
13
- * - Continue action: proceed to rebind-secrets step
14
- * - Transfer flow execution: runtime (sync) and managed (async) flows
15
- * - State persistence/resume: derive correct screen state after resume
16
- * - State change callbacks: UI reactivity during transfer
17
- */
18
-
19
- import { describe, expect, test } from "bun:test";
20
-
21
- import type {
22
- ExportManagedResult,
23
- ImportCommitResponse,
24
- ImportPreflightResponse,
25
- TransportConfig,
26
- ValidateResponse,
27
- } from "../runtime/migrations/migration-transport.js";
28
- import type {
29
- MigrationWizardState,
30
- StepExecutorOptions,
31
- WizardStep,
32
- } from "../runtime/migrations/migration-wizard.js";
33
- import {
34
- completeRebindSecrets,
35
- createWizardState,
36
- deserializeWizardState,
37
- prepareForResume,
38
- selectDirection,
39
- serializeWizardState,
40
- setBundleUploaded,
41
- } from "../runtime/migrations/migration-wizard.js";
42
- import type { TransferScreenState } from "../runtime/migrations/transfer-progress-screen.js";
43
- import {
44
- deriveTransferScreenState,
45
- executeTransferFlow,
46
- goBackToPreflight,
47
- isTransferScreenAccessible,
48
- retryTransferFlow,
49
- } from "../runtime/migrations/transfer-progress-screen.js";
50
-
51
- // ---------------------------------------------------------------------------
52
- // Test helpers
53
- // ---------------------------------------------------------------------------
54
-
55
- function mockFetch(
56
- status: number,
57
- body: unknown,
58
- headers?: Record<string, string>,
59
- ): typeof fetch {
60
- return (async () => {
61
- const responseHeaders = new Headers(headers);
62
- if (
63
- typeof body === "object" &&
64
- body !== undefined &&
65
- !(body instanceof ArrayBuffer)
66
- ) {
67
- responseHeaders.set("Content-Type", "application/json");
68
- return new Response(JSON.stringify(body), {
69
- status,
70
- headers: responseHeaders,
71
- });
72
- }
73
- if (body instanceof ArrayBuffer) {
74
- return new Response(body, { status, headers: responseHeaders });
75
- }
76
- return new Response(String(body), { status, headers: responseHeaders });
77
- }) as unknown as typeof fetch;
78
- }
79
-
80
- function runtimeConfig(overrides?: Partial<TransportConfig>): TransportConfig {
81
- return {
82
- baseURL: "http://localhost:7821",
83
- target: "runtime",
84
- authHeader: "Bearer test-jwt",
85
- fetchFn: mockFetch(200, {}),
86
- ...overrides,
87
- };
88
- }
89
-
90
- function managedConfig(overrides?: Partial<TransportConfig>): TransportConfig {
91
- return {
92
- baseURL: "https://cloud.mycadet.ai",
93
- target: "managed",
94
- authHeader: "session-token-abc",
95
- fetchFn: mockFetch(200, {}),
96
- ...overrides,
97
- };
98
- }
99
-
100
- function makeExecutorOptions(
101
- overrides?: Partial<StepExecutorOptions>,
102
- ): StepExecutorOptions {
103
- return {
104
- sourceConfig: runtimeConfig(),
105
- destConfig: runtimeConfig(),
106
- bundleData: new ArrayBuffer(16),
107
- ...overrides,
108
- };
109
- }
110
-
111
- function makeValidateSuccess(): ValidateResponse {
112
- return {
113
- is_valid: true,
114
- errors: [],
115
- manifest: {
116
- schema_version: 1,
117
- bundle_id: "00000000-0000-4000-8000-000000000000",
118
- created_at: "2026-03-01T00:00:00Z",
119
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
120
- origin: { mode: "self-hosted-local" },
121
- compatibility: {
122
- min_runtime_version: "0.0.0-test",
123
- max_runtime_version: null,
124
- },
125
- contents: [{ path: "config.json", sha256: "abc123", size_bytes: 1024 }],
126
- checksum: "manifest-hash",
127
- secrets_redacted: false,
128
- export_options: {
129
- include_logs: false,
130
- include_browser_state: false,
131
- include_memory_vectors: false,
132
- },
133
- },
134
- };
135
- }
136
-
137
- function makePreflightSuccess(): ImportPreflightResponse {
138
- return {
139
- can_import: true,
140
- summary: {
141
- total_files: 3,
142
- files_to_create: 1,
143
- files_to_overwrite: 1,
144
- files_unchanged: 1,
145
- files_to_skip: 0,
146
- },
147
- files: [
148
- {
149
- path: "config.json",
150
- action: "overwrite",
151
- bundle_size: 1024,
152
- current_size: 800,
153
- bundle_sha256: "abc123",
154
- current_sha256: "old123",
155
- },
156
- ],
157
- conflicts: [],
158
- manifest: {
159
- schema_version: 1,
160
- bundle_id: "00000000-0000-4000-8000-000000000000",
161
- created_at: "2026-03-01T00:00:00Z",
162
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
163
- origin: { mode: "self-hosted-local" },
164
- compatibility: {
165
- min_runtime_version: "0.0.0-test",
166
- max_runtime_version: null,
167
- },
168
- contents: [{ path: "config.json", sha256: "abc123", size_bytes: 1024 }],
169
- checksum: "manifest-hash",
170
- secrets_redacted: false,
171
- export_options: {
172
- include_logs: false,
173
- include_browser_state: false,
174
- include_memory_vectors: false,
175
- },
176
- },
177
- };
178
- }
179
-
180
- function makeImportSuccess(): ImportCommitResponse {
181
- return {
182
- success: true,
183
- summary: {
184
- total_files: 3,
185
- files_created: 1,
186
- files_overwritten: 1,
187
- files_skipped: 1,
188
- backups_created: 1,
189
- },
190
- files: [
191
- {
192
- path: "config.json",
193
- disk_path: "/data/config.json",
194
- action: "overwritten",
195
- size: 1024,
196
- sha256: "abc123",
197
- backup_path: "/data/config.json.bak",
198
- },
199
- ],
200
- manifest: {
201
- schema_version: 1,
202
- bundle_id: "00000000-0000-4000-8000-000000000000",
203
- created_at: "2026-03-01T00:00:00Z",
204
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
205
- origin: { mode: "self-hosted-local" },
206
- compatibility: {
207
- min_runtime_version: "0.0.0-test",
208
- max_runtime_version: null,
209
- },
210
- contents: [{ path: "config.json", sha256: "abc123", size_bytes: 1024 }],
211
- checksum: "manifest-hash",
212
- secrets_redacted: false,
213
- export_options: {
214
- include_logs: false,
215
- include_browser_state: false,
216
- include_memory_vectors: false,
217
- },
218
- },
219
- warnings: ["Backup created for config.json"],
220
- };
221
- }
222
-
223
- /** Advance state to a specific step for testing. */
224
- function advanceTo(step: WizardStep): MigrationWizardState {
225
- let state = createWizardState();
226
- const stepOrder: WizardStep[] = [
227
- "select-direction",
228
- "upload-bundle",
229
- "validate",
230
- "preflight-review",
231
- "transfer",
232
- "rebind-secrets",
233
- "complete",
234
- ];
235
- const targetIdx = stepOrder.indexOf(step);
236
-
237
- if (targetIdx >= 1) {
238
- state = selectDirection(state, "managed-to-self-hosted");
239
- }
240
- if (targetIdx >= 2) {
241
- state = setBundleUploaded(state);
242
- }
243
- if (targetIdx >= 3) {
244
- state = {
245
- ...state,
246
- steps: { ...state.steps, validate: { status: "success" } },
247
- validateResult: makeValidateSuccess(),
248
- currentStep: "preflight-review",
249
- };
250
- }
251
- if (targetIdx >= 4) {
252
- state = {
253
- ...state,
254
- steps: { ...state.steps, "preflight-review": { status: "success" } },
255
- preflightResult: makePreflightSuccess(),
256
- currentStep: "transfer",
257
- };
258
- }
259
- if (targetIdx >= 5) {
260
- state = {
261
- ...state,
262
- steps: { ...state.steps, transfer: { status: "success" } },
263
- importResult: makeImportSuccess(),
264
- currentStep: "rebind-secrets",
265
- };
266
- }
267
- if (targetIdx >= 6) {
268
- state = completeRebindSecrets(state);
269
- }
270
-
271
- return state;
272
- }
273
-
274
- // ---------------------------------------------------------------------------
275
- // Disabled state
276
- // ---------------------------------------------------------------------------
277
-
278
- describe("deriveTransferScreenState -- disabled", () => {
279
- test("returns disabled when on select-direction step", () => {
280
- const state = createWizardState();
281
- const screen = deriveTransferScreenState(state);
282
- expect(screen.phase).toBe("disabled");
283
- });
284
-
285
- test("returns disabled when on upload-bundle step", () => {
286
- const state = advanceTo("upload-bundle");
287
- const screen = deriveTransferScreenState(state);
288
- expect(screen.phase).toBe("disabled");
289
- });
290
-
291
- test("returns disabled when on validate step", () => {
292
- const state = advanceTo("validate");
293
- const screen = deriveTransferScreenState(state);
294
- expect(screen.phase).toBe("disabled");
295
- });
296
-
297
- test("returns disabled when on preflight-review step (not yet completed)", () => {
298
- const state = advanceTo("preflight-review");
299
- const screen = deriveTransferScreenState(state);
300
- expect(screen.phase).toBe("disabled");
301
- });
302
-
303
- test("isTransferScreenAccessible returns false for early steps", () => {
304
- expect(isTransferScreenAccessible(createWizardState())).toBe(false);
305
- });
306
-
307
- test("isTransferScreenAccessible returns false for validate step", () => {
308
- const state = advanceTo("validate");
309
- expect(isTransferScreenAccessible(state)).toBe(false);
310
- });
311
-
312
- test("isTransferScreenAccessible returns true for transfer step", () => {
313
- const state = advanceTo("transfer");
314
- expect(isTransferScreenAccessible(state)).toBe(true);
315
- });
316
- });
317
-
318
- // ---------------------------------------------------------------------------
319
- // Importing state (default phase -- import-only flow)
320
- // ---------------------------------------------------------------------------
321
-
322
- describe("deriveTransferScreenState -- importing (default)", () => {
323
- test("shows importing when transfer step is idle (preparing)", () => {
324
- const state = advanceTo("transfer");
325
- const screen = deriveTransferScreenState(state);
326
- expect(screen.phase).toBe("importing");
327
- if (screen.phase === "importing") {
328
- expect(screen.message).toContain("Importing");
329
- }
330
- });
331
-
332
- test("shows importing when transfer step is loading with no export result", () => {
333
- let state = advanceTo("transfer");
334
- state = {
335
- ...state,
336
- steps: { ...state.steps, transfer: { status: "loading" } },
337
- };
338
- const screen = deriveTransferScreenState(state);
339
- expect(screen.phase).toBe("importing");
340
- if (screen.phase === "importing") {
341
- expect(screen.message).toContain("Importing");
342
- }
343
- });
344
- });
345
-
346
- // ---------------------------------------------------------------------------
347
- // Polling state
348
- // ---------------------------------------------------------------------------
349
-
350
- describe("deriveTransferScreenState -- polling", () => {
351
- test("shows polling when managed export result exists with pending status", () => {
352
- let state = advanceTo("transfer");
353
- state = {
354
- ...state,
355
- steps: { ...state.steps, transfer: { status: "loading" } },
356
- exportResult: {
357
- ok: true,
358
- jobId: "exp-123",
359
- status: "pending",
360
- } as ExportManagedResult,
361
- };
362
- const screen = deriveTransferScreenState(state);
363
- expect(screen.phase).toBe("polling");
364
- if (screen.phase === "polling") {
365
- expect(screen.jobId).toBe("exp-123");
366
- expect(screen.message).toContain("export job");
367
- }
368
- });
369
-
370
- test("shows polling when managed export result exists with processing status", () => {
371
- let state = advanceTo("transfer");
372
- state = {
373
- ...state,
374
- steps: { ...state.steps, transfer: { status: "loading" } },
375
- exportResult: {
376
- ok: true,
377
- jobId: "exp-456",
378
- status: "processing",
379
- } as ExportManagedResult,
380
- };
381
- const screen = deriveTransferScreenState(state);
382
- expect(screen.phase).toBe("polling");
383
- if (screen.phase === "polling") {
384
- expect(screen.jobId).toBe("exp-456");
385
- }
386
- });
387
- });
388
-
389
- // ---------------------------------------------------------------------------
390
- // Importing state
391
- // ---------------------------------------------------------------------------
392
-
393
- describe("deriveTransferScreenState -- importing", () => {
394
- test("shows importing when runtime export result exists but no import result", () => {
395
- let state = advanceTo("transfer");
396
- state = {
397
- ...state,
398
- steps: { ...state.steps, transfer: { status: "loading" } },
399
- exportResult: {
400
- ok: true,
401
- filename: "export.vbundle",
402
- schemaVersion: 1,
403
- checksum: "abc",
404
- },
405
- };
406
- const screen = deriveTransferScreenState(state);
407
- expect(screen.phase).toBe("importing");
408
- if (screen.phase === "importing") {
409
- expect(screen.message).toContain("Importing");
410
- }
411
- });
412
-
413
- test("shows importing when managed export completed and import in progress", () => {
414
- let state = advanceTo("transfer");
415
- state = {
416
- ...state,
417
- steps: { ...state.steps, transfer: { status: "loading" } },
418
- exportResult: {
419
- ok: true,
420
- jobId: "exp-789",
421
- status: "complete",
422
- } as ExportManagedResult,
423
- };
424
- const screen = deriveTransferScreenState(state);
425
- expect(screen.phase).toBe("importing");
426
- if (screen.phase === "importing") {
427
- expect(screen.message).toContain("Importing");
428
- }
429
- });
430
- });
431
-
432
- // ---------------------------------------------------------------------------
433
- // Success state
434
- // ---------------------------------------------------------------------------
435
-
436
- describe("deriveTransferScreenState -- success", () => {
437
- test("shows success with import summary after transfer completes", () => {
438
- const state = advanceTo("rebind-secrets");
439
- const screen = deriveTransferScreenState(state);
440
- expect(screen.phase).toBe("success");
441
- if (screen.phase === "success") {
442
- expect(screen.importSummary.totalFiles).toBe(3);
443
- expect(screen.importSummary.filesCreated).toBe(1);
444
- expect(screen.importSummary.filesOverwritten).toBe(1);
445
- expect(screen.importSummary.filesSkipped).toBe(1);
446
- expect(screen.importSummary.backupsCreated).toBe(1);
447
- expect(screen.importSummary.warnings).toHaveLength(1);
448
- expect(screen.importSummary.warnings[0]).toContain("Backup");
449
- }
450
- });
451
-
452
- test("canContinue is true when rebind-secrets step is accessible", () => {
453
- const state = advanceTo("rebind-secrets");
454
- const screen = deriveTransferScreenState(state);
455
- if (screen.phase === "success") {
456
- expect(screen.canContinue).toBe(true);
457
- }
458
- });
459
-
460
- test("success state persists when viewing from later steps", () => {
461
- const state = advanceTo("complete");
462
- const screen = deriveTransferScreenState(state);
463
- expect(screen.phase).toBe("success");
464
- });
465
-
466
- test("shows success with empty summary when importResult is missing (defensive)", () => {
467
- let state = advanceTo("transfer");
468
- state = {
469
- ...state,
470
- steps: { ...state.steps, transfer: { status: "success" } },
471
- currentStep: "rebind-secrets",
472
- // No importResult
473
- };
474
- const screen = deriveTransferScreenState(state);
475
- expect(screen.phase).toBe("success");
476
- if (screen.phase === "success") {
477
- expect(screen.importSummary.totalFiles).toBe(0);
478
- }
479
- });
480
- });
481
-
482
- // ---------------------------------------------------------------------------
483
- // Error states
484
- // ---------------------------------------------------------------------------
485
-
486
- describe("deriveTransferScreenState -- error", () => {
487
- test("shows error when transfer step fails with retryable error", () => {
488
- let state = advanceTo("transfer");
489
- state = {
490
- ...state,
491
- steps: {
492
- ...state.steps,
493
- transfer: {
494
- status: "error",
495
- error: {
496
- message: "export: HTTP 500",
497
- code: "HTTP_500",
498
- retryable: true,
499
- },
500
- },
501
- },
502
- };
503
- const screen = deriveTransferScreenState(state);
504
- expect(screen.phase).toBe("error");
505
- if (screen.phase === "error") {
506
- expect(screen.error.message).toContain("HTTP 500");
507
- expect(screen.canRetry).toBe(true);
508
- }
509
- });
510
-
511
- test("shows error when transfer step fails with non-retryable error", () => {
512
- let state = advanceTo("transfer");
513
- state = {
514
- ...state,
515
- steps: {
516
- ...state.steps,
517
- transfer: {
518
- status: "error",
519
- error: {
520
- message: "import: HTTP 400",
521
- code: "HTTP_400",
522
- retryable: false,
523
- },
524
- },
525
- },
526
- };
527
- const screen = deriveTransferScreenState(state);
528
- expect(screen.phase).toBe("error");
529
- if (screen.phase === "error") {
530
- expect(screen.canRetry).toBe(false);
531
- }
532
- });
533
-
534
- test("infers failed phase as import when no export result exists (import-only flow)", () => {
535
- let state = advanceTo("transfer");
536
- state = {
537
- ...state,
538
- steps: {
539
- ...state.steps,
540
- transfer: {
541
- status: "error",
542
- error: {
543
- message: "import: HTTP 503",
544
- code: "HTTP_503",
545
- retryable: true,
546
- },
547
- },
548
- },
549
- };
550
- const screen = deriveTransferScreenState(state);
551
- if (screen.phase === "error") {
552
- expect(screen.failedPhase).toBe("import");
553
- }
554
- });
555
-
556
- test("infers failed phase as export for EXPORT_JOB_FAILED code", () => {
557
- let state = advanceTo("transfer");
558
- state = {
559
- ...state,
560
- steps: {
561
- ...state.steps,
562
- transfer: {
563
- status: "error",
564
- error: {
565
- message: "Export job failed: Timeout",
566
- code: "EXPORT_JOB_FAILED",
567
- retryable: true,
568
- },
569
- },
570
- },
571
- exportResult: {
572
- ok: true,
573
- jobId: "exp-fail",
574
- status: "pending",
575
- } as ExportManagedResult,
576
- };
577
- const screen = deriveTransferScreenState(state);
578
- if (screen.phase === "error") {
579
- expect(screen.failedPhase).toBe("export");
580
- }
581
- });
582
-
583
- test("infers failed phase as export for NO_DOWNLOAD_URL code", () => {
584
- let state = advanceTo("transfer");
585
- state = {
586
- ...state,
587
- steps: {
588
- ...state.steps,
589
- transfer: {
590
- status: "error",
591
- error: {
592
- message: "Export completed but no download URL was provided",
593
- code: "NO_DOWNLOAD_URL",
594
- retryable: true,
595
- },
596
- },
597
- },
598
- exportResult: {
599
- ok: true,
600
- jobId: "exp-no-url",
601
- status: "complete",
602
- } as ExportManagedResult,
603
- };
604
- const screen = deriveTransferScreenState(state);
605
- if (screen.phase === "error") {
606
- expect(screen.failedPhase).toBe("export");
607
- }
608
- });
609
-
610
- test("infers failed phase as import when import result exists and failed", () => {
611
- let state = advanceTo("transfer");
612
- state = {
613
- ...state,
614
- steps: {
615
- ...state.steps,
616
- transfer: {
617
- status: "error",
618
- error: {
619
- message: "Disk full",
620
- code: "write_failed",
621
- retryable: true,
622
- },
623
- },
624
- },
625
- exportResult: {
626
- ok: true,
627
- filename: "export.vbundle",
628
- schemaVersion: 1,
629
- checksum: "abc",
630
- },
631
- importResult: {
632
- success: false,
633
- reason: "write_failed",
634
- message: "Disk full",
635
- },
636
- };
637
- const screen = deriveTransferScreenState(state);
638
- if (screen.phase === "error") {
639
- expect(screen.failedPhase).toBe("import");
640
- }
641
- });
642
-
643
- test("infers failed phase as poll for managed export transport error during download", () => {
644
- let state = advanceTo("transfer");
645
- state = {
646
- ...state,
647
- steps: {
648
- ...state.steps,
649
- transfer: {
650
- status: "error",
651
- error: {
652
- message: "Failed to download export: HTTP 404",
653
- code: "HTTP_404",
654
- retryable: false,
655
- },
656
- },
657
- },
658
- exportResult: {
659
- ok: true,
660
- jobId: "exp-download-fail",
661
- status: "complete",
662
- } as ExportManagedResult,
663
- // No importResult -- download failed before import
664
- };
665
- const screen = deriveTransferScreenState(state);
666
- if (screen.phase === "error") {
667
- expect(screen.failedPhase).toBe("poll");
668
- }
669
- });
670
-
671
- test("shows default error when step has error status but no error details", () => {
672
- let state = advanceTo("transfer");
673
- state = {
674
- ...state,
675
- steps: {
676
- ...state.steps,
677
- transfer: { status: "error" },
678
- },
679
- };
680
- const screen = deriveTransferScreenState(state);
681
- expect(screen.phase).toBe("error");
682
- if (screen.phase === "error") {
683
- expect(screen.error.message).toBe("Transfer failed");
684
- expect(screen.error.retryable).toBe(false);
685
- expect(screen.canRetry).toBe(false);
686
- }
687
- });
688
- });
689
-
690
- // ---------------------------------------------------------------------------
691
- // Retry behavior
692
- // ---------------------------------------------------------------------------
693
-
694
- describe("retryTransferFlow", () => {
695
- test("retries transfer step after export error", async () => {
696
- let state = advanceTo("transfer");
697
- const importResponse = makeImportSuccess();
698
-
699
- // Simulate a failed export
700
- state = {
701
- ...state,
702
- steps: {
703
- ...state.steps,
704
- transfer: {
705
- status: "error",
706
- error: {
707
- message: "export: HTTP 500",
708
- code: "HTTP_500",
709
- retryable: true,
710
- },
711
- },
712
- },
713
- };
714
-
715
- const archiveBytes = new ArrayBuffer(32);
716
- const sourceFetch = (async () => {
717
- return new Response(archiveBytes, {
718
- status: 200,
719
- headers: {
720
- "Content-Disposition": 'attachment; filename="export.vbundle"',
721
- "X-Vbundle-Schema-Version": "1",
722
- "X-Vbundle-Manifest-Sha256": "abc",
723
- },
724
- });
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 options = makeExecutorOptions({
735
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
736
- destConfig: runtimeConfig({ fetchFn: destFetch }),
737
- });
738
-
739
- const result = await retryTransferFlow(state, options);
740
- expect(result.steps.transfer.status).toBe("success");
741
- expect(result.currentStep).toBe("rebind-secrets");
742
- });
743
-
744
- test("throws when trying to retry non-retryable error", () => {
745
- let state = advanceTo("transfer");
746
- state = {
747
- ...state,
748
- steps: {
749
- ...state.steps,
750
- transfer: {
751
- status: "error",
752
- error: {
753
- message: "Fatal error",
754
- retryable: false,
755
- },
756
- },
757
- },
758
- };
759
-
760
- const options = makeExecutorOptions();
761
- expect(retryTransferFlow(state, options)).rejects.toThrow(
762
- "not in a retryable error state",
763
- );
764
- });
765
-
766
- test("emits state change on reset before retry", async () => {
767
- let state = advanceTo("transfer");
768
- const importResponse = makeImportSuccess();
769
-
770
- state = {
771
- ...state,
772
- steps: {
773
- ...state.steps,
774
- transfer: {
775
- status: "error",
776
- error: {
777
- message: "export: HTTP 500",
778
- code: "HTTP_500",
779
- retryable: true,
780
- },
781
- },
782
- },
783
- };
784
-
785
- const archiveBytes = new ArrayBuffer(32);
786
- const sourceFetch = (async () => {
787
- return new Response(archiveBytes, {
788
- status: 200,
789
- headers: {
790
- "Content-Disposition": 'attachment; filename="export.vbundle"',
791
- "X-Vbundle-Schema-Version": "1",
792
- "X-Vbundle-Manifest-Sha256": "abc",
793
- },
794
- });
795
- }) as unknown as typeof fetch;
796
-
797
- const destFetch = (async () => {
798
- return new Response(JSON.stringify(importResponse), {
799
- status: 200,
800
- headers: { "Content-Type": "application/json" },
801
- });
802
- }) as unknown as typeof fetch;
803
-
804
- const stateChanges: MigrationWizardState[] = [];
805
- const options = makeExecutorOptions({
806
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
807
- destConfig: runtimeConfig({ fetchFn: destFetch }),
808
- onStateChange: (s) => stateChanges.push(s),
809
- });
810
-
811
- await retryTransferFlow(state, options);
812
-
813
- // The first state change should be the reset (idle status)
814
- expect(stateChanges.length).toBeGreaterThan(0);
815
- expect(stateChanges[0].steps.transfer.status).toBe("idle");
816
- });
817
- });
818
-
819
- // ---------------------------------------------------------------------------
820
- // Transfer flow execution
821
- // ---------------------------------------------------------------------------
822
-
823
- describe("executeTransferFlow", () => {
824
- test("runtime export + import -- success", async () => {
825
- const archiveBytes = new ArrayBuffer(32);
826
- const importResponse = makeImportSuccess();
827
-
828
- const sourceFetch = (async () => {
829
- return new Response(archiveBytes, {
830
- status: 200,
831
- headers: {
832
- "Content-Disposition": 'attachment; filename="export.vbundle"',
833
- "X-Vbundle-Schema-Version": "1",
834
- "X-Vbundle-Manifest-Sha256": "abc",
835
- },
836
- });
837
- }) as unknown as typeof fetch;
838
-
839
- const destFetch = (async () => {
840
- return new Response(JSON.stringify(importResponse), {
841
- status: 200,
842
- headers: { "Content-Type": "application/json" },
843
- });
844
- }) as unknown as typeof fetch;
845
-
846
- const state = advanceTo("transfer");
847
- const options = makeExecutorOptions({
848
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
849
- destConfig: runtimeConfig({ fetchFn: destFetch }),
850
- });
851
-
852
- const result = await executeTransferFlow(state, options);
853
- expect(result.steps.transfer.status).toBe("success");
854
- expect(result.currentStep).toBe("rebind-secrets");
855
-
856
- const screen = deriveTransferScreenState(result);
857
- expect(screen.phase).toBe("success");
858
- if (screen.phase === "success") {
859
- expect(screen.importSummary.totalFiles).toBe(3);
860
- }
861
- });
862
-
863
- test("managed export + poll + import -- success", async () => {
864
- let fetchCallCount = 0;
865
- const importResponse = makeImportSuccess();
866
-
867
- const sourceFetch = (async (url: string | URL | Request) => {
868
- fetchCallCount++;
869
- const urlStr = String(url);
870
- if (urlStr.endsWith("/export/")) {
871
- return new Response(
872
- JSON.stringify({ job_id: "exp-1", status: "pending" }),
873
- {
874
- status: 200,
875
- headers: { "Content-Type": "application/json" },
876
- },
877
- );
878
- }
879
- if (urlStr.includes("/export/") && urlStr.includes("/status/")) {
880
- if (fetchCallCount <= 3) {
881
- return new Response(
882
- JSON.stringify({ status: "processing", job_id: "exp-1" }),
883
- {
884
- status: 200,
885
- headers: { "Content-Type": "application/json" },
886
- },
887
- );
888
- }
889
- return new Response(
890
- JSON.stringify({
891
- status: "complete",
892
- job_id: "exp-1",
893
- download_url: "https://cdn.example.com/export.vbundle",
894
- }),
895
- {
896
- status: 200,
897
- headers: { "Content-Type": "application/json" },
898
- },
899
- );
900
- }
901
- if (urlStr === "https://cdn.example.com/export.vbundle") {
902
- return new Response(new ArrayBuffer(32), { status: 200 });
903
- }
904
- return new Response("Not found", { status: 404 });
905
- }) as unknown as typeof fetch;
906
-
907
- const destFetch = (async () => {
908
- return new Response(JSON.stringify(importResponse), {
909
- status: 200,
910
- headers: { "Content-Type": "application/json" },
911
- });
912
- }) as unknown as typeof fetch;
913
-
914
- const state = advanceTo("transfer");
915
- const options = makeExecutorOptions({
916
- sourceConfig: managedConfig({ fetchFn: sourceFetch }),
917
- destConfig: runtimeConfig({ fetchFn: destFetch }),
918
- });
919
-
920
- const result = await executeTransferFlow(state, options);
921
- expect(result.steps.transfer.status).toBe("success");
922
- expect(result.currentStep).toBe("rebind-secrets");
923
- });
924
-
925
- test("transfer failure sets error state with import phase", async () => {
926
- const state = advanceTo("transfer");
927
- const options = makeExecutorOptions({
928
- sourceConfig: runtimeConfig({ fetchFn: mockFetch(500, "Server Error") }),
929
- });
930
-
931
- const result = await executeTransferFlow(state, options);
932
- expect(result.steps.transfer.status).toBe("error");
933
-
934
- const screen = deriveTransferScreenState(result);
935
- expect(screen.phase).toBe("error");
936
- if (screen.phase === "error") {
937
- expect(screen.canRetry).toBe(true);
938
- expect(screen.failedPhase).toBe("import");
939
- }
940
- });
941
-
942
- test("import failure sets error state with import reason", async () => {
943
- const archiveBytes = new ArrayBuffer(32);
944
- const importResponse: ImportCommitResponse = {
945
- success: false,
946
- reason: "write_failed",
947
- message: "Disk full",
948
- };
949
-
950
- const sourceFetch = (async () => {
951
- return new Response(archiveBytes, {
952
- status: 200,
953
- headers: {
954
- "Content-Disposition": 'attachment; filename="export.vbundle"',
955
- "X-Vbundle-Schema-Version": "1",
956
- "X-Vbundle-Manifest-Sha256": "abc",
957
- },
958
- });
959
- }) as unknown as typeof fetch;
960
-
961
- const destFetch = (async () => {
962
- return new Response(JSON.stringify(importResponse), {
963
- status: 200,
964
- headers: { "Content-Type": "application/json" },
965
- });
966
- }) as unknown as typeof fetch;
967
-
968
- const state = advanceTo("transfer");
969
- const options = makeExecutorOptions({
970
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
971
- destConfig: runtimeConfig({ fetchFn: destFetch }),
972
- });
973
-
974
- const result = await executeTransferFlow(state, options);
975
- expect(result.steps.transfer.status).toBe("error");
976
- expect(result.steps.transfer.error?.message).toContain("Disk full");
977
- });
978
- });
979
-
980
- // ---------------------------------------------------------------------------
981
- // Back navigation
982
- // ---------------------------------------------------------------------------
983
-
984
- describe("goBackToPreflight", () => {
985
- test("navigates back to preflight-review from transfer", () => {
986
- const state = advanceTo("transfer");
987
- const result = goBackToPreflight(state);
988
- expect(result.currentStep).toBe("preflight-review");
989
- expect(result.steps.transfer.status).toBe("idle");
990
- });
991
-
992
- test("clears export and import results when going back", () => {
993
- let state = advanceTo("transfer");
994
- state = {
995
- ...state,
996
- exportResult: {
997
- ok: true,
998
- filename: "export.vbundle",
999
- schemaVersion: 1,
1000
- checksum: "abc",
1001
- },
1002
- importResult: makeImportSuccess(),
1003
- };
1004
- const result = goBackToPreflight(state);
1005
- expect(result.exportResult).toBeUndefined();
1006
- expect(result.importResult).toBeUndefined();
1007
- });
1008
-
1009
- test("preserves validation results but clears preflight when going back", () => {
1010
- let state = advanceTo("transfer");
1011
- state = {
1012
- ...state,
1013
- validateResult: makeValidateSuccess(),
1014
- preflightResult: makePreflightSuccess(),
1015
- };
1016
- const result = goBackToPreflight(state);
1017
- // Validation results are preserved since we're only going back to preflight
1018
- expect(result.validateResult).toBeDefined();
1019
- // Preflight results are cleared because going back resets the target step
1020
- expect(result.preflightResult).toBeUndefined();
1021
- });
1022
- });
1023
-
1024
- // ---------------------------------------------------------------------------
1025
- // State change callbacks
1026
- // ---------------------------------------------------------------------------
1027
-
1028
- describe("executeTransferFlow -- onStateChange callbacks", () => {
1029
- test("emits state changes during transfer flow", async () => {
1030
- const archiveBytes = new ArrayBuffer(32);
1031
- const importResponse = makeImportSuccess();
1032
-
1033
- const sourceFetch = (async () => {
1034
- return new Response(archiveBytes, {
1035
- status: 200,
1036
- headers: {
1037
- "Content-Disposition": 'attachment; filename="export.vbundle"',
1038
- "X-Vbundle-Schema-Version": "1",
1039
- "X-Vbundle-Manifest-Sha256": "abc",
1040
- },
1041
- });
1042
- }) as unknown as typeof fetch;
1043
-
1044
- const destFetch = (async () => {
1045
- return new Response(JSON.stringify(importResponse), {
1046
- status: 200,
1047
- headers: { "Content-Type": "application/json" },
1048
- });
1049
- }) as unknown as typeof fetch;
1050
-
1051
- const state = advanceTo("transfer");
1052
- const stateChanges: TransferScreenState[] = [];
1053
- const options = makeExecutorOptions({
1054
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
1055
- destConfig: runtimeConfig({ fetchFn: destFetch }),
1056
- onStateChange: (s) => stateChanges.push(deriveTransferScreenState(s)),
1057
- });
1058
-
1059
- await executeTransferFlow(state, options);
1060
-
1061
- // Should have gone through at least the loading state
1062
- const phases = stateChanges.map((s) => s.phase);
1063
- expect(phases).toContain("importing");
1064
- });
1065
- });
1066
-
1067
- // ---------------------------------------------------------------------------
1068
- // State persistence and resume
1069
- // ---------------------------------------------------------------------------
1070
-
1071
- describe("transfer screen state persistence/resume", () => {
1072
- test("derives correct state after serialize + deserialize at transfer step", () => {
1073
- const state = advanceTo("transfer");
1074
- const json = serializeWizardState(state);
1075
- const restored = deserializeWizardState(json);
1076
- expect(restored).toBeDefined();
1077
-
1078
- // After deserialization, hasBundleData is false, so prepareForResume
1079
- // should redirect to upload-bundle
1080
- const resumed = prepareForResume(restored!);
1081
- const screen = deriveTransferScreenState(resumed);
1082
- expect(screen.phase).toBe("disabled");
1083
- expect(resumed.currentStep).toBe("upload-bundle");
1084
- });
1085
-
1086
- test("derives correct state after resume at transfer step with loading status", () => {
1087
- let state = advanceTo("transfer");
1088
- state = {
1089
- ...state,
1090
- steps: { ...state.steps, transfer: { status: "loading" } },
1091
- };
1092
-
1093
- const json = serializeWizardState(state);
1094
- const restored = deserializeWizardState(json);
1095
- expect(restored).toBeDefined();
1096
-
1097
- // prepareForResume resets loading to idle and redirects to upload-bundle
1098
- // because hasBundleData is false after deserialization
1099
- const resumed = prepareForResume(restored!);
1100
- expect(resumed.currentStep).toBe("upload-bundle");
1101
- });
1102
-
1103
- test("success results persist across serialize/deserialize", () => {
1104
- let state = advanceTo("rebind-secrets");
1105
- state = {
1106
- ...state,
1107
- importResult: makeImportSuccess(),
1108
- };
1109
-
1110
- const json = serializeWizardState(state);
1111
- const restored = deserializeWizardState(json);
1112
- expect(restored).toBeDefined();
1113
-
1114
- const screen = deriveTransferScreenState(restored!);
1115
- expect(screen.phase).toBe("success");
1116
- if (screen.phase === "success") {
1117
- expect(screen.importSummary.totalFiles).toBe(3);
1118
- }
1119
- });
1120
-
1121
- test("error state is preserved after serialize/deserialize", () => {
1122
- let state = advanceTo("transfer");
1123
- state = {
1124
- ...state,
1125
- steps: {
1126
- ...state.steps,
1127
- transfer: {
1128
- status: "error",
1129
- error: {
1130
- message: "export: HTTP 500",
1131
- code: "HTTP_500",
1132
- retryable: true,
1133
- },
1134
- },
1135
- },
1136
- };
1137
-
1138
- const json = serializeWizardState(state);
1139
- const restored = deserializeWizardState(json);
1140
- expect(restored).toBeDefined();
1141
-
1142
- // The raw deserialized state (before resume) preserves the error
1143
- const screen = deriveTransferScreenState(restored!);
1144
- expect(screen.phase).toBe("error");
1145
- if (screen.phase === "error") {
1146
- expect(screen.error.message).toContain("HTTP 500");
1147
- expect(screen.canRetry).toBe(true);
1148
- }
1149
- });
1150
- });
1151
-
1152
- // ---------------------------------------------------------------------------
1153
- // Edge cases
1154
- // ---------------------------------------------------------------------------
1155
-
1156
- describe("transfer screen edge cases", () => {
1157
- test("handles transfer step at complete state -- success visible", () => {
1158
- const state = advanceTo("complete");
1159
- const screen = deriveTransferScreenState(state);
1160
- expect(screen.phase).toBe("success");
1161
- });
1162
-
1163
- test("handles transfer error from wrong step gracefully", () => {
1164
- const state = advanceTo("validate");
1165
- // Attempting to derive transfer screen from validate step
1166
- const screen = deriveTransferScreenState(state);
1167
- expect(screen.phase).toBe("disabled");
1168
- });
1169
-
1170
- test("runtime flow does not show polling phase", async () => {
1171
- const archiveBytes = new ArrayBuffer(32);
1172
- const importResponse = makeImportSuccess();
1173
-
1174
- const sourceFetch = (async () => {
1175
- return new Response(archiveBytes, {
1176
- status: 200,
1177
- headers: {
1178
- "Content-Disposition": 'attachment; filename="export.vbundle"',
1179
- "X-Vbundle-Schema-Version": "1",
1180
- "X-Vbundle-Manifest-Sha256": "abc",
1181
- },
1182
- });
1183
- }) as unknown as typeof fetch;
1184
-
1185
- const destFetch = (async () => {
1186
- return new Response(JSON.stringify(importResponse), {
1187
- status: 200,
1188
- headers: { "Content-Type": "application/json" },
1189
- });
1190
- }) as unknown as typeof fetch;
1191
-
1192
- const state = advanceTo("transfer");
1193
- const stateChanges: TransferScreenState[] = [];
1194
- const options = makeExecutorOptions({
1195
- sourceConfig: runtimeConfig({ fetchFn: sourceFetch }),
1196
- destConfig: runtimeConfig({ fetchFn: destFetch }),
1197
- onStateChange: (s) => stateChanges.push(deriveTransferScreenState(s)),
1198
- });
1199
-
1200
- await executeTransferFlow(state, options);
1201
-
1202
- // Runtime flow should not have any polling phases
1203
- const phases = stateChanges.map((s) => s.phase);
1204
- expect(phases).not.toContain("polling");
1205
- });
1206
-
1207
- test("transfer flow throws when called from wrong step", async () => {
1208
- const state = advanceTo("validate");
1209
- const options = makeExecutorOptions();
1210
- try {
1211
- await executeTransferFlow(state, options);
1212
- expect(true).toBe(false);
1213
- } catch (err) {
1214
- expect((err as Error).message).toContain("wrong step");
1215
- }
1216
- });
1217
- });