Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .agents/plans/m17-cross-platform-collectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,9 @@ standard too, so D-M17-4's "free ARM Mac runners" premise holds for the Intel ha
- [ ] Every 17.2a **and** 17.2b finding is either fixed in a later slice or carried as an open
`.agents/research/incidents.md` entry with a stated reason. (17.2a raised **INC-2026-08…13**;
**three** routed to a slice — 08, 09 → **17.3**; 12 → **17.4** — one to a re-derivation of
INC-2026-05 (11), one to a tooling backlog (10), and one (13) a procedure fix already applied
in-slice and inherited by 17.2b.)
INC-2026-05 (11), one to a tooling backlog (10, **since CLOSED `2026-08-09`** out-of-slice:
the bootstrap ownership claim in `packages/db/src/test-db-guard.ts`), and one (13) a
procedure fix already applied in-slice and inherited by 17.2b.)
- [ ] **No claim in the release notes, docs or UAT carries a verification tier it did not earn.**
Every `linux-x86_64` claim traceable only to 17.2a is `VM-verified`, not `hardware-verified`
(`emulated ≠ hardware-verified`) — the same discipline D-M17-4 already applies to
Expand Down
59 changes: 54 additions & 5 deletions .agents/research/incidents.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,13 @@ _(This sentence undercounted the six as five, and called all of them unfixed, fr
the one `DATABASE_URL` points at on a foreign cluster** before it truncates — a guard at the
destructive step rather than at the config step, which is the only place that cannot be bypassed by
hand-editing `.env`.
- **Regression test?** Yes, and it must sit at the **destructive step**, not the config step: a test
on the integration bootstrap asserting it **refuses to `TRUNCATE`** when `DATABASE_URL_TEST`
shares host:port with a cluster this checkout did not create — negative-controlled by pointing
both handles at one cluster and asserting the guard throws. That fails on today's code (no such
guard exists) and passes only after the fix.
- **Regression test?** Yes — **29 tests, both layers**, and they sit at the **bootstrap**, not the
config step. `packages/db/src/test-db-guard.test.ts` (21) is the pure value oracle for the
decision; `packages/db/src/test-db-guard.int.test.ts` (8) proves against real Postgres that the
claim round-trips and — the property the whole design rests on — **survives the
`TRUNCATE … CASCADE` it defends against**. Negative-controlled twice: neutering `decideClaim` to
always proceed reddens 9 of the 29, and the behavioural control below reproduces the incident
outright.

**An earlier draft proposed "a unit test on `setup-env.mjs` asserting the test handles are
derivable from the root `.env`", which cannot discriminate**, for two independent reasons.
Expand All @@ -269,6 +271,53 @@ _(This sentence undercounted the six as five, and called all of them unfixed, fr
for `SESSION_SECRET`), and borrowing it here is what made a broken line read as sound. Note the
Disposition above already named the right answer as option (b).

- **Resolved:** `2026-08-09`, tooling fix on `inc-2026-10-test-db-claim` — **option (b), the guard
at the bootstrap**. `packages/db/src/test-db-guard.ts` is called from `vitest.global-setup.ts`
BEFORE `runMigrations` and therefore before any test file's `beforeEach`, so a refusal aborts the
run with **zero `TRUNCATE`s executed**. It is the only choke point that sees every vitest
invocation (`npm test`, `repo-health`, a bare `npx vitest run <file>`) and evaluates the handles
as they actually resolve — which is what makes it un-bypassable by hand-editing `.env`, the
property the config-step candidate (a) lacked.

Two checks. The cheap catastrophic one is pure and needs no connection:
`assertTestDbIsNotArchive` refuses when `DATABASE_URL_TEST` names the same host:port:database as
`DATABASE_URL` — the case where the first `beforeEach` truncates the live archive rather than
test data. The real one is an **ownership claim stamped on the test database itself**, as a
`COMMENT ON DATABASE` read back through `shobj_description(oid, 'pg_database')`. The first
checkout to run stamps its absolute path; every other checkout is refused by name, with
`TEST_DB_CLAIM_TAKEOVER=1` as the documented escape hatch.

**Why a comment and not a marker table:** the claim has to survive the exact operation it defends
against, and a row does not — `TRUNCATE … CASCADE` is the first thing every int test runs. A
database comment also needs no schema change, so it does not perturb the 33-table count this
incident's own evidence used, nor any TRUNCATE list. `test-db-guard.int.test.ts` pins the
survival property directly.

**The guarantee, stated exactly:** _at most one checkout can ever `TRUNCATE` a given test
database._ Note the corollary rather than hiding it — if the SECOND checkout runs first it takes
the claim and the FIRST is the one refused later. That is still the invariant, because the hazard
is two checkouts sharing one test database, not which arrived first; either way the collision is
reported instead of silently destroying data.

**Behavioural negative control (the incident fired, then prevented).** A canary row was inserted
into `organizations` on the real `420ai_test`, and the database was stamped with a foreign claim
(`420ai-test-claim:/home/seanr/cleanroom/420AI`) to simulate the second checkout. With the guard
**on**, `npx vitest run packages/db/src/repositories/organizations.int.test.ts` aborted in global
setup and the canary survived (`count = 1`). With the two guard calls stripped from
`vitest.global-setup.ts`, the identical command reported **`Test Files 1 passed`, `Tests 6
passed`, exit 0** — and the canary was **gone** (`count = 0`). Green, silent, destructive: the
incident as described, reproduced on demand.

**Verification tier:** `hardware-verified` on `windows-x86_64` against the real compose Postgres
17 (the claim round-trip, the TRUNCATE-survival property and the behavioural control were all
executed, not reasoned about). The `ubuntu-latest` half is `CI-verified` — `repo-health.yml`
creates a fresh `420ai_test` per run, so CI exercises the unclaimed → stamp path every time.

One thing the fix deliberately does **not** do: change `.env.example` or `setup-env.mjs`. The
generated handles still point at `localhost:5433`, because that is correct for the overwhelmingly
common single-checkout case, and a config-step guess cannot know whether the cluster already
there is yours. The claim answers that question with evidence instead.

## INC-2026-09 — `build-sea.mjs` reports **PASS** on a non-Windows triple while producing an unbundlable, mislabelled artifact

- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a
Expand Down
4 changes: 3 additions & 1 deletion SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,9 @@ records` and `0 ÷ 4000` are opposite facts. An HTTP test asserts the literal st
(09) and boots `serve` into the operator's **real** collector home (08 — a double-writer
hazard, and the one isolation constraint this run breached, repaired in place under D-16.0-2);
`npm run setup` aims the test handles at whatever cluster is already on the host, so a second
checkout's `npx vitest run` would TRUNCATE the real test DB (10 — demonstrated, not fired);
checkout's `npx vitest run` would TRUNCATE the real test DB (10 — demonstrated, not fired;
**CLOSED `2026-08-09`** by an ownership claim stamped on the test database itself and checked
in `vitest.global-setup.ts` before any `TRUNCATE` — see the entry's **Resolved** block);
**INC-2026-05's recorded root cause contradicts `file-watcher.ts:136`** and 16.3 was designed
against it (11); a nonexistent connector source leaves **no trace anywhere** (12); and WSL
interop fabricated a plausible wrong value in the transcript (13). **17.3 re-scored 65% → 80%
Expand Down
12 changes: 7 additions & 5 deletions docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ Common failure modes, what they mean, and how to fix them. Grouped by where they

## Tests & gates

| Symptom | Cause | Fix |
| ----------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Integration tests "skipped" but suite green | `DATABASE_URL_TEST` unset ⇒ `*.int.test.ts` self-skip | For real sign-off: `npm run db:up && npm run db:migrate` then `npm run repo-health -- --require-db` (fails if any int test skipped). |
| `ExperimentalWarning` from `node:sqlite` | Node 24's `node:sqlite` is experimental | **Expected by design** — does not affect correctness; don't suppress it in a way that breaks tests. |
| `repo-health` fails on NUL-byte / stray-artifact scan | A source file has embedded NULs, or emitted `*.js`/`dist/`/`*.sqlite` got staged | Rewrite the offending file as clean UTF-8; unstage build artifacts (`*.sqlite` and `src/`-emitted JS are gitignored). |
| Symptom | Cause | Fix |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Integration tests "skipped" but suite green | `DATABASE_URL_TEST` unset ⇒ `*.int.test.ts` self-skip | For real sign-off: `npm run db:up && npm run db:migrate` then `npm run repo-health -- --require-db` (fails if any int test skipped). |
| `ExperimentalWarning` from `node:sqlite` | Node 24's `node:sqlite` is experimental | **Expected by design** — does not affect correctness; don't suppress it in a way that breaks tests. |
| `repo-health` fails on NUL-byte / stray-artifact scan | A source file has embedded NULs, or emitted `*.js`/`dist/`/`*.sqlite` got staged | Rewrite the offending file as clean UTF-8; unstage build artifacts (`*.sqlite` and `src/`-emitted JS are gitignored). |
| "Refusing to run integration tests: … is claimed by a DIFFERENT checkout" | INC-2026-10 — this checkout's `DATABASE_URL_TEST` points at a test database another checkout already owns. Every `*.int.test.ts` opens with `TRUNCATE … CASCADE`, so the run was stopped **before** it destroyed that checkout's data. | Give this checkout its own cluster: change the published port in `docker-compose.yml` and the four handles in `.env`. If the database really is yours (you moved or renamed the checkout), re-claim it once with `TEST_DB_CLAIM_TAKEOVER=1 npx vitest run`. |
| "Refusing to run integration tests: `DATABASE_URL_TEST` names the SAME database as `DATABASE_URL`" | The test handle points at the live archive, so the first `beforeEach` would truncate production. | Point `DATABASE_URL_TEST` at the separate `420ai_test` database the compose stack provisions beside `420ai` (`docker/init-test-db.sql`). |

---

Expand Down
15 changes: 15 additions & 0 deletions packages/db/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ export { encryptField, decryptField, activeKeyId } from "./crypto.js";
export type { EncryptedField } from "./crypto.js";
export { API_KEY_PREFIX, generateToken, hashToken } from "./tokens.js";
export { runMigrations } from "./migrate.js";
// INC-2026-10 — the integration-suite ownership guards. Called from `vitest.global-setup.ts`
// BEFORE `runMigrations`, so a refusal aborts the run before any `TRUNCATE`. Exported from the
// barrel because the global setup is the consumer and it lives at the repo root.
export {
assertTestDbIsNotArchive,
claimTestDatabase,
decideClaim,
describeTarget,
normalizeCheckout,
sameDatabaseTarget,
ForeignTestDatabaseError,
CLAIM_PREFIX,
TAKEOVER_ENV,
} from "./test-db-guard.js";
export type { ClaimDecision } from "./test-db-guard.js";
export { createPairingCode, redeemPairingCode, PairingError } from "./repositories/pairing.js";
export {
createMachine,
Expand Down
Loading
Loading