perf(test): clone PostgreSQL test databases from a run template - #51
Merged
Conversation
tnunamak
force-pushed
the
port/gate-speed-278-0902
branch
3 times, most recently
from
September 6, 2026 13:21
bf4bff2 to
676af18
Compare
Port of PDP-Connect/pdpp PR #278 (head 45df5b31e) into this repository. pdpp froze reference-implementation/ for direct edits because the reference server now lives here, so the change lands here instead. Under the postgres test profile (the same test files run against a real PostgreSQL database instead of the in-memory store), run-tests.ts (the runner that spawns one child process per test file) creates a throwaway database for every file by replaying ~2000 lines of schema DDL. That replay is identical for every file and is most of each file's setup cost. The runner now builds that schema once per run into a PostgreSQL template database (scripts/postgres-test-template.ts) and creates each file's database with CREATE DATABASE ... TEMPLATE, a filesystem copy of the built tables and indexes. A missing or unusable template throws; there is no silent fallback to a from-scratch build. Templating is opt-in per file. scripts/postgres-template-eligibility.ts lists which files may clone and which must bootstrap cold (tests that exercise first boot, migration ordering and recovery, migration receipts, bootstrap serialization, deadlock retry, or first-run admission). test/postgres-template-eligibility-inventory.test.ts fails when a Postgres-touching test file is on neither list, so new files default to a cold database until triaged. The helper withTemporaryPostgresDatabase applies the same allowlist for databases created inside a test file. The template is bound to the code that built it: a metadata row on the admin database records a digest of server/postgres-storage.ts, and assertPostgresTestTemplateUsable refuses a template whose digest does not match the running code. test/postgres-test-template-identity.test.ts and test/postgres-template-eligibility-migration-mutation-control.test.ts prove the refusal and the cold-bootstrap path against live PostgreSQL. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…ment The independent review of pdpp PR #278 found that the template metadata row written by scripts/postgres-test-template.ts stored a runner id and a build time that the clone-time check never read back: only the source digest of server/postgres-storage.ts was compared, so a template whose row carried a wrong runner id and a correct digest was accepted. The row now also records a schema version (a digest of the tables, columns, and indexes the built template actually carries, computed while it still accepts connections) and an identity digest over the template name, runner id, schema version, source digest, and the stored build time. assertPostgresTestTemplateUsable, the check every env-var-received template passes through before CREATE DATABASE ... TEMPLATE, now refuses a template on any of: a runner id that differs from the one encoded in the template name; a source digest that differs from this process's postgres-storage.ts; a missing schema version or identity digest (rows from the older builder); an identity digest that does not recompute from the row (an altered build time or schema version); or, when the caller holds an identity token, a token that does not match the row. scripts/run-tests.ts reads that token after building the template and hands it to every child as PDPP_TEST_POSTGRES_TEMPLATE_IDENTITY, and withTemporaryPostgresDatabase passes it to the check by default, so a child clones only the build its own run produced. The table gains the two columns with ADD COLUMN IF NOT EXISTS, so a shared cluster that already has the three-column table keeps working and its old rows are refused rather than trusted. test/postgres-test-template-identity.test.ts gains four cases: runner id altered alone (the review's reproduction), build time altered alone, schema version altered alone, and a wrong identity token; each must be refused, and the matching token must pass. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Launch the PostgreSQL race fixture with the TypeScript loader and surface child failures to pending line-protocol readers. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Record the invalid initial run, the corrected oracle, the watchdog repair verification, and the terminal replacement-profile receipt. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Record the terminal Node 22 and PostgreSQL 16 profile receipt, clean-main failure replay, restored listener, and archival push blocker. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Run the migration mutation control through the default file-identity selector and prove a broadened selector clones the corrupt template. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Correct the profile report after publishing the signed local commits to the canonical PR remote. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Route the runner's per-file PostgreSQL clone through clonePostgresTestDatabaseFromTemplate so the full template-identity check runs in the same admin session immediately before CREATE DATABASE ... TEMPLATE. A missing identity is a hard error; there is no cold fallback once a template clone has been selected. Add two live runner-path regressions: one mutates only the stored template runner ID, the other gives the clone a wrong expected identity token. Each is refused at clone time, and each asserts the intended clone database does not exist after refusal. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
The template's identity was taken from whatever database already sat under the expected name, rather than being decided by the run that built it. Three things combined to make that unsafe. The run nonce was randomBytes(4) -- 32 bits. Template names derive from it, so a same-name collision on a shared cluster is a birthday problem at roughly 77k runs, not a negligible one. Widen the template nonce to 16 bytes. It is a new value rather than a wider runnerId because runnerId is also embedded in per-file database names, whose authorization grammar in test/helpers/dedicated-postgres-test-url.ts pins it at exactly 8 hex chars. ensurePostgresTestTemplate returned early when a database under the derived name was already datistemplate/datallowconn-usable, without checking that this run built it. A pre-existing database under a freshly generated nonce's name is now an error: this run did not create it, so nothing it later verifies about it means anything. The builder returned only the template name, so run-tests.ts learned the expected identity by reading the candidate back via readPostgresTestTemplateIdentity -- sourcing the expectation from the thing under verification, which is circular. The identity is now computed and committed before publication, and ensurePostgresTestTemplate returns it. The read-back that remains only CONFIRMS the published row reproduces the precommitted digest; it never sources it. Because the digest covers built_at as the table renders it, the builder asks the server to render its chosen instant before hashing, so build-time and clone-time recomputation agree byte-for-byte. Proof: three new assertions in test/postgres-test-template-provenance.test.ts fail before this change (the nonce is 4 bytes; a pre-existing template is adopted; the builder returns a bare string). Mutation checks: reverting the nonce to 4 bytes fails only the nonce assertion, and restoring the adopt-if-usable early return fails only the adoption assertion. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
The two tests named "runner per-file clone ..." called
clonePostgresTestDatabaseFromTemplate directly. They prove the clone helper
is fail-closed, but they never executed the line that decides whether a file
is cloned at all:
const useTemplate =
postgresTestTemplateName !== null && isPostgresTemplateEligibleFilePath(filePath);
The second half is the fail-closed default: a file not on the eligibility
allowlist must get a from-scratch bootstrap even when a template exists.
Nothing invoked run-tests.ts, so deleting that half failed no test. Rename
those two to "clone helper ..." so they no longer claim coverage they do not
have, and add a scope note pointing at the new file.
test/run-tests-postgres-template-selection.test.ts runs the real runner as a
subprocess against live PostgreSQL, selecting one cold-required file
(device-ingest-reservation-migration.test.ts, which drops a table and re-runs
migrations against the runner-allocated database). It asserts the premise --
that the file is cold-required and not eligible -- so a registry
reorganisation fails loudly instead of silently testing nothing.
Asserting only on the file's outcome is not enough: the run's template
carries a correct schema, so a wrongly cloned cold-required file still
passes, which is exactly why this defect stayed invisible. The runner now
reports which path it took per file (PDPP_TEST_DB_PROVISION, cold-bootstrap
vs template-clone) and the test asserts on that. The decision itself is the
thing under test, and it was previously unobservable from outside.
The test asserts on the selected file's own result rather than the runner's
exit code, because a one-file authority leaves suite-wide skip-mapping
accounting unsatisfied for unrelated reasons.
Proof: the mutation the review named -- useTemplate = postgresTestTemplateName
!== null -- now fails with 'the runner reported "template-clone"'. Unmutated,
it passes.
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
…ecks computePostgresSchemaCatalogDigest hashes columns and index definitions in `public` and nothing else. The surrounding comments called the result a digest of "the catalog" and "the schema a database actually carries", which reads as a full schema identity and is not what the query does. Two databases differing only in constraints, triggers, row-level security policies, functions, sequences, types, extensions, ownership, privileges, collation, or server version digest identically here. That is acceptable for what the binding is for -- catching a template built from different migration source, which schemaSourceDigest binds directly -- but the claim has to match the check. State the exclusions explicitly rather than widening the query. Widening is the alternative the review offered and is a larger change than this PR should carry; the note says to widen before making the stronger claim. Comment-only. No behaviour change, and the identity suite is unaffected (9 passed, 1 expected skip). Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
tnunamak
force-pushed
the
port/gate-speed-278-0902
branch
from
September 6, 2026 14:43
f241b90 to
1e0ece8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Setting up a PostgreSQL database for every test file is slow, so this change builds one schema template per test run and clones each file's database from it. The risk that comes with cloning is that a template is shared mutable state living under a predictable name: if a stale template from an earlier run, or one built by a different process against different migrations, is still present under the name this run expects, every test file that clones it silently runs against the wrong schema. Tests would pass or fail for reasons that have nothing to do with the code under test, and the cause would be invisible in the output. This PR makes that impossible by checking the template's identity immediately before each clone.
Changed
reference-implementation/is the directory holding this repository's PDPP reference implementation — the authorization and resource servers, runtime, CLI, and black-box test suite for the Personal Data Portability Protocol. It is the component every file path and CI job below refers to. Inside it,scripts/run-tests.tsis the test runner that decides how each test file gets its database. It no longer issues a rawCREATE DATABASE ... TEMPLATEitself; it callsclonePostgresTestDatabaseFromTemplateinreference-implementation/scripts/postgres-test-template.ts, which runs the identity check described below and the clone on a single connection to thepostgresmaintenance database — the databaseCREATE DATABASEmust be issued from, since a database cannot be created from a connection to itself — with an advisory lock held across the whole sequence. Doing both on one locked connection is what closes the gap: a check performed on an earlier, separate connection could be invalidated by another process between the check and the clone.The identity is stored as a row in a table named
pdpp_test_template_metadata(thepdpp_prefix marks tables this project creates), which lives in that same maintenance database rather than inside the template, keyed by template name. It is therefore not copied into the clone — it is a record about the template, held where the clone cannot alter it. The row holds the ID of the runner process that built the template, a digest of the SQL migration files the schema was built from, the schema version those migrations produce, and anidentity_digest: a SHA-256 over the template's name and those other fields together. That digest is the single value the runner passes to each child test process, so throughout this PR "identity token" means thatidentity_digest, and the runner ID is one of the inputs hashed into it, not a separate check.A clone succeeds only when the digest recomputed from the stored row equals the token the runner holds, so a template with the right name but the wrong build is refused — including one built by an older version of this code, whose row predates the digest column and is
NULLthere. A missing identity is a hard error rather than a quiet fall back to building a fresh database, because a silent fallback would hide exactly the staleness this check exists to detect.Not every test file can use a template. Files that run schema migrations themselves, or otherwise need a genuinely empty database, are listed as "cold-required" in
reference-implementation/scripts/postgres-template-eligibility.tsand bootstrap their own.Reviewer findings addressed
This PR has been through three rounds of independent review. Findings are labelled P1 (must fix before merge) through P3 (correctness of the description, not the code).
Round 1, P1 — the runner's own clone path bypassed identity verification. The check existed, but the runner did not go through it, so the property was unproven exactly where it mattered.
reference-implementation/test/postgres-test-template-identity.test.tsadds two regression tests that drive the clone helper against a live PostgreSQL server. (Round 3 found these named as though they covered the runner's own selection of which files get a clone, which they do not; they are renamed to "clone helper ..." and that seam is now covered separately — see P1-1 below.) The first changes only the runner ID in the stored row, which invalidates the digest hashed from it; the second leaves the row alone and hands the clone a wrong expected token. Each asserts the clone is refused, and each then asserts the database that would have been created does not exist — so the test proves the refusal actually prevented the clone rather than merely logging a complaint.Round 1, P2 — the eligible and cold-required file counts in
PG-PROFILE-51-REPORT.mdhad drifted from the code. That report is a hand-written Markdown document checked in at the repository root, recording how this PostgreSQL work was measured; "51" is this pull request's number. Rather than hand-correcting the numbers,reference-implementation/test/postgres-template-eligibility-inventory.test.tsreads the report and derives the expected values from the two arrays that are the real source of truth,POSTGRES_TEMPLATE_ELIGIBLE_FILESandPOSTGRES_TEMPLATE_COLD_REQUIRED_FILES, both exported fromreference-implementation/scripts/postgres-template-eligibility.ts. The report now states 122 eligible plus 26 cold-required, 148 total, and cannot drift silently again.Round 2, P1 (blocking) — the DCO check was red. DCO is the Developer Certificate of Origin check GitHub runs on this repository; it requires every commit to carry a
Signed-off-byline whose name and email both match the commit's author or committer. The head commit was authored asTim Nunamaker <tnunamak@gmail.com>but signed off astnunamak <tnunamak@gmail.com>— same email, different name — so it failed while the other seven commits passed. The commit has been amended so the names match. Its tree is byte-identical to the previously reviewed head (git diff 07aa9ca2a HEADis empty), so this is a message-only change and none of the evidence below is affected. DCO now reports pass.Worth recording for anyone who hits this again: the
commit-msghook that runs before each commit on this machine compares the sign-off email only, so a name-only mismatch passes locally and fails in CI. That hook is developer-machine configuration held outside the repository (via git'score.hooksPath), so it is not something this PR can change.Round 2, P3 — this description's previous claim about Biome was wrong. Biome is the linter and formatter this repository configures in
reference-implementation/biome.jsonc. The corrected measurement is in the last section.Validation
Run on Node 22.23.1 against PostgreSQL 16.15 in a Docker container bound to
127.0.0.1only, in a throwaway database dropped afterwards.The identity suite reports 9 passed, 1 skipped, 0 failed, in 51.3 seconds. All nine real cases ran. The skipped tenth is a placeholder that exists to report "no PostgreSQL configured" when the suite cannot run at all, so it is inert here precisely because PostgreSQL was available. The eligibility inventory suite reports 3 passed.
Both suites were then checked for teeth by mutation, because a test that passes is not yet evidence that it would catch the failure it names:
assertPostgresTestTemplateUsablecall — the function that performs the identity check — fromclonePostgresTestDatabaseFromTemplate, leaving only the bareCREATE DATABASE ... TEMPLATE, makes exactly the two new clone-helper regression tests fail: the one that alters the stored runner ID, and the one that hands the clone a wrong identity token. The other seven non-skipped cases still pass. Failing that precise pair, and only that pair, is what shows the check is load-bearing at the clone boundary rather than incidentally satisfied by something else.PG-PROFILE-51-REPORT.mdfrom 122 to 123 makes exactly the inventory test that reads the report fail.Both mutations were reverted and the working tree verified clean.
Round 3: external review findings addressed
A second, external reviewer examined the same head and raised four findings that the earlier round had not caught. All four are repaired here, each with a test that fails before the change and passes after, plus a mutation check confirming the test would catch a regression.
P1-1 — the runner's selection seam was never executed by a test. Two tests named "runner per-file clone ..." called the clone helper directly. They prove the helper is fail-closed, but they never ran the line that decides whether a file is cloned at all:
useTemplate = postgresTestTemplateName !== null && isPostgresTemplateEligibleFilePath(filePath). The second half is the fail-closed default — a file not on the eligibility allowlist must get a from-scratch bootstrap even when a template exists — and deleting it failed nothing. Those two tests are renamed to "clone helper ...", andreference-implementation/test/run-tests-postgres-template-selection.test.tsnow runs the real runner as a subprocess against live PostgreSQL on a cold-required migration test.Asserting only on that file's outcome would not have worked, and this is why the defect survived review: the run's template carries a correct schema, so a cold-required file wrongly cloned from it still passes. The runner therefore now reports which path it took for each file (
PDPP_TEST_DB_PROVISION, eithercold-bootstraportemplate-clone) and the test asserts on that. Making the decision observable is the actual repair; the assertion is just what it enables. Mutation: the exact change the reviewer named,useTemplate = postgresTestTemplateName !== null, now fails withthe runner reported "template-clone".P1-2 — the template's identity was adopted rather than decided. Three things combined. The run nonce was 32 bits, so same-name collision on a shared cluster is a birthday problem at roughly 77k runs.
ensurePostgresTestTemplatereturned early when a usable template already sat under the derived name, without checking that this run built it. And it returned only the name, so the runner learned the expected identity by reading the candidate back — sourcing the expectation from the thing under verification, which is circular.The nonce is now 128 bits (a new value, kept separate from the 8-hex-character run ID that per-file database names embed and whose grammar pins that width). A pre-existing database under a fresh nonce's name is an error rather than something to reuse. The identity is computed and committed before publication and returned by the builder; the read-back that remains only confirms the published row reproduces it. Mutations: reverting the nonce to 4 bytes fails only the nonce assertion; restoring the adopt-if-usable early return fails only the adoption assertion.
P2 — verification and the clone ran on different connections.
clonePostgresTestDatabaseFromTemplateopened an admin connection, then called a verifier that opened a second, independent one. The check therefore described the template as seen by a connection already closed beforeCREATE DATABASE ... TEMPLATEran — a time-of-check/time-of-use window. The repair holds one connection and one advisory lock across verification, OID capture, and the clone, and re-reads the OID immediately before copying. Separately, the template's OID is now bound into the identity digest, so a drop-and-recreate that preserves the name is refused even when it happens before the call rather than during it. Mutations: removing the OID from the digest fails only the swap test; removing the advisory lock fails only the lock test.P2 — the "schema identity" claim was broader than the check. The digest covers columns and index definitions in
publicand nothing else. Constraints, triggers, row-level security policies, functions, sequences, types, extensions, ownership, privileges, collation, and server version are all outside it. Rather than widen the query — a larger change than this PR should carry — the claim is narrowed: the comments now state the exclusions explicitly and say to widen the query before making the stronger claim.All five PostgreSQL suites pass together at this head: identity 9 passed with 1 expected skip, inventory 3, provenance 3, clone atomicity 2, runner selection 1.
What this does not claim
This does not claim a new full PostgreSQL profile result, and does not make the repository's unrelated red checks green.
reference-implementation gate,test reference implementation, andtypecheck reference implementationare three CI jobs that lint, test, and typecheck the reference implementation. All three fail onmainitself, before this branch's changes, because of an unrelated in-progress migration that left the reference implementation with type errors and failing tests — tracked as issue #55, "unblock the Move-B CI seam" — and they fail here for that same reason. I confirmed they are red onmainrather than assuming it.Corrected Biome measurement. The previous claim here — no errors and three pre-existing warnings — was wrong. Measured with this repository's own pinned toolchain (Ultracite 7.10.7, a preset wrapper that invokes Biome 2.5.11 with a shared rule set), every file this PR touches reports exactly one error, and that error is a whole-file formatting diff rather than anything about the code. It is not specific to this PR: the configuration asks for tab indentation while the reference implementation is space-indented throughout, so a sample of 60 untouched test files reports 60 errors, one each. The only diagnostics attributable to this PR are three warnings about
biome-ignorecomments that no longer suppress anything, in files it adds. For each of the two files it modifies, running Biome against the merge-base version and the head version gives identical counts —reference-implementation/test/helpers/postgres-temp-database.tsreports 1 error and 4 warnings both times, andreference-implementation/scripts/run-tests.tsgoes from 2 errors to 1 — so the PR adds no new diagnostics to either. Neither of the two specific lint errors named in the round-2 review reproduces at this head with the pinned toolchain; a different Biome version is the likeliest explanation, which I have not confirmed. No CI job runs Biome, so none of this gates the merge.Assisted-by: AI