130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { createApplication } from "../../src/application/create-application.js";
|
|
import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.js";
|
|
import type { StoragePort } from "../../src/application/ports/storage-port.js";
|
|
|
|
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;
|
|
}) {
|
|
const current = release("build-a", "release-a");
|
|
return createApplication({
|
|
session: createAnonymousSessionAdapter(),
|
|
preferences: options.storage ?? memoryStorage(),
|
|
diagnostics: { emit: () => {} },
|
|
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 application = applicationWith({ reload });
|
|
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",
|
|
});
|
|
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",
|
|
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();
|
|
});
|
|
});
|