@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,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
- });