Files
govoplan-datasources/webui/scripts/test-detail-refresh.mjs
T
zemion 9d067f1bad
Module Package Release / publish-packages (push) Successful in 12s
fix(datasources): preserve governed CSV originals and fresh detail state
Release v0.1.26. Coordinated integrity review: GovOPlaN/govoplan-core#298.
2026-09-08 12:19:38 +02:00

259 lines
13 KiB
JavaScript
Executable File

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import test from "node:test";
const require = createRequire(new URL("../../../govoplan-core/webui/package.json", import.meta.url));
const { transformSync } = require("esbuild");
const page = readFileSync(new URL("../src/features/datasources/DatasourcesPage.tsx", import.meta.url), "utf8");
function extract(startMarker, endMarker) {
const start = page.indexOf(startMarker);
const end = page.indexOf(endMarker, start);
assert.ok(start >= 0 && end > start, "test the actual page closures");
return page.slice(start + startMarker.length, end);
}
const reloadBody = extract(" const reload = useCallback(async (preferredDatasourceRef?: string) => {", " }, [dialogScope]);");
const detailBody = extract(" useEffect(() => {\n if (!selectedDatasourceRef || loading || !catalogueReady)", " }, [selectedDatasourceRef, detailScope, loading, catalogueReady]);");
function evaluate(body, bindings) {
const code = transformSync(body, { loader: "ts", target: "es2022" }).code;
return new Function(...Object.keys(bindings), code)(...Object.values(bindings));
}
const deferred = () => {
let resolve, reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
};
const settle = async () => { await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); };
function harness() {
const state = { selectedDatasourceRef: "source-1", loading: false, detailRevision: 0, preview: null, materializations: [], detailLoading: false, catalogueAuthority: 0 };
const settings = {};
const auth = {};
const bindings = {
settings, auth, reloadRequestId: { current: 0 }, authority: { current: "authority-A" }, authorityKey: "authority-A", dialogScope: 0, authorityEpoch: { current: { revision: 0 } },
currentDetailScope: { current: null }, apiErrorMessage: String,
listDatasources: async () => [{ ref: "source-1" }], listDatasourceStages: async () => [],
listDatasourceOrigins: async () => ({ origins: [], available: false })
};
bindings.isCurrentAuthority = () => bindings.authority.current === "authority-A" && bindings.authorityEpoch.current.revision === 0;
for (const key of ["loading", "error", "datasources", "stages", "origins", "originsAvailable", "detailRevision", "selectedDatasourceRef", "selectedStageRef", "selectedOriginRef", "preview", "materializations", "detailLoading", "detailResponseScope", "catalogueAuthority", "working", "success", "view", "addOpen", "freezeOpen", "freezeLabel", "retireOpen", "promoteOpen", "governanceOpen", "decisionOpen", "retentionOpen"]) {
bindings[`set${key[0].toUpperCase()}${key.slice(1)}`] = (value) => { state[key] = typeof value === "function" ? value(state[key]) : value; };
}
return {
state, bindings,
reload: (preferredDatasourceRef) => evaluate(`return (async () => {${reloadBody}})();`, { ...bindings, preferredDatasourceRef }),
startDetail: () => {
const preview = deferred();
const history = deferred();
const scope = {};
bindings.currentDetailScope.current = scope;
const cleanup = evaluate(`if (!selectedDatasourceRef || loading || !catalogueReady)${detailBody}`, {
...bindings, selectedDatasourceRef: state.selectedDatasourceRef, loading: state.loading, detailScope: scope,
catalogueReady: state.catalogueAuthority === bindings.authorityEpoch.current.revision,
previewDatasource: () => preview.promise, listDatasourceMaterializations: () => history.promise
});
return { preview, history, scope, cleanup };
}
};
}
test("same-reference reload invalidates preview/history once, including freeze with an unchanged current materialization", async () => {
const h = harness();
for (const operation of ["manual reload", "refresh", "freeze", "promotion"]) {
const previous = h.state.detailRevision;
await h.reload("source-1");
assert.equal(h.state.selectedDatasourceRef, "source-1", operation);
assert.equal(h.state.detailRevision, previous + 1, operation);
const detail = h.startDetail();
detail.preview.resolve({ rows: [{ value: operation }] });
detail.history.resolve([{ revision: h.state.detailRevision }]);
await settle();
assert.equal(h.state.preview.rows[0].value, operation);
assert.equal(h.state.materializations[0].revision, h.state.detailRevision);
assert.equal(h.state.detailResponseScope, detail.scope);
detail.cleanup();
}
});
test("a changed scope immediately suppresses old preview, history and errors, even before effect cleanup", async () => {
const h = harness();
const old = h.startDetail();
const current = h.startDetail();
current.preview.resolve({ rows: [{ value: "current" }] });
current.history.resolve([{ revision: 2 }]);
await settle();
old.preview.reject(new Error("stale error"));
old.history.resolve([{ revision: 1 }]);
await settle();
assert.equal(h.state.preview.rows[0].value, "current");
assert.equal(h.state.materializations[0].revision, 2);
assert.equal(h.state.error, undefined);
current.cleanup();
});
test("unmount cancellation and partial detail failures preserve allSettled behavior", async () => {
const h = harness();
const cancelled = h.startDetail();
cancelled.cleanup();
cancelled.preview.resolve({ rows: ["obsolete"] });
cancelled.history.resolve([]);
await settle();
assert.equal(h.state.preview, null);
const current = h.startDetail();
current.preview.reject(new Error("current preview denied"));
current.history.resolve([{ revision: 3 }]);
await settle();
assert.equal(h.state.preview, null);
assert.equal(h.state.materializations[0].revision, 3);
assert.match(h.state.error, /current preview denied/);
assert.equal(h.state.detailLoading, false);
});
test("initial/loading catalogue does not issue duplicate detail reads; scoped rendering cannot flash old rows", () => {
const h = harness();
h.state.loading = true;
const detail = h.startDetail();
assert.equal(detail.cleanup, undefined);
assert.equal(h.state.preview, null);
assert.match(page, /useMemo\(\(\) => \(\{\}\), \[selectedDatasourceRef, dialogScope, detailRevision\]\)/);
assert.match(page, /preview=\{!loading && detailResponseScope === detailScope \? preview : null\}/);
assert.match(page, /materializations=\{!loading && detailResponseScope === detailScope \? materializations : \[\]\}/);
});
test("older catalogue reloads and revoked authority cannot invalidate or replace the current catalogue", async () => {
const h = harness();
const older = deferred();
h.bindings.listDatasources = () => older.promise;
const pending = h.reload();
h.bindings.listDatasources = async () => [{ ref: "source-1", revision: 2 }];
await h.reload();
older.resolve([{ ref: "source-1", revision: 1 }]);
await pending;
assert.equal(h.state.datasources[0].revision, 2);
assert.equal(h.state.detailRevision, 1);
const revoked = deferred();
h.bindings.listDatasources = () => revoked.promise;
const revokedPending = h.reload();
h.bindings.authority.current = "authority-B";
revoked.resolve([{ ref: "source-1", revision: 3 }]);
await revokedPending;
assert.equal(h.state.datasources[0].revision, 2);
});
test("an old reload invoked only after B is active cannot issue requests or invalidate B state", async () => {
const h = harness();
const oldReload = h.reload;
h.bindings.authority.current = "authority-B";
h.bindings.authorityEpoch.current.revision = 1;
h.state.catalogueAuthority = 1;
h.state.datasources = [{ ref: "B-source" }];
h.state.detailRevision = 8;
let calls = 0;
h.bindings.listDatasources = async () => { calls += 1; return [{ ref: "A-source" }]; };
const requestId = h.bindings.reloadRequestId.current;
await oldReload();
assert.equal(calls, 0);
assert.equal(h.bindings.reloadRequestId.current, requestId);
assert.equal(h.state.datasources[0].ref, "B-source");
assert.equal(h.state.detailRevision, 8);
});
test("the first authority-transition render cannot request details from a stale catalogue", () => {
const h = harness();
h.bindings.authority.current = "authority-B";
h.bindings.authorityEpoch.current.revision = 1;
h.state.loading = false; // The new reload's effect has not committed loading=true yet.
const detail = h.startDetail();
assert.equal(detail.cleanup, undefined);
assert.equal(h.state.detailLoading, false);
assert.match(page, /const datasources = catalogueReady \? loadedDatasources : \[\]/);
assert.match(page, /const stages = catalogueReady \? loadedStages : \[\]/);
});
test("real-authority setup closes confirmations and clears freeze input; unmount blocks old callbacks", () => {
const h = harness();
const body = extract(" useEffect(() => {\n authority.current = authorityKey;", " }, [reload]);");
const flags = ["addOpen", "freezeOpen", "retireOpen", "promoteOpen", "governanceOpen", "decisionOpen", "retentionOpen"];
for (const flag of flags) {
h.state[flag] = true;
// Hide the old intent already in the first transition render, before the
// passive effect clears it; it cannot be clicked against a new selection.
assert.ok(page.includes(`open={${flag} && catalogueReady}`), flag);
}
h.state.freezeLabel = "Evidence from prior authority";
let reloads = 0;
const cleanup = evaluate(`authority.current = authorityKey;${body}`, {
...h.bindings, reload: () => { reloads += 1; }
});
assert.equal(reloads, 1);
for (const flag of flags) assert.equal(h.state[flag], false, flag);
assert.equal(h.state.freezeLabel, "");
assert.equal(h.bindings.isCurrentAuthority(), true);
cleanup();
assert.equal(h.bindings.isCurrentAuthority(), false);
// The reload callback/effect depend on the stable numeric authority epoch,
// not cosmetic settings/auth object identities.
assert.match(page, /\}, \[dialogScope\]\);\s*useEffect/);
});
test("stale mutation success/failure/finally cannot reload, select or overwrite B messages", async () => {
for (const operation of ["promoteStage", "refreshSelected", "freezeSelected", "retireSelected"]) {
for (const outcome of ["resolve", "reject"]) {
const h = harness();
const pending = deferred();
let reloads = 0;
const signature = operation === "freezeSelected" ? "async (): Promise<boolean>" : "async ()";
const body = extract(` const ${operation} = ${signature} => {`, "\n };");
const result = evaluate(`return (async () => {${body}})();`, {
...h.bindings,
selectedDatasource: { ref: "A-source", governance: { approval_policy: { required: false } } },
selectedStage: { ref: "A-stage", state: "ready" }, freezeLabel: "Freeze A",
reload: async () => { reloads += 1; },
promoteDatasourceStage: () => pending.promise, refreshDatasource: () => pending.promise,
freezeDatasource: () => pending.promise, retireDatasource: () => pending.promise
});
h.bindings.authority.current = "authority-B";
Object.assign(h.state, { success: "B success", error: "B error", view: "origins", working: true });
if (outcome === "reject") pending.reject(new Error("A error"));
else pending.resolve({ datasource: { ref: "A-source", name: "A" }, materialization: { revision: 1 }, revision: 1 });
await result;
assert.equal(reloads, 0, operation);
assert.equal(h.state.success, "B success", operation);
assert.equal(h.state.error, "B error", operation);
assert.equal(h.state.view, "origins", operation);
assert.equal(h.state.working, true, operation);
}
}
});
test("CSV stage dialog sends its explicit mode and exact text; JSON rows remain independent", async () => {
const dialog = page.slice(page.indexOf("function AddDatasourceDialog("));
const start = dialog.indexOf(" const create = async (): Promise<boolean> => {");
const end = dialog.indexOf("\n };", start);
assert.ok(start >= 0 && end > start);
const body = dialog.slice(start + " const create = async (): Promise<boolean> => {".length, end);
const csvText = 'code;value\r\n001;" text "\r\n';
for (const format of ["csv", "json"]) {
for (const csvValueMode of ["text", "legacy_typed"]) {
let payload;
const result = await evaluate(`return (async () => {${body}})();`, {
kind: "upload", settings: {}, format, csvValueMode, csvText, delimiter: ";",
mode: "static", name: "Fixture", sourceName: "fixture", description: "", targetRef: "",
rowsText: '[{"code":"001","value":" text "}]', parseRows: JSON.parse,
createDatasourceStage: async (_settings, value) => { payload = value; return {}; },
onCreated: async () => {}, setBusy: () => {}, setError: () => {}, apiErrorMessage: String
});
assert.equal(result, true);
if (format === "csv") {
assert.equal(payload.csv_value_mode, csvValueMode);
assert.equal(payload.csv_text, csvText);
} else {
assert.equal("csv_value_mode" in payload, false);
assert.deepEqual(payload.rows, [{ code: "001", value: " text " }]);
}
}
}
assert.match(dialog, /\[csvValueMode, setCsvValueMode\] = useState<"text" \| "legacy_typed">\("text"\)/);
assert.match(dialog, /setCsvValueMode\("text"\)/);
assert.match(dialog, /csvValueMode: "text"/);
});