Module Package Release / publish-packages (push) Successful in 12s
Release v0.1.26. Coordinated integrity review: GovOPlaN/govoplan-core#298.
259 lines
13 KiB
JavaScript
Executable File
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"/);
|
|
});
|