CI (SIAX Cloud) / quality (push) Successful in 50s
Security Scan (SIAX Cloud) / security (push) Successful in 45s
CI (SIAX Cloud) / security (push) Successful in 1m0s
CI (SIAX Cloud) / integration (push) Successful in 1m12s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m6s
CI (SIAX Cloud) / security (pull_request) Successful in 33s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / integration (pull_request) Successful in 47s
DRAFT -> PUBLISHED -> SUPERSEDED as a ledger (sql/010) rather than a column on
the frozen version: sql/008 already makes DashboardVersion immutable at the
database, and a lifecycle column there would have meant weakening that.
Rollback mints a NEW version carrying the old document and publishes it, so
"we went back to v3" is a timestamped fact (v5 = rollback_of_version 3) rather
than a hole where v4 used to be. It is verified by BI-P060 canonical hash, and
documentOf() refuses to serve a stored document whose hash no longer matches
the release — a dashboard that renders an unapproved definition is worse than
one that will not open.
The claim worth attacking is "only one release can be live". The application
check is read-then-write, so it is NOT the guarantee, and the evidence says so
in both directions against Postgres 16.15:
guard_absent partial unique index dropped -> BOTH concurrent publishes
commit, dashboard ends with TWO live versions
guard_present index in place -> loser fails 23505 on
opt_analytics_dashboard_release_one_published, one live
evidence/bi-p061/dashboard-release-race.json, reproducible with
scripts/bi-p061-release-race.mjs.
Two findings while writing the tests, both fixed here rather than papered over:
the one-draft-per-dashboard index makes the two-draft race impossible to even
construct (so the race is run as two create-and-publish transactions, which is
what a "save and publish" button does), and an UPDATE matching zero rows
raises nothing — which reads as "refused" to a careless test, so the
immutability test asserts the row exists first.
23 tests: 8 unit + 15 against live Postgres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
134 lines
5.6 KiB
JavaScript
134 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* BI-P061: is "only one published release per dashboard" actually enforced?
|
|
*
|
|
* The application checks for a live release before publishing, and that check
|
|
* is a read-then-write race. This script measures BOTH directions against a
|
|
* real Postgres and writes evidence/bi-p061/dashboard-release-race.json:
|
|
*
|
|
* guard_absent the partial unique index dropped -> both concurrent
|
|
* publishes commit, and the dashboard has TWO live versions.
|
|
* guard_present the index in place -> the loser fails with 23505.
|
|
*
|
|
* Without the first run, the second proves nothing: a race that never
|
|
* interleaved would also show one published release.
|
|
*
|
|
* ANALYTICS_TEST_DATABASE_URL=postgres://postgres:test@127.0.0.1:25441/opt \
|
|
* node scripts/bi-p061-release-race.mjs
|
|
*
|
|
* It CREATEs and DROPs its own throwaway schemas and touches nothing else.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import postgres from 'postgres';
|
|
|
|
const URL_ = process.env.ANALYTICS_TEST_DATABASE_URL;
|
|
if (!URL_) {
|
|
console.error('ANALYTICS_TEST_DATABASE_URL is required. Refusing to invent numbers.');
|
|
process.exit(2);
|
|
}
|
|
|
|
const STAMP = Date.now().toString(36);
|
|
const SCHEMAS = { guard_present: `bi_p061_ev_on_${STAMP}`, guard_absent: `bi_p061_ev_off_${STAMP}` };
|
|
const MIGRATIONS = ['sql/008_analytics_domain_model.sql', 'sql/010_analytics_dashboard_releases.sql'];
|
|
const HASH = (c) => c.repeat(64);
|
|
|
|
const connect = (schema, max = 1) =>
|
|
postgres(URL_, { max, onnotice: () => undefined, connection: { search_path: schema } });
|
|
|
|
async function setup(schema, { dropIndex }) {
|
|
const admin = postgres(URL_, { max: 1, onnotice: () => undefined });
|
|
await admin.unsafe(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
|
|
await admin.end({ timeout: 5 });
|
|
|
|
const c = connect(schema, 2);
|
|
for (const f of MIGRATIONS) await c.unsafe(fs.readFileSync(path.join(process.cwd(), f), 'utf8'));
|
|
if (dropIndex) await c.unsafe('DROP INDEX opt_analytics_dashboard_release_one_published');
|
|
await c.unsafe(`
|
|
INSERT INTO opt_analytics_dashboard_definition
|
|
(dashboard_id, tenant_id, project_id, name, current_version, owner_id)
|
|
VALUES ('dash_race', 't_alpha', 'p_main', 'Race', 1, 'u_1');
|
|
INSERT INTO opt_analytics_dashboard_version
|
|
(dashboard_version_id, dashboard_id, tenant_id, project_id, version, layout_json, layout_hash)
|
|
VALUES ('dv_1', 'dash_race', 't_alpha', 'p_main', 1, '{}', '${HASH('a')}'),
|
|
('dv_2', 'dash_race', 't_alpha', 'p_main', 2, '{}', '${HASH('b')}');`);
|
|
return c;
|
|
}
|
|
|
|
/** Both sessions read "nothing is live", then both write. */
|
|
async function race(schema) {
|
|
const conns = [connect(schema), connect(schema)];
|
|
const publish = (conn, n) =>
|
|
conn.begin(async (tx) => {
|
|
await tx.unsafe(
|
|
`SELECT release_id FROM opt_analytics_dashboard_release
|
|
WHERE dashboard_id='dash_race' AND state='published'`,
|
|
);
|
|
await new Promise((r) => setTimeout(r, 60));
|
|
await tx.unsafe(`
|
|
INSERT INTO opt_analytics_dashboard_release
|
|
(release_id, dashboard_id, dashboard_version_id, tenant_id, project_id, version, state,
|
|
document_hash, published_at)
|
|
VALUES ('rel_${n}', 'dash_race', 'dv_${n}', 't_alpha', 'p_main', ${n},
|
|
'published', '${HASH(n === 1 ? 'a' : 'b')}', now())`);
|
|
});
|
|
const results = await Promise.allSettled([publish(conns[0], 1), publish(conns[1], 2)]);
|
|
for (const c of conns) await c.end({ timeout: 5 });
|
|
return {
|
|
committed: results.filter((r) => r.status === 'fulfilled').length,
|
|
rejected: results
|
|
.filter((r) => r.status === 'rejected')
|
|
.map((r) => ({ code: r.reason?.code ?? null, constraint: r.reason?.constraint_name ?? null })),
|
|
};
|
|
}
|
|
|
|
async function publishedRows(conn) {
|
|
const rows = await conn.unsafe(
|
|
`SELECT release_id, version FROM opt_analytics_dashboard_release
|
|
WHERE dashboard_id='dash_race' AND state='published' ORDER BY version`,
|
|
);
|
|
return rows.map((r) => ({ release_id: r.release_id, version: r.version }));
|
|
}
|
|
|
|
const out = { recorded_at: new Date().toISOString() };
|
|
const cleanup = [];
|
|
try {
|
|
for (const [label, schema] of Object.entries(SCHEMAS)) {
|
|
const conn = await setup(schema, { dropIndex: label === 'guard_absent' });
|
|
cleanup.push([schema, conn]);
|
|
if (!out.postgres_version) {
|
|
const v = await conn.unsafe('SHOW server_version');
|
|
out.postgres_version = v[0].server_version;
|
|
}
|
|
const result = await race(schema);
|
|
out[label] = {
|
|
partial_unique_index_present: label === 'guard_present',
|
|
transactions_committed: result.committed,
|
|
rejections: result.rejected,
|
|
published_releases_after: await publishedRows(conn),
|
|
};
|
|
}
|
|
|
|
out.verdict = {
|
|
counterfactual_produced_two_live_dashboards: out.guard_absent.published_releases_after.length === 2,
|
|
guard_left_exactly_one: out.guard_present.published_releases_after.length === 1,
|
|
loser_error_code: out.guard_present.rejections[0]?.code ?? null,
|
|
application_check_alone_is_sufficient: false,
|
|
note:
|
|
'DashboardReleaseService.publish() reads for a live release before writing. Both sessions in ' +
|
|
'guard_absent passed that check and both committed. The partial unique index is what holds.',
|
|
};
|
|
|
|
const dir = path.join(process.cwd(), 'evidence/bi-p061');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const file = path.join(dir, 'dashboard-release-race.json');
|
|
fs.writeFileSync(file, `${JSON.stringify(out, null, 2)}\n`);
|
|
console.log(`wrote ${file}`);
|
|
console.log(JSON.stringify(out.verdict, null, 2));
|
|
} finally {
|
|
for (const [schema, conn] of cleanup) {
|
|
await conn.unsafe(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
|
|
await conn.end({ timeout: 5 });
|
|
}
|
|
}
|