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