sync robustness: per-file retry with backoff, file-granular progress, bounded config fetch timeout, honest error details (bad origin / storage unavailable), isAvailable no longer gated on navigator.onLine (isolated LAN reports offline while origin is reachable)
This commit is contained in:
parent
bb40c96f98
commit
a0db9f7862
5 changed files with 156 additions and 14 deletions
|
|
@ -236,4 +236,16 @@ describe("app sync coordinator", () => {
|
|||
});
|
||||
expect(result.status).toBe("not-configured");
|
||||
});
|
||||
|
||||
it("returns an error outcome (never throws/hangs) for a plain-HTTP origin", async () => {
|
||||
const origin = buildOrigin();
|
||||
const httpConfig = { ...config(origin), origin: "http://10.144.0.221:8080/origin" };
|
||||
const result = await runSync({
|
||||
fetchConfig: async () => httpConfig,
|
||||
fetchImpl: fakeFetch(origin),
|
||||
appVersion: "1.0.0",
|
||||
});
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.status === "error" && result.detail).toContain("HTTPS");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,10 +48,13 @@ describe("Stage 9 HttpTransport", () => {
|
|||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reports availability from the browser online hint without making a request", () => {
|
||||
it("reports available even when the browser thinks it is offline (isolated LAN hub)", () => {
|
||||
expect(transport.isAvailable()).toBe(true);
|
||||
Object.defineProperty(navigator, "onLine", { configurable: true, value: false });
|
||||
expect(transport.isAvailable()).toBe(false);
|
||||
// A festival box has no upstream internet: onLine is false while the
|
||||
// origin is reachable. Availability must not gate the attempt — a truly
|
||||
// dead network fails fast at fetch time instead.
|
||||
expect(transport.isAvailable()).toBe(true);
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -164,6 +167,49 @@ describe("Stage 9 pull and verifier boundary", () => {
|
|||
).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
it("retries a transient network failure mid-download and reports progress", async () => {
|
||||
const keys = generateTestKeyPair();
|
||||
const built = buildPackage(makeValidInput(), { signWith: keys });
|
||||
if (!built.ok || !built.pkg.signature) throw new Error("fixture build failed");
|
||||
const manifestBytes = new TextEncoder().encode(canonicalJson(built.pkg.manifest));
|
||||
const files = new Map<string, Uint8Array>();
|
||||
files.set("/editions/lumen-2026/packages/1/manifest.json", manifestBytes);
|
||||
files.set(
|
||||
"/editions/lumen-2026/packages/1/signature.json",
|
||||
new TextEncoder().encode(JSON.stringify(built.pkg.signature)),
|
||||
);
|
||||
for (const [name, file] of built.pkg.files)
|
||||
files.set(`/editions/lumen-2026/packages/1/${name}`, file.canonicalBytes);
|
||||
for (const asset of built.pkg.assets)
|
||||
files.set(`/editions/lumen-2026/packages/1/${asset.file}`, asset.bytesContent);
|
||||
let emergencyFailures = 1;
|
||||
const seen: string[] = [];
|
||||
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
||||
const url = inputUrl(input);
|
||||
if (url.endsWith("/latest.json")) return Promise.resolve(response(pointer()));
|
||||
if (url.endsWith("/emergency.json") && emergencyFailures > 0) {
|
||||
emergencyFailures -= 1;
|
||||
return Promise.reject(new TypeError("wifi hiccup"));
|
||||
}
|
||||
const body = files.get(new URL(url).pathname);
|
||||
return Promise.resolve(body ? response(body) : response("missing", 404));
|
||||
});
|
||||
const result = await pullCandidate(
|
||||
new HttpTransport("https://festival.example/", { fetchImpl }),
|
||||
"lumen-2026",
|
||||
{
|
||||
trustedKeys: new Map([[keys.fingerprint, publicKeyFromDerBase64(keys.publicKeyDerBase64)]]),
|
||||
appVersion: "1.0.0",
|
||||
supportedSchemaRange: [1],
|
||||
},
|
||||
{ onProgress: (progress) => seen.push(progress.label) },
|
||||
);
|
||||
if (!result || "skipped" in result) throw new Error("expected candidate");
|
||||
expect(result.result.ok).toBe(true);
|
||||
expect(emergencyFailures).toBe(0);
|
||||
expect(seen).toContain("emergency.json");
|
||||
});
|
||||
|
||||
it("does not retrieve protected files when the signature gate fails", async () => {
|
||||
const keys = generateTestKeyPair();
|
||||
const built = buildPackage(makeValidInput(), { signWith: keys });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue