Files
clean-architecture-frontend…/tests/unit/chunk-recovery-runtime.test.ts
T
2026-08-01 19:39:59 +09:00

157 lines
4.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createApplication } from "../../src/application/create-application.ts";
import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts";
import type { StoragePort } from "../../src/application/ports/storage-port.ts";
import type { DiagnosticsPort } from "../../src/application/ports/diagnostics-port.ts";
import type { TelemetryPort } from "../../src/application/ports/telemetry-port.ts";
import { createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
type ReleaseFixture = {
buildId: string;
releaseId: string;
configSchemaVersion: string;
apiContractVersion: string;
assetManifestHash: string;
routeChunks: Record<string, string>;
};
function release(buildId: string, releaseId: string): ReleaseFixture {
return {
buildId,
releaseId,
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: `${buildId}-assets`,
routeChunks: { "route-home": `assets/${buildId}-home.js` },
};
}
function memoryStorage(): StoragePort {
let value: unknown;
return {
read: () => ({ ok: true, value }),
write: (_name, next) => {
value = next;
return { ok: true };
},
remove: () => ({ ok: true }),
};
}
function applicationWith(options: {
storage?: StoragePort;
refresh?: () => Promise<ReturnType<typeof release>>;
reload?: () => void;
diagnostics?: DiagnosticsPort;
telemetry?: TelemetryPort;
}) {
const current = release("build-a", "release-a");
return createApplication({
session: createAnonymousSessionAdapter(),
preferences: options.storage ?? memoryStorage(),
diagnostics: options.diagnostics ?? { record: () => {} },
telemetry: options.telemetry ?? { emit: () => {} },
runtimeCapabilities: createRuntimeCapabilitiesStub(),
releaseInfo: {
getCurrent: async () => current,
refresh:
options.refresh ??
(async () => release("build-b", "release-b")),
},
navigation: { reload: options.reload ?? (() => {}) },
});
}
describe("production chunk recovery application input", () => {
it("reloads exactly once for one active build/release pair", async () => {
const reload = vi.fn();
const record = vi.fn<DiagnosticsPort["record"]>();
const emit = vi.fn<TelemetryPort["emit"]>();
const application = applicationWith({
reload,
diagnostics: { record },
telemetry: { emit },
});
const input = {
chunkId: "route-home",
failureKind: "CHUNK_LOAD_FAILURE" as const,
};
await expect(application.recovery.recoverChunk(input)).resolves.toEqual({
action: "reload-once",
releasePair: "build-a/release-a->build-b/release-b",
});
expect(record).toHaveBeenCalledWith({
level: "warn",
eventId: "release.mismatch.detected",
context: {
build_id: "build-a",
active_release_id: "release-b",
mismatch_kind: "BUILD_MISMATCH",
},
});
expect(emit).toHaveBeenCalledWith("release.mismatch.detected", {
build_id: "build-a",
active_release_id: "release-b",
mismatch_kind: "BUILD_MISMATCH",
});
await expect(application.recovery.recoverChunk(input)).resolves.toEqual({
action: "support",
reason: "reload-already-attempted",
});
expect(reload).toHaveBeenCalledOnce();
});
it.each([
[
{
refresh: async () => {
throw new Error("offline");
},
},
"manifest-unavailable",
],
[
{
refresh: async () => ({
...release("build-b", "release-b"),
routeChunks: {},
}),
},
"active-chunk-unknown",
],
[
{
storage: {
read: () => ({
ok: false as const,
error: {
kind: "STORAGE_UNAVAILABLE" as const,
code: "STORAGE_UNAVAILABLE",
retryable: false,
operationId: "STORAGE",
attemptCount: 1,
userMessageKey: "error.storage_unavailable",
action: "none" as const,
},
}),
write: () => ({ ok: true as const }),
remove: () => ({ ok: true as const }),
},
},
"guard-read-failed",
],
])("fails closed for recovery dependency case %#", async (options, reason) => {
const reload = vi.fn();
const application = applicationWith({ ...options, reload });
await expect(
application.recovery.recoverChunk({
chunkId: "route-home",
failureKind: "CHUNK_LOAD_FAILURE",
}),
).resolves.toEqual({ action: "support", reason });
expect(reload).not.toHaveBeenCalled();
});
});