fix(core): preserve data integrity and bound shared UI and response work
Module Package Release / publish-packages (push) Successful in 13s

Release v0.1.46. Coordinated integrity review: GovOPlaN/govoplan-core#298.
This commit is contained in:
2026-09-08 12:30:38 +02:00
parent dc1f244f17
commit 6591aaa3fd
33 changed files with 1889 additions and 114 deletions
+57
View File
@@ -44,3 +44,60 @@ test("lightweight session changes trigger full auth refresh for required actions
assert.equal(sessionMatchesAuth({ ...session, user: { ...session.user, local_password: false } }, auth), false);
assert.equal(sessionMatchesAuth({ ...session, session_id: "rotated-session" }, auth), false);
});
const authorityContext = vm.createContext({ module: { exports: {} } });
authorityContext.exports = authorityContext.module.exports;
vm.runInContext(transformSync(readFileSync(new URL("../src/api/authAuthority.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code, authorityContext);
const { authAuthorityKey } = authorityContext.module.exports;
const settings = { apiBaseUrl: "https://fixture.invalid", apiKey: "fixture-key", accessToken: "fixture-token" };
test("authority fences ignore fresh object identity and cosmetic profile changes", () => {
const auth = normalizeAuthInfo(base);
const key = authAuthorityKey(auth, settings);
const refreshed = structuredClone(auth);
refreshed.user.display_name = "Changed display name";
refreshed.user.preferred_language = "de";
refreshed.user.ui_preferences = { compact_tables: true };
refreshed.tenant.name = "Changed tenant name";
refreshed.profile_loaded = true;
assert.equal(authAuthorityKey(refreshed, { ...settings }), key);
});
test("authority fences include principal, tenant, credential, scope, acting and required-action changes", () => {
const auth = normalizeAuthInfo(base);
const key = authAuthorityKey(auth, settings);
for (const mutate of [
(value) => { value.user.account_id = "different-account"; },
(value) => { value.user.email = "different@example.test"; },
(value) => { value.user.is_tenant_admin = true; },
(value) => { value.user.required_auth_action = "change_password"; },
(value) => { value.user.local_password = !value.user.local_password; },
(value) => { value.tenant.id = "different-tenant"; },
(value) => { value.active_tenant = { ...value.tenant, id: "active-tenant" }; },
(value) => { value.tenant.is_active = false; },
(value) => { value.scopes = ["new:permission"]; },
(value) => { value.roles = [{ id: "role", slug: "role", permissions: ["new:permission"] }]; },
(value) => { value.groups = [{ id: "group" }]; },
(value) => { value.principal.session_id = "rotated-session"; },
(value) => { value.principal.acting_assignment_id = "assignment"; },
(value) => { value.principal.acting_for_account_id = "actor"; },
(value) => { value.principal.delegation_ids = ["delegation"]; }
]) {
const changed = structuredClone(auth);
mutate(changed);
assert.notEqual(authAuthorityKey(changed, settings), key);
}
for (const field of ["apiBaseUrl", "apiKey", "accessToken"]) {
assert.notEqual(authAuthorityKey(auth, { ...settings, [field]: "changed" }), key);
}
});
test("authority set ordering and duplicate entries do not invent a context change", () => {
const auth = normalizeAuthInfo(base);
auth.scopes = ["b", "a", "b"];
auth.principal.group_ids = ["g2", "g1"];
const reordered = structuredClone(auth);
reordered.scopes = ["a", "b"];
reordered.principal.group_ids = ["g1", "g2", "g1"];
assert.equal(authAuthorityKey(auth, settings), authAuthorityKey(reordered, settings));
});
+38
View File
@@ -6,9 +6,47 @@ import { renderToStaticMarkup } from "react-dom/server";
import Button from "../src/components/Button";
import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../src/components/table/DataGrid";
import TableActionGroup, { runTableAction } from "../src/components/table/TableActionGroup";
import { dataGridRowIndices } from "../src/components/table/dataGridRowIndices";
function noop() {}
const repeatedRow = { label: "same" };
const distinctRow = { label: "same" };
const identityRows = [repeatedRow, distinctRow, repeatedRow];
const identityIndex = dataGridRowIndices(identityRows);
assertEqual(identityIndex(repeatedRow), 0, "repeated object references retain their first original index");
assertEqual(identityIndex(distinctRow), 1, "equal-looking objects keep distinct identities");
assertEqual(identityIndex({ label: "same" }), -1, "absent objects retain indexOf semantics");
const primitiveRows = [NaN, -0, 2, 2, 0];
const primitiveIndex = dataGridRowIndices(primitiveRows);
for (const value of primitiveRows) {
assertEqual(primitiveIndex(value), primitiveRows.indexOf(value), "primitive indices preserve NaN, zero and duplicate behavior");
}
let linearVisits = 0;
const measuredRows = Array.from({ length: 4_000 }, (_, index) => ({ index }));
measuredRows.forEach = (callback) => Array.prototype.forEach.call(measuredRows, (row, index, array) => {
linearVisits += 1;
callback(row, index, array);
});
const measuredIndex = dataGridRowIndices(measuredRows);
for (let pass = 0; pass < 10; pass += 1) measuredRows.map(measuredIndex);
assertEqual(linearVisits, 4_000, "index construction traverses once, independent of lookup count");
const callbackIndices: number[] = [];
const stableRows = [{ id: "a", rank: 2 }, { id: "b", rank: 1 }, { id: "c", rank: 1 }];
const stableMarkup = renderToStaticMarkup(<DataGrid
id="stable-original-row-index" rows={stableRows}
columns={[{ id: "rank", header: "Rank", sortValue: (row, index) => {
assertEqual(stableRows[index], row, "sort callbacks receive original indices");
return row.rank;
}, render: (row, index) => { callbackIndices.push(index); return row.id; } }]}
getRowKey={(row, index) => { assertEqual(stableRows[index], row, "row keys receive original indices"); return row.id; }}
initialSort={{ columnId: "rank", direction: "asc" }}
/>);
assertEqual(stableMarkup.includes("stable-original-row-index"), true, "sorted grid renders");
assertEqual(callbackIndices.slice(-3).join(), "1,2,0", "equal sort values remain stable without mutating source order after the unchanged sizing pass");
assertEqual(stableRows.map((row) => row.id).join(), "a,b,c", "sorting never reorders input data");
function buttonCount(markup: string): number {
return markup.match(/<button\b/g)?.length ?? 0;
}
+46
View File
@@ -1,4 +1,5 @@
import { importModuleWithRetry } from "../src/platform/moduleLoading";
import { createModuleRefresh } from "../src/platform/moduleRefresh";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
@@ -34,3 +35,48 @@ async function verifyModuleImportRetry() {
}
void verifyModuleImportRetry().catch((error) => { throw error; });
async function verifyModuleRefresh() {
const requests: Array<{ resolve: (value: number) => void; reject: (error: unknown) => void }> = [];
const accepted: number[] = [];
const errors: unknown[] = [];
let time = 0;
const refresh = createModuleRefresh({
load: () => new Promise<number>((resolve, reject) => requests.push({ resolve, reject })),
accept: (value) => accepted.push(value), reject: (error) => errors.push(error), now: () => time
});
const settle = async () => { await Promise.resolve(); await Promise.resolve(); };
refresh.invalidate();
refresh.invalidate();
refresh.invalidate();
assert(requests.length === 1, "mutation bursts cannot start overlapping requests");
requests[0].resolve(0);
await settle();
assert(accepted.length === 0 && Number(requests.length) === 2, "obsolete success is ignored and invalidation gets one trailing read");
refresh.invalidate();
requests[1].reject(new Error("obsolete"));
await settle();
assert(errors.length === 0 && Number(requests.length) === 3, "obsolete error cannot clear the current catalogue or drop invalidation");
requests[2].resolve(2);
await settle();
assert(accepted.join() === "2", "only the current generation publishes");
refresh.refreshVisible(true);
time = 6_000;
refresh.refreshVisible(false);
assert(Number(requests.length) === 3, "focus throttle and hidden-document checks remain");
refresh.refreshVisible(true);
assert(Number(requests.length) === 4, "visible focus refresh remains available after the throttle");
requests[3].reject("current failure");
await settle();
assert(errors.join() === "current failure", "current errors retain fail-closed handling without automatic retries");
refresh.invalidate();
refresh.invalidate();
refresh.dispose();
requests[4].resolve(4);
await settle();
refresh.invalidate();
assert(Number(requests.length) === 5 && accepted.join() === "2", "authority change/unmount discards both old response and queued work");
console.log("Module refresh coalescing and generation tests passed.");
}
void verifyModuleRefresh().catch((error) => { throw error; });