@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,1130 +0,0 @@
1
- /**
2
- * Tests for the validation results screen view model.
3
- *
4
- * Covers:
5
- * - Disabled state: screen not accessible when earlier steps incomplete
6
- * - Loading states: validation loading, preflight loading, idle/preparing
7
- * - Validation error: bundle invalid with error details
8
- * - Transport error: network failures for validate and preflight steps
9
- * - Preflight error: preflight validation failure
10
- * - Success state: full results with summary, files, conflicts
11
- * - Retry behavior: reset and re-execute on retryable errors
12
- * - Back navigation: return to upload-bundle step
13
- * - File grouping: group files by action (create/overwrite/unchanged)
14
- * - File size formatting: human-readable byte sizes
15
- * - Validation flow execution: chained validate + preflight
16
- * - State persistence/resume: derive correct screen state after resume
17
- */
18
-
19
- import { describe, expect, test } from "bun:test";
20
-
21
- import type {
22
- ImportPreflightResponse,
23
- TransportConfig,
24
- ValidateResponse,
25
- } from "../runtime/migrations/migration-transport.js";
26
- import type {
27
- MigrationWizardState,
28
- StepExecutorOptions,
29
- WizardStep,
30
- } from "../runtime/migrations/migration-wizard.js";
31
- import {
32
- completeRebindSecrets,
33
- createWizardState,
34
- deserializeWizardState,
35
- prepareForResume,
36
- selectDirection,
37
- serializeWizardState,
38
- setBundleUploaded,
39
- } from "../runtime/migrations/migration-wizard.js";
40
- import type {
41
- PreflightFileEntry,
42
- ValidationScreenState,
43
- } from "../runtime/migrations/validation-results-screen.js";
44
- import {
45
- deriveValidationScreenState,
46
- executeValidationFlow,
47
- formatFileSize,
48
- goBackToUpload,
49
- groupFilesByAction,
50
- isValidationScreenAccessible,
51
- retryValidationFlow,
52
- } from "../runtime/migrations/validation-results-screen.js";
53
-
54
- // ---------------------------------------------------------------------------
55
- // Test helpers
56
- // ---------------------------------------------------------------------------
57
-
58
- function mockFetch(
59
- status: number,
60
- body: unknown,
61
- headers?: Record<string, string>,
62
- ): typeof fetch {
63
- return (async () => {
64
- const responseHeaders = new Headers(headers);
65
- if (
66
- typeof body === "object" &&
67
- body !== undefined &&
68
- !(body instanceof ArrayBuffer)
69
- ) {
70
- responseHeaders.set("Content-Type", "application/json");
71
- return new Response(JSON.stringify(body), {
72
- status,
73
- headers: responseHeaders,
74
- });
75
- }
76
- if (body instanceof ArrayBuffer) {
77
- return new Response(body, { status, headers: responseHeaders });
78
- }
79
- return new Response(String(body), { status, headers: responseHeaders });
80
- }) as unknown as typeof fetch;
81
- }
82
-
83
- function runtimeConfig(overrides?: Partial<TransportConfig>): TransportConfig {
84
- return {
85
- baseURL: "http://localhost:7821",
86
- target: "runtime",
87
- authHeader: "Bearer test-jwt",
88
- fetchFn: mockFetch(200, {}),
89
- ...overrides,
90
- };
91
- }
92
-
93
- function makeExecutorOptions(
94
- overrides?: Partial<StepExecutorOptions>,
95
- ): StepExecutorOptions {
96
- return {
97
- sourceConfig: runtimeConfig(),
98
- destConfig: runtimeConfig(),
99
- bundleData: new ArrayBuffer(16),
100
- ...overrides,
101
- };
102
- }
103
-
104
- /** Advance state to a specific step for testing. */
105
- function advanceTo(step: WizardStep): MigrationWizardState {
106
- let state = createWizardState();
107
- const stepOrder: WizardStep[] = [
108
- "select-direction",
109
- "upload-bundle",
110
- "validate",
111
- "preflight-review",
112
- "transfer",
113
- "rebind-secrets",
114
- "complete",
115
- ];
116
- const targetIdx = stepOrder.indexOf(step);
117
-
118
- if (targetIdx >= 1) {
119
- state = selectDirection(state, "managed-to-self-hosted");
120
- }
121
- if (targetIdx >= 2) {
122
- state = setBundleUploaded(state);
123
- }
124
- if (targetIdx >= 3) {
125
- state = {
126
- ...state,
127
- steps: { ...state.steps, validate: { status: "success" } },
128
- validateResult: makeValidateSuccess(),
129
- currentStep: "preflight-review",
130
- };
131
- }
132
- if (targetIdx >= 4) {
133
- state = {
134
- ...state,
135
- steps: { ...state.steps, "preflight-review": { status: "success" } },
136
- preflightResult: makePreflightSuccess(),
137
- currentStep: "transfer",
138
- };
139
- }
140
- if (targetIdx >= 5) {
141
- state = {
142
- ...state,
143
- steps: { ...state.steps, transfer: { status: "success" } },
144
- currentStep: "rebind-secrets",
145
- };
146
- }
147
- if (targetIdx >= 6) {
148
- state = completeRebindSecrets(state);
149
- }
150
-
151
- return state;
152
- }
153
-
154
- function makeValidateSuccess(): ValidateResponse {
155
- return {
156
- is_valid: true,
157
- errors: [],
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: [
169
- { path: "config.json", sha256: "abc123", size_bytes: 1024 },
170
- { path: "skills/test.md", sha256: "def456", size_bytes: 2048 },
171
- ],
172
- checksum: "manifest-hash",
173
- secrets_redacted: false,
174
- export_options: {
175
- include_logs: false,
176
- include_browser_state: false,
177
- include_memory_vectors: false,
178
- },
179
- },
180
- };
181
- }
182
-
183
- function makeValidateFailure(): ValidateResponse {
184
- return {
185
- is_valid: false,
186
- errors: [
187
- {
188
- code: "INVALID_SCHEMA",
189
- message: "Bundle schema version 0.5 is not supported",
190
- path: "manifest.json",
191
- },
192
- {
193
- code: "MISSING_FILE",
194
- message: "Referenced file config.json not found in archive",
195
- },
196
- ],
197
- };
198
- }
199
-
200
- function makePreflightSuccess(): ImportPreflightResponse {
201
- return {
202
- can_import: true,
203
- summary: {
204
- total_files: 5,
205
- files_to_create: 2,
206
- files_to_overwrite: 2,
207
- files_unchanged: 1,
208
- files_to_skip: 0,
209
- },
210
- files: [
211
- {
212
- path: "config.json",
213
- action: "overwrite",
214
- bundle_size: 1024,
215
- current_size: 800,
216
- bundle_sha256: "abc123",
217
- current_sha256: "old123",
218
- },
219
- {
220
- path: "skills/new-skill.md",
221
- action: "create",
222
- bundle_size: 512,
223
- current_size: null,
224
- bundle_sha256: "ghi789",
225
- current_sha256: null,
226
- },
227
- {
228
- path: "memory/index.json",
229
- action: "overwrite",
230
- bundle_size: 4096,
231
- current_size: 3500,
232
- bundle_sha256: "jkl012",
233
- current_sha256: "old012",
234
- },
235
- {
236
- path: "playbooks/default.md",
237
- action: "unchanged",
238
- bundle_size: 2048,
239
- current_size: 2048,
240
- bundle_sha256: "mno345",
241
- current_sha256: "mno345",
242
- },
243
- {
244
- path: "tools/custom.ts",
245
- action: "create",
246
- bundle_size: 768,
247
- current_size: null,
248
- bundle_sha256: "pqr678",
249
- current_sha256: null,
250
- },
251
- ],
252
- conflicts: [
253
- {
254
- code: "SIZE_MISMATCH",
255
- message: "config.json will increase from 800B to 1024B",
256
- path: "config.json",
257
- },
258
- ],
259
- manifest: {
260
- schema_version: 1,
261
- bundle_id: "00000000-0000-4000-8000-000000000000",
262
- created_at: "2026-03-01T00:00:00Z",
263
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
264
- origin: { mode: "self-hosted-local" },
265
- compatibility: {
266
- min_runtime_version: "0.0.0-test",
267
- max_runtime_version: null,
268
- },
269
- contents: [
270
- { path: "config.json", sha256: "abc123", size_bytes: 1024 },
271
- { path: "skills/new-skill.md", sha256: "ghi789", size_bytes: 512 },
272
- ],
273
- checksum: "manifest-hash",
274
- secrets_redacted: false,
275
- export_options: {
276
- include_logs: false,
277
- include_browser_state: false,
278
- include_memory_vectors: false,
279
- },
280
- },
281
- };
282
- }
283
-
284
- function makePreflightFailure(): ImportPreflightResponse {
285
- return {
286
- can_import: false,
287
- validation: {
288
- is_valid: false,
289
- errors: [
290
- {
291
- code: "INCOMPATIBLE_VERSION",
292
- message: "Bundle requires schema version 2.0, current is 1.0",
293
- },
294
- ],
295
- },
296
- };
297
- }
298
-
299
- // ---------------------------------------------------------------------------
300
- // Disabled state
301
- // ---------------------------------------------------------------------------
302
-
303
- describe("deriveValidationScreenState — disabled", () => {
304
- test("returns disabled when on select-direction step", () => {
305
- const state = createWizardState();
306
- const screen = deriveValidationScreenState(state);
307
- expect(screen.phase).toBe("disabled");
308
- });
309
-
310
- test("returns disabled when on upload-bundle step", () => {
311
- const state = advanceTo("upload-bundle");
312
- const screen = deriveValidationScreenState(state);
313
- expect(screen.phase).toBe("disabled");
314
- });
315
-
316
- test("isValidationScreenAccessible returns false for early steps", () => {
317
- expect(isValidationScreenAccessible(createWizardState())).toBe(false);
318
- });
319
-
320
- test("isValidationScreenAccessible returns true for validate step", () => {
321
- const state = advanceTo("validate");
322
- expect(isValidationScreenAccessible(state)).toBe(true);
323
- });
324
-
325
- test("isValidationScreenAccessible returns true for preflight-review step", () => {
326
- const state = advanceTo("preflight-review");
327
- expect(isValidationScreenAccessible(state)).toBe(true);
328
- });
329
- });
330
-
331
- // ---------------------------------------------------------------------------
332
- // Loading states
333
- // ---------------------------------------------------------------------------
334
-
335
- describe("deriveValidationScreenState — loading", () => {
336
- test("shows loading when validate step is idle (preparing)", () => {
337
- const state = advanceTo("validate");
338
- const screen = deriveValidationScreenState(state);
339
- expect(screen.phase).toBe("loading");
340
- if (screen.phase === "loading") {
341
- expect(screen.message).toContain("validation");
342
- }
343
- });
344
-
345
- test("shows loading when validate step is in loading status", () => {
346
- let state = advanceTo("validate");
347
- state = {
348
- ...state,
349
- steps: { ...state.steps, validate: { status: "loading" } },
350
- };
351
- const screen = deriveValidationScreenState(state);
352
- expect(screen.phase).toBe("loading");
353
- if (screen.phase === "loading") {
354
- expect(screen.message).toContain("Validating");
355
- }
356
- });
357
-
358
- test("shows loading when preflight-review step is idle (preparing)", () => {
359
- let state = advanceTo("preflight-review");
360
- state = {
361
- ...state,
362
- steps: { ...state.steps, "preflight-review": { status: "idle" } },
363
- };
364
- const screen = deriveValidationScreenState(state);
365
- expect(screen.phase).toBe("loading");
366
- if (screen.phase === "loading") {
367
- expect(screen.message).toContain("import analysis");
368
- }
369
- });
370
-
371
- test("shows loading when preflight-review step is in loading status", () => {
372
- let state = advanceTo("preflight-review");
373
- state = {
374
- ...state,
375
- steps: { ...state.steps, "preflight-review": { status: "loading" } },
376
- };
377
- const screen = deriveValidationScreenState(state);
378
- expect(screen.phase).toBe("loading");
379
- if (screen.phase === "loading") {
380
- expect(screen.message).toContain("Analyzing");
381
- }
382
- });
383
- });
384
-
385
- // ---------------------------------------------------------------------------
386
- // Validation error
387
- // ---------------------------------------------------------------------------
388
-
389
- describe("deriveValidationScreenState — validation-error", () => {
390
- test("shows validation-error with error details when bundle is invalid", () => {
391
- let state = advanceTo("validate");
392
- state = {
393
- ...state,
394
- steps: {
395
- ...state.steps,
396
- validate: {
397
- status: "error",
398
- error: {
399
- message: "Bundle validation failed",
400
- code: "INVALID_SCHEMA",
401
- retryable: true,
402
- },
403
- },
404
- },
405
- validateResult: makeValidateFailure(),
406
- };
407
- const screen = deriveValidationScreenState(state);
408
- expect(screen.phase).toBe("validation-error");
409
- if (screen.phase === "validation-error") {
410
- expect(screen.errors).toHaveLength(2);
411
- expect(screen.errors[0].code).toBe("INVALID_SCHEMA");
412
- expect(screen.errors[0].message).toContain("schema version");
413
- expect(screen.errors[1].code).toBe("MISSING_FILE");
414
- expect(screen.canRetry).toBe(true);
415
- }
416
- });
417
-
418
- test("shows validation-error as non-retryable when error is not retryable", () => {
419
- let state = advanceTo("validate");
420
- state = {
421
- ...state,
422
- steps: {
423
- ...state.steps,
424
- validate: {
425
- status: "error",
426
- error: {
427
- message: "Bundle validation failed",
428
- code: "INVALID_SCHEMA",
429
- retryable: false,
430
- },
431
- },
432
- },
433
- validateResult: makeValidateFailure(),
434
- };
435
- const screen = deriveValidationScreenState(state);
436
- expect(screen.phase).toBe("validation-error");
437
- if (screen.phase === "validation-error") {
438
- expect(screen.canRetry).toBe(false);
439
- }
440
- });
441
- });
442
-
443
- // ---------------------------------------------------------------------------
444
- // Transport error
445
- // ---------------------------------------------------------------------------
446
-
447
- describe("deriveValidationScreenState — transport-error", () => {
448
- test("shows transport-error for validate step network failure", () => {
449
- let state = advanceTo("validate");
450
- state = {
451
- ...state,
452
- steps: {
453
- ...state.steps,
454
- validate: {
455
- status: "error",
456
- error: {
457
- message: "validate: HTTP 500",
458
- code: "HTTP_500",
459
- retryable: true,
460
- },
461
- },
462
- },
463
- // No validateResult means it was a transport failure, not a validation failure
464
- };
465
- const screen = deriveValidationScreenState(state);
466
- expect(screen.phase).toBe("transport-error");
467
- if (screen.phase === "transport-error") {
468
- expect(screen.step).toBe("validate");
469
- expect(screen.error.message).toContain("HTTP 500");
470
- expect(screen.canRetry).toBe(true);
471
- }
472
- });
473
-
474
- test("shows transport-error for preflight step network failure", () => {
475
- let state = advanceTo("preflight-review");
476
- state = {
477
- ...state,
478
- steps: {
479
- ...state.steps,
480
- "preflight-review": {
481
- status: "error",
482
- error: {
483
- message: "import-preflight: HTTP 503",
484
- code: "HTTP_503",
485
- retryable: true,
486
- },
487
- },
488
- },
489
- };
490
- const screen = deriveValidationScreenState(state);
491
- expect(screen.phase).toBe("transport-error");
492
- if (screen.phase === "transport-error") {
493
- expect(screen.step).toBe("preflight-review");
494
- expect(screen.error.message).toContain("HTTP 503");
495
- expect(screen.canRetry).toBe(true);
496
- }
497
- });
498
-
499
- test("shows transport-error with 429 rate limit as retryable", () => {
500
- let state = advanceTo("validate");
501
- state = {
502
- ...state,
503
- steps: {
504
- ...state.steps,
505
- validate: {
506
- status: "error",
507
- error: {
508
- message: "validate: HTTP 429",
509
- code: "HTTP_429",
510
- retryable: true,
511
- },
512
- },
513
- },
514
- };
515
- const screen = deriveValidationScreenState(state);
516
- expect(screen.phase).toBe("transport-error");
517
- if (screen.phase === "transport-error") {
518
- expect(screen.canRetry).toBe(true);
519
- }
520
- });
521
- });
522
-
523
- // ---------------------------------------------------------------------------
524
- // Preflight error
525
- // ---------------------------------------------------------------------------
526
-
527
- describe("deriveValidationScreenState — preflight-error", () => {
528
- test("shows preflight-error when preflight validation fails", () => {
529
- let state = advanceTo("preflight-review");
530
- state = {
531
- ...state,
532
- steps: {
533
- ...state.steps,
534
- "preflight-review": {
535
- status: "error",
536
- error: {
537
- message: "Import preflight validation failed",
538
- code: "INCOMPATIBLE_VERSION",
539
- retryable: true,
540
- },
541
- },
542
- },
543
- preflightResult: makePreflightFailure(),
544
- };
545
- const screen = deriveValidationScreenState(state);
546
- expect(screen.phase).toBe("preflight-error");
547
- if (screen.phase === "preflight-error") {
548
- expect(screen.error.message).toContain("preflight");
549
- expect(screen.canRetry).toBe(true);
550
- }
551
- });
552
- });
553
-
554
- // ---------------------------------------------------------------------------
555
- // Success state
556
- // ---------------------------------------------------------------------------
557
-
558
- describe("deriveValidationScreenState — success", () => {
559
- test("shows success with full results when both steps pass", () => {
560
- const state = advanceTo("transfer");
561
- const screen = deriveValidationScreenState(state);
562
- expect(screen.phase).toBe("success");
563
- if (screen.phase === "success") {
564
- expect(screen.validation.isValid).toBe(true);
565
- expect(screen.validation.manifest.schema_version).toBe(1);
566
- }
567
- });
568
-
569
- test("includes preflight summary counts", () => {
570
- const state = advanceTo("transfer");
571
- const screen = deriveValidationScreenState(state);
572
- if (screen.phase === "success") {
573
- expect(screen.preflight.summary.totalFiles).toBe(5);
574
- expect(screen.preflight.summary.filesToCreate).toBe(2);
575
- expect(screen.preflight.summary.filesToOverwrite).toBe(2);
576
- expect(screen.preflight.summary.filesUnchanged).toBe(1);
577
- }
578
- });
579
-
580
- test("includes normalized file entries", () => {
581
- const state = advanceTo("transfer");
582
- const screen = deriveValidationScreenState(state);
583
- if (screen.phase === "success") {
584
- expect(screen.preflight.files).toHaveLength(5);
585
- const createFile = screen.preflight.files.find(
586
- (f) => f.path === "skills/new-skill.md",
587
- );
588
- expect(createFile).toBeDefined();
589
- expect(createFile!.action).toBe("create");
590
- expect(createFile!.bundleSize).toBe(512);
591
- expect(createFile!.currentSize).toBeNull();
592
- }
593
- });
594
-
595
- test("includes conflict entries", () => {
596
- const state = advanceTo("transfer");
597
- const screen = deriveValidationScreenState(state);
598
- if (screen.phase === "success") {
599
- expect(screen.preflight.conflicts).toHaveLength(1);
600
- expect(screen.preflight.conflicts[0].code).toBe("SIZE_MISMATCH");
601
- expect(screen.preflight.conflicts[0].path).toBe("config.json");
602
- }
603
- });
604
-
605
- test("canContinue is true when transfer step is accessible", () => {
606
- const state = advanceTo("transfer");
607
- const screen = deriveValidationScreenState(state);
608
- if (screen.phase === "success") {
609
- expect(screen.canContinue).toBe(true);
610
- }
611
- });
612
-
613
- test("success state persists when viewing from later steps", () => {
614
- const state = advanceTo("rebind-secrets");
615
- const screen = deriveValidationScreenState(state);
616
- expect(screen.phase).toBe("success");
617
- });
618
- });
619
-
620
- // ---------------------------------------------------------------------------
621
- // Retry behavior
622
- // ---------------------------------------------------------------------------
623
-
624
- describe("retryValidationFlow", () => {
625
- test("retries validate step after transport error", async () => {
626
- let state = advanceTo("validate");
627
- const validateSuccess = makeValidateSuccess();
628
- const preflightSuccess = makePreflightSuccess();
629
-
630
- // Simulate a failed validate step
631
- state = {
632
- ...state,
633
- steps: {
634
- ...state.steps,
635
- validate: {
636
- status: "error",
637
- error: {
638
- message: "validate: HTTP 500",
639
- code: "HTTP_500",
640
- retryable: true,
641
- },
642
- },
643
- },
644
- };
645
-
646
- // Set up sequential responses: validate success, then preflight success
647
- let callCount = 0;
648
- const sequentialFetch = (async () => {
649
- callCount++;
650
- if (callCount === 1) {
651
- return new Response(JSON.stringify(validateSuccess), {
652
- status: 200,
653
- headers: { "Content-Type": "application/json" },
654
- });
655
- }
656
- return new Response(JSON.stringify(preflightSuccess), {
657
- status: 200,
658
- headers: { "Content-Type": "application/json" },
659
- });
660
- }) as unknown as typeof fetch;
661
-
662
- const options = makeExecutorOptions({
663
- destConfig: runtimeConfig({ fetchFn: sequentialFetch }),
664
- });
665
-
666
- const result = await retryValidationFlow(state, options);
667
- expect(result.steps["validate"].status).toBe("success");
668
- expect(result.currentStep).toBe("transfer");
669
- });
670
-
671
- test("retries preflight step after error", async () => {
672
- let state = advanceTo("preflight-review");
673
- const preflightSuccess = makePreflightSuccess();
674
-
675
- state = {
676
- ...state,
677
- steps: {
678
- ...state.steps,
679
- "preflight-review": {
680
- status: "error",
681
- error: {
682
- message: "import-preflight: HTTP 500",
683
- code: "HTTP_500",
684
- retryable: true,
685
- },
686
- },
687
- },
688
- };
689
-
690
- const options = makeExecutorOptions({
691
- destConfig: runtimeConfig({
692
- fetchFn: mockFetch(200, preflightSuccess),
693
- }),
694
- });
695
-
696
- const result = await retryValidationFlow(state, options);
697
- expect(result.steps["preflight-review"].status).toBe("success");
698
- expect(result.currentStep).toBe("transfer");
699
- });
700
-
701
- test("throws when trying to retry non-retryable error", () => {
702
- let state = advanceTo("validate");
703
- state = {
704
- ...state,
705
- steps: {
706
- ...state.steps,
707
- validate: {
708
- status: "error",
709
- error: {
710
- message: "Fatal error",
711
- retryable: false,
712
- },
713
- },
714
- },
715
- };
716
-
717
- const options = makeExecutorOptions();
718
- expect(retryValidationFlow(state, options)).rejects.toThrow(
719
- "not in a retryable error state",
720
- );
721
- });
722
- });
723
-
724
- // ---------------------------------------------------------------------------
725
- // Validation flow execution
726
- // ---------------------------------------------------------------------------
727
-
728
- describe("executeValidationFlow", () => {
729
- test("runs validate and preflight in sequence on success", async () => {
730
- const state = advanceTo("validate");
731
- const validateSuccess = makeValidateSuccess();
732
- const preflightSuccess = makePreflightSuccess();
733
-
734
- let callCount = 0;
735
- const sequentialFetch = (async () => {
736
- callCount++;
737
- if (callCount === 1) {
738
- return new Response(JSON.stringify(validateSuccess), {
739
- status: 200,
740
- headers: { "Content-Type": "application/json" },
741
- });
742
- }
743
- return new Response(JSON.stringify(preflightSuccess), {
744
- status: 200,
745
- headers: { "Content-Type": "application/json" },
746
- });
747
- }) as unknown as typeof fetch;
748
-
749
- const stateChanges: MigrationWizardState[] = [];
750
- const options = makeExecutorOptions({
751
- destConfig: runtimeConfig({ fetchFn: sequentialFetch }),
752
- onStateChange: (s) => stateChanges.push(s),
753
- });
754
-
755
- const result = await executeValidationFlow(state, options);
756
-
757
- expect(result.steps["validate"].status).toBe("success");
758
- expect(result.steps["preflight-review"].status).toBe("success");
759
- expect(result.currentStep).toBe("transfer");
760
- expect(result.validateResult).toBeDefined();
761
- expect(result.preflightResult).toBeDefined();
762
-
763
- // State changes should include loading states
764
- const loadingStates = stateChanges.filter(
765
- (s) =>
766
- s.steps["validate"].status === "loading" ||
767
- s.steps["preflight-review"].status === "loading",
768
- );
769
- expect(loadingStates.length).toBeGreaterThan(0);
770
- });
771
-
772
- test("stops at validate when validation fails", async () => {
773
- const state = advanceTo("validate");
774
- const validateFailure = makeValidateFailure();
775
-
776
- const options = makeExecutorOptions({
777
- destConfig: runtimeConfig({
778
- fetchFn: mockFetch(200, validateFailure),
779
- }),
780
- });
781
-
782
- const result = await executeValidationFlow(state, options);
783
-
784
- expect(result.steps["validate"].status).toBe("error");
785
- expect(result.currentStep).toBe("validate");
786
- expect(result.validateResult).toEqual(validateFailure);
787
-
788
- const screen = deriveValidationScreenState(result);
789
- expect(screen.phase).toBe("validation-error");
790
- });
791
-
792
- test("stops at validate on transport error", async () => {
793
- const state = advanceTo("validate");
794
-
795
- const options = makeExecutorOptions({
796
- destConfig: runtimeConfig({
797
- fetchFn: mockFetch(500, "Internal server error"),
798
- }),
799
- });
800
-
801
- const result = await executeValidationFlow(state, options);
802
-
803
- expect(result.steps["validate"].status).toBe("error");
804
- expect(result.currentStep).toBe("validate");
805
-
806
- const screen = deriveValidationScreenState(result);
807
- expect(screen.phase).toBe("transport-error");
808
- });
809
- });
810
-
811
- // ---------------------------------------------------------------------------
812
- // Back navigation
813
- // ---------------------------------------------------------------------------
814
-
815
- describe("goBackToUpload", () => {
816
- test("navigates back to upload-bundle from validate", () => {
817
- const state = advanceTo("validate");
818
- const result = goBackToUpload(state);
819
- expect(result.currentStep).toBe("upload-bundle");
820
- expect(result.steps["validate"].status).toBe("idle");
821
- });
822
-
823
- test("navigates back to upload-bundle from preflight-review", () => {
824
- const state = advanceTo("preflight-review");
825
- const result = goBackToUpload(state);
826
- expect(result.currentStep).toBe("upload-bundle");
827
- expect(result.steps["validate"].status).toBe("idle");
828
- expect(result.steps["preflight-review"].status).toBe("idle");
829
- });
830
-
831
- test("clears validation and preflight results when going back", () => {
832
- let state = advanceTo("preflight-review");
833
- state = {
834
- ...state,
835
- validateResult: makeValidateSuccess(),
836
- preflightResult: makePreflightSuccess(),
837
- };
838
- const result = goBackToUpload(state);
839
- expect(result.validateResult).toBeUndefined();
840
- expect(result.preflightResult).toBeUndefined();
841
- });
842
- });
843
-
844
- // ---------------------------------------------------------------------------
845
- // File grouping
846
- // ---------------------------------------------------------------------------
847
-
848
- describe("groupFilesByAction", () => {
849
- test("groups files by action type", () => {
850
- const files: PreflightFileEntry[] = [
851
- {
852
- path: "a.txt",
853
- action: "create",
854
- bundleSize: 100,
855
- currentSize: null,
856
- bundleSha256: "a",
857
- currentSha256: null,
858
- },
859
- {
860
- path: "b.txt",
861
- action: "overwrite",
862
- bundleSize: 200,
863
- currentSize: 150,
864
- bundleSha256: "b",
865
- currentSha256: "b-old",
866
- },
867
- {
868
- path: "c.txt",
869
- action: "unchanged",
870
- bundleSize: 300,
871
- currentSize: 300,
872
- bundleSha256: "c",
873
- currentSha256: "c",
874
- },
875
- {
876
- path: "d.txt",
877
- action: "create",
878
- bundleSize: 400,
879
- currentSize: null,
880
- bundleSha256: "d",
881
- currentSha256: null,
882
- },
883
- ];
884
-
885
- const grouped = groupFilesByAction(files);
886
- expect(grouped.create).toHaveLength(2);
887
- expect(grouped.overwrite).toHaveLength(1);
888
- expect(grouped.unchanged).toHaveLength(1);
889
- expect(grouped.create[0].path).toBe("a.txt");
890
- expect(grouped.create[1].path).toBe("d.txt");
891
- expect(grouped.overwrite[0].path).toBe("b.txt");
892
- expect(grouped.unchanged[0].path).toBe("c.txt");
893
- });
894
-
895
- test("handles empty file list", () => {
896
- const grouped = groupFilesByAction([]);
897
- expect(grouped.create).toHaveLength(0);
898
- expect(grouped.overwrite).toHaveLength(0);
899
- expect(grouped.unchanged).toHaveLength(0);
900
- });
901
- });
902
-
903
- // ---------------------------------------------------------------------------
904
- // File size formatting
905
- // ---------------------------------------------------------------------------
906
-
907
- describe("formatFileSize", () => {
908
- test("formats bytes", () => {
909
- expect(formatFileSize(0)).toBe("0 B");
910
- expect(formatFileSize(100)).toBe("100 B");
911
- expect(formatFileSize(1023)).toBe("1023 B");
912
- });
913
-
914
- test("formats kilobytes", () => {
915
- expect(formatFileSize(1024)).toBe("1.0 KB");
916
- expect(formatFileSize(1536)).toBe("1.5 KB");
917
- expect(formatFileSize(10240)).toBe("10.0 KB");
918
- });
919
-
920
- test("formats megabytes", () => {
921
- expect(formatFileSize(1048576)).toBe("1.0 MB");
922
- expect(formatFileSize(5242880)).toBe("5.0 MB");
923
- });
924
-
925
- test("formats gigabytes", () => {
926
- expect(formatFileSize(1073741824)).toBe("1.0 GB");
927
- });
928
- });
929
-
930
- // ---------------------------------------------------------------------------
931
- // State persistence and resume
932
- // ---------------------------------------------------------------------------
933
-
934
- describe("validation screen state persistence/resume", () => {
935
- test("derives correct state after serialize + deserialize at validate step", () => {
936
- const state = advanceTo("validate");
937
- const json = serializeWizardState(state);
938
- const restored = deserializeWizardState(json);
939
- expect(restored).toBeDefined();
940
-
941
- // After deserialization, hasBundleData is false, so prepareForResume
942
- // should redirect to upload-bundle
943
- const resumed = prepareForResume(restored!);
944
- const screen = deriveValidationScreenState(resumed);
945
- // After resume without bundle data, goes back to upload-bundle
946
- expect(screen.phase).toBe("disabled");
947
- expect(resumed.currentStep).toBe("upload-bundle");
948
- });
949
-
950
- test("derives correct state after resume at preflight-review step with loading status", () => {
951
- let state = advanceTo("preflight-review");
952
- state = {
953
- ...state,
954
- steps: { ...state.steps, "preflight-review": { status: "loading" } },
955
- };
956
-
957
- const json = serializeWizardState(state);
958
- const restored = deserializeWizardState(json);
959
- expect(restored).toBeDefined();
960
-
961
- // prepareForResume resets loading to idle and redirects to upload-bundle
962
- // because hasBundleData is false after deserialization
963
- const resumed = prepareForResume(restored!);
964
- expect(resumed.currentStep).toBe("upload-bundle");
965
- });
966
-
967
- test("success results persist across serialize/deserialize", () => {
968
- let state = advanceTo("transfer");
969
- state = {
970
- ...state,
971
- validateResult: makeValidateSuccess(),
972
- preflightResult: makePreflightSuccess(),
973
- };
974
-
975
- const json = serializeWizardState(state);
976
- const restored = deserializeWizardState(json);
977
- expect(restored).toBeDefined();
978
-
979
- const screen = deriveValidationScreenState(restored!);
980
- expect(screen.phase).toBe("success");
981
- if (screen.phase === "success") {
982
- expect(screen.preflight.summary.totalFiles).toBe(5);
983
- }
984
- });
985
-
986
- test("error state is preserved after serialize/deserialize", () => {
987
- let state = advanceTo("validate");
988
- state = {
989
- ...state,
990
- steps: {
991
- ...state.steps,
992
- validate: {
993
- status: "error",
994
- error: {
995
- message: "Bundle validation failed",
996
- code: "INVALID_SCHEMA",
997
- retryable: true,
998
- },
999
- },
1000
- },
1001
- validateResult: makeValidateFailure(),
1002
- };
1003
-
1004
- const json = serializeWizardState(state);
1005
- const restored = deserializeWizardState(json);
1006
- expect(restored).toBeDefined();
1007
-
1008
- // After deserialization, hasBundleData is false. prepareForResume
1009
- // will redirect to upload-bundle since bundle data is needed.
1010
- // But the raw deserialized state (before resume) preserves the error.
1011
- const screen = deriveValidationScreenState(restored!);
1012
- expect(screen.phase).toBe("validation-error");
1013
- if (screen.phase === "validation-error") {
1014
- expect(screen.errors).toHaveLength(2);
1015
- }
1016
- });
1017
- });
1018
-
1019
- // ---------------------------------------------------------------------------
1020
- // State change callbacks
1021
- // ---------------------------------------------------------------------------
1022
-
1023
- describe("executeValidationFlow — onStateChange callbacks", () => {
1024
- test("emits state changes during validation flow", async () => {
1025
- const state = advanceTo("validate");
1026
- const validateSuccess = makeValidateSuccess();
1027
- const preflightSuccess = makePreflightSuccess();
1028
-
1029
- let callCount = 0;
1030
- const sequentialFetch = (async () => {
1031
- callCount++;
1032
- if (callCount === 1) {
1033
- return new Response(JSON.stringify(validateSuccess), {
1034
- status: 200,
1035
- headers: { "Content-Type": "application/json" },
1036
- });
1037
- }
1038
- return new Response(JSON.stringify(preflightSuccess), {
1039
- status: 200,
1040
- headers: { "Content-Type": "application/json" },
1041
- });
1042
- }) as unknown as typeof fetch;
1043
-
1044
- const stateChanges: ValidationScreenState[] = [];
1045
- const options = makeExecutorOptions({
1046
- destConfig: runtimeConfig({ fetchFn: sequentialFetch }),
1047
- onStateChange: (s) => stateChanges.push(deriveValidationScreenState(s)),
1048
- });
1049
-
1050
- await executeValidationFlow(state, options);
1051
-
1052
- // Should have gone through loading states
1053
- const phases = stateChanges.map((s) => s.phase);
1054
- expect(phases).toContain("loading");
1055
- });
1056
- });
1057
-
1058
- // ---------------------------------------------------------------------------
1059
- // Edge cases
1060
- // ---------------------------------------------------------------------------
1061
-
1062
- describe("validation screen edge cases", () => {
1063
- test("handles validate error without validateResult gracefully", () => {
1064
- let state = advanceTo("validate");
1065
- state = {
1066
- ...state,
1067
- steps: {
1068
- ...state.steps,
1069
- validate: {
1070
- status: "error",
1071
- error: {
1072
- message: "Network timeout",
1073
- retryable: true,
1074
- },
1075
- },
1076
- },
1077
- // No validateResult — pure transport error
1078
- };
1079
- const screen = deriveValidationScreenState(state);
1080
- expect(screen.phase).toBe("transport-error");
1081
- if (screen.phase === "transport-error") {
1082
- expect(screen.error.message).toBe("Network timeout");
1083
- }
1084
- });
1085
-
1086
- test("handles preflight error without preflightResult gracefully", () => {
1087
- let state = advanceTo("preflight-review");
1088
- state = {
1089
- ...state,
1090
- steps: {
1091
- ...state.steps,
1092
- "preflight-review": {
1093
- status: "error",
1094
- error: {
1095
- message: "Connection refused",
1096
- retryable: true,
1097
- },
1098
- },
1099
- },
1100
- // No preflightResult — pure transport error
1101
- };
1102
- const screen = deriveValidationScreenState(state);
1103
- expect(screen.phase).toBe("transport-error");
1104
- if (screen.phase === "transport-error") {
1105
- expect(screen.error.message).toBe("Connection refused");
1106
- }
1107
- });
1108
-
1109
- test("handles validate step with error but no error details", () => {
1110
- let state = advanceTo("validate");
1111
- state = {
1112
- ...state,
1113
- steps: {
1114
- ...state.steps,
1115
- validate: { status: "error" },
1116
- },
1117
- };
1118
- const screen = deriveValidationScreenState(state);
1119
- expect(screen.phase).toBe("transport-error");
1120
- if (screen.phase === "transport-error") {
1121
- expect(screen.error.message).toBe("Validation failed");
1122
- }
1123
- });
1124
-
1125
- test("handles complete wizard state — success still visible", () => {
1126
- const state = advanceTo("complete");
1127
- const screen = deriveValidationScreenState(state);
1128
- expect(screen.phase).toBe("success");
1129
- });
1130
- });