@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- 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
|
-
});
|