Files
Voiced/src/selftestBoot.ts
T
xavierk aec24be680 Add unattended end-to-end self-test mode for the production webview
VOICED_SELFTEST_OUT runs the Diagnostics checks plus an issue, archive,
export (searchable, flattened, re-render), cancel and archive-immutability
flow through the real IPC, writes a JSON report and exits. Inert without
the variable; doubles as the per-release gate on installed builds.
2026-10-04 06:17:04 +05:30

87 lines
3.1 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
import type { SelfTestReport } from "./lib/selfTest";
import type { StepResult } from "./lib/selfTestSteps";
/**
* Unattended self-test mode, enabled by the VOICED_SELFTEST_OUT environment variable (the backend
* returns the report path, or null in normal use). It runs the diagnostics and an end-to-end invoice
* flow in this very webview, writes a JSON report and makes the process exit with the verdict.
*/
const WATCHDOG_MS = 120_000;
export async function maybeRunSelfTest(): Promise<void> {
let reportPath: string | null = null;
try {
reportPath = await invoke<string | null>("selftest_config");
} catch {
return; // Not running inside Tauri (plain browser dev server).
}
if (!reportPath) return;
const path = reportPath;
const start = performance.now();
const startedAt = new Date().toISOString();
let diagnostics: SelfTestReport | null = null;
let steps: StepResult[] = [];
let renderMode: () => string = () => "unknown";
let finished = false;
const finish = async (ok: boolean, error?: string) => {
if (finished) return;
finished = true;
const body = {
ok,
...(error ? { error } : {}),
startedAt,
totalMs: Math.round(performance.now() - start),
userAgent: navigator.userAgent,
renderMode: renderMode(),
steps,
diagnostics,
};
try {
await invoke("selftest_report", { json: JSON.stringify(body, null, 2), ok });
} catch (e) {
console.error("self-test report could not be written", e);
}
};
const failWith = (what: string) => void finish(false, what);
const timer = window.setTimeout(() => {
if (finished) return;
void finish(false, "timeout");
}, WATCHDOG_MS);
window.addEventListener("error", (e) => failWith(`uncaught error: ${e.message}`));
window.addEventListener("unhandledrejection", (e) => failWith(`unhandled rejection: ${String(e.reason)}`));
try {
const [{ createStepLog }, { runSelfTest }, { browserSelfTestDeps }, { runE2eSteps }, { getRenderClient }] = await Promise.all([
import("./lib/selfTestSteps"),
import("./lib/selfTest"),
import("./lib/selfTestBrowser"),
import("./lib/selfTestE2e"),
import("./lib/renderClient"),
]);
renderMode = () => getRenderClient().mode;
const log = createStepLog();
steps = log.steps;
await log.run("diagnostics: render, rasterise, fingerprint, flatten", async () => {
const report = await runSelfTest(browserSelfTestDeps(), (r) => {
diagnostics = r;
});
diagnostics = report;
const failed = report.sections.flatMap((s) => s.checks.filter((c) => c.status === "fail").map((c) => `${s.title} / ${c.label}: ${c.detail ?? ""}`));
return { ok: report.passed, detail: failed.length ? failed.join("\n") : "all checks passed" };
});
await runE2eSteps(log, path);
window.clearTimeout(timer);
await finish(log.steps.every((s) => s.ok));
} catch (e) {
window.clearTimeout(timer);
await finish(false, e instanceof Error ? `${e.name}: ${e.message}\n${e.stack ?? ""}` : String(e));
}
}