diff --git a/docs/audit/live-drift-forensics-2026-08.md b/docs/audit/live-drift-forensics-2026-08.md index 1846e8abe1..50c111e669 100644 --- a/docs/audit/live-drift-forensics-2026-08.md +++ b/docs/audit/live-drift-forensics-2026-08.md @@ -27,9 +27,36 @@ runs on pushes to `main` touching `supabase/migrations/**` or `supabase/schema.s `workflow_dispatch`, the secret preflight, and `concurrency.cancel-in-progress: false` were kept unchanged. No hosted Supabase call was made. -Outstanding for the operator: dispatch `live-drift` once to confirm a real failure produces the -pinned issue (provider-backed — not run from the authoring session), and add -`SUPABASE_ACCESS_TOKEN` to environment secrets per plan step 0.3 and ledger `#183`. +_2026-08-14, forced-dispatch proof (owner-authorized)._ `live-drift` dispatched on `main` +(Actions run `31813064485`). The definition-of-done behaviour was observed end-to-end: + +- `live-drift` job **failed** at `Compare live schema drift`, as intended for this proof. +- `Capture drift and migration-history findings` still ran (`if: always()`), and + `Align migration history for Supabase Preview` correctly **skipped** after the failing step. +- The separate `drift-routing` job then ran (`if: ${{ !cancelled() }}`) and **succeeded**, + creating issue **#1963 "Live drift check failing"** with label `live-drift-failure`, the run + URL, `Job result: failure`, `Trigger: workflow_dispatch`, and the full findings block. + +That run also supersedes the stale 2026-08-09 figures this file was opened with. Measured +2026-08-14, `UNEXPECTED DRIFT (32)`: + +| Category | 2026-08-09 | 2026-08-14 | +| ---------------------------------------- | ---------- | ------------------ | +| `match_*` function `def_hash` mismatches | 10 | **10 — unchanged** | +| `missing_live` indexes | 21 | **20** | +| `unexpected_live` indexes | 2 | **2 — unchanged** | + +`documents_title_trgm_idx` and `document_chunks_content_trgm_idx` are absent from the missing +list, independently corroborating the Phase 4 restoration below (verified separately by +read-only query against `sjrfecxgysukkwxsowpy`: both `indisvalid`/`indisready`, 648 kB and +68 MB). The 10 RPC mismatches are untouched, so **Phase 3 remains entirely outstanding** and is +the next step per the plan's ordering. + +Routing is also covered offline by `tests/live-drift-workflow.test.ts` (mutation-verified), so a +future regression fails a test rather than waiting for a live failure to be mishandled. + +Outstanding for the operator: add `SUPABASE_ACCESS_TOKEN` to environment secrets per plan step +0.3 and ledger `#183` (dashboard work; names only, never values). ## Phase 1 — Read-only forensics diff --git a/docs/branch-review-records/0ab0fabc55b3958783c42bcd3a18b865415668919f9bc7f081bedf9fe16aecdf.record.md b/docs/branch-review-records/0ab0fabc55b3958783c42bcd3a18b865415668919f9bc7f081bedf9fe16aecdf.record.md new file mode 100644 index 0000000000..dba4be1fb9 --- /dev/null +++ b/docs/branch-review-records/0ab0fabc55b3958783c42bcd3a18b865415668919f9bc7f081bedf9fe16aecdf.record.md @@ -0,0 +1 @@ +| 2026-08-14 | PR-1951 | 7d70c74cc5449d577df3895aa766ad31f3204045 | tests/live-drift-workflow.test.ts; docs/outstanding-issues-inbox; docs/branch-review-records | preserved prior test fixes; merged latest main; cancelled superseded #331/#333 ledger mutations to restore deterministic queue application | manual adversarial review; current thread verification; docs links passed; ledger inbox passed; ledger guards passed; git merge-tree; git diff --check; focused Vitest unavailable (node_modules absent) | diff --git a/docs/branch-review-records/21c77c3c5582a1424f204ead318b9b780ef55dcb9d41bac400841a83eb4564f3.record.md b/docs/branch-review-records/21c77c3c5582a1424f204ead318b9b780ef55dcb9d41bac400841a83eb4564f3.record.md new file mode 100644 index 0000000000..a0675df92d --- /dev/null +++ b/docs/branch-review-records/21c77c3c5582a1424f204ead318b9b780ef55dcb9d41bac400841a83eb4564f3.record.md @@ -0,0 +1 @@ +| 2026-08-14 | claude/db-remediation-phase-0-wfaiyl | 20228faa6002da009e290bc398345a47d2cd7a02 | PR #1951 review-and-fix | reviewed live-drift routing workflow contract and Phase 0 records; fixed no PR-introduced defect; merged latest main | offline: routing-script stub exercise; check-outstanding-issues; ledger-inbox check; Prettier changed files; check:github-actions; check:gate-manifest; check:verification-plan; maintainability budgets; targeted Vitest unavailable (isolated worktree has no node_modules); manual adversarial pass | diff --git a/docs/branch-review-records/248b36b92b06adf7c5c774b595dbdd7338816418b90abb2584b88d3d49962c5d.record.md b/docs/branch-review-records/248b36b92b06adf7c5c774b595dbdd7338816418b90abb2584b88d3d49962c5d.record.md new file mode 100644 index 0000000000..5577d0b845 --- /dev/null +++ b/docs/branch-review-records/248b36b92b06adf7c5c774b595dbdd7338816418b90abb2584b88d3d49962c5d.record.md @@ -0,0 +1 @@ +| 2026-08-14 | PR-1951 | 4c55ec05875dcf063555e71021adb9d1f1a42f5c | PR #1951 full review and unblock | fixed | workflow permission-map parser; GitHub Issue API owner/repo assertions; node scripts/check-docs-links.mjs; node scripts/ledger-inbox.mjs check; node scripts/check-ledger-write-discipline.mjs --self-test; Vitest unavailable: node_modules absent | diff --git a/docs/branch-review-records/28051a48f8acb8c67da3f5d43e867ed46a9dbfa89fdd249cead35fa563bd9119.record.md b/docs/branch-review-records/28051a48f8acb8c67da3f5d43e867ed46a9dbfa89fdd249cead35fa563bd9119.record.md new file mode 100644 index 0000000000..c775fff880 --- /dev/null +++ b/docs/branch-review-records/28051a48f8acb8c67da3f5d43e867ed46a9dbfa89fdd249cead35fa563bd9119.record.md @@ -0,0 +1 @@ +| 2026-08-14 | PR-1951 | 809c50bf4ca8ede8c2c0ec49df9371cd4d56c517 | PR #1951 CI format repair | fixed the exact-head Changed-file format check failure in live-drift workflow coverage | Prettier 3.9.6; All matched files use Prettier code style!; Tests 15 passed (15); git diff --check passed; docs link check passed: 1775 repo path references resolve.; Ledger inbox check passed: 22 pending request(s), 138 applied.; ledger write discipline self-test passed.; Branch review ledger guard passed: 880 live table records + 1206 archived + 91 immutable | diff --git a/docs/branch-review-records/9cb91634447e60affd610471e14ffdbacf8e042b608a2c3553c5ae303b34e92b.record.md b/docs/branch-review-records/9cb91634447e60affd610471e14ffdbacf8e042b608a2c3553c5ae303b34e92b.record.md new file mode 100644 index 0000000000..8cdd6530c8 --- /dev/null +++ b/docs/branch-review-records/9cb91634447e60affd610471e14ffdbacf8e042b608a2c3553c5ae303b34e92b.record.md @@ -0,0 +1 @@ +| 2026-08-14 | PR-1951 | 6ed1fb871c7e16e89ed111a9576d502d90a765b1 | PR #1951 final base sync after formatting fix | Merged the current main including the #1959 ledger reconciliation after the targeted Prettier repair; merge tree is clean and the run-scoped workflow regression suite remains green. | All matched files use Prettier code style; Test Files 1 passed; Tests 15 passed; docs link check passed: 1775 repo path references resolve; Ledger inbox check passed: 22 pending request(s), 138 applied; branch-review-ledger self-test passed; Branch review ledger guard passed: 880 live table records + 1206 archived + 98 immutable; verify:pr-local unavailable: tsx/cli absent from isolated worktree (Node v24.14.0). | diff --git a/docs/branch-review-records/c0e02ee4e90a6401ef8316af58b1bf530d74e359a217e4d91d21eae705b7ab81.record.md b/docs/branch-review-records/c0e02ee4e90a6401ef8316af58b1bf530d74e359a217e4d91d21eae705b7ab81.record.md new file mode 100644 index 0000000000..36caa306c4 --- /dev/null +++ b/docs/branch-review-records/c0e02ee4e90a6401ef8316af58b1bf530d74e359a217e4d91d21eae705b7ab81.record.md @@ -0,0 +1 @@ +| 2026-08-14 | claude/db-remediation-phase-0-wfaiyl | 08a49b316b505a5ef39d0e7fd514a7ece7993f8f | PR #1951 review-and-fix | fixed stale pending cancellation after base reconciliation; preserved only valid remaining queue mutations | offline: reproduced docs:check-links failure; docs:check-links pass after fix; check-outstanding-issues; ledger-inbox check; Prettier affected requests; git diff --check | diff --git a/docs/medication-interaction-lexicon-review.md b/docs/medication-interaction-lexicon-review.md index 8c8687a64c..ff70571c26 100644 --- a/docs/medication-interaction-lexicon-review.md +++ b/docs/medication-interaction-lexicon-review.md @@ -96,8 +96,8 @@ the class cannot be enumerated, and holds the medication at grey rather than gre **35 of the catalogue's 328 medications sit outside both ends of every resolved interaction row.** Entering one of them produces no alert — not because the combination was checked and -found clear, but because no machine-resolved edge in the corpus includes that drug. On screen those -outcomes look the same, so this list is the honest boundary of the feature. +found clear, but because no machine-resolved edge in the corpus includes that drug. On screen those outcomes look the +same, so this list is the honest boundary of the feature. This is a **corpus coverage** limit, not necessarily a lexicon fault. Widening it means adding an interaction row or making an existing row machine-resolvable, with clinical review of the source content. diff --git a/docs/outstanding-issues-inbox/21b6525a-dd2d-4dcc-95d5-c3c777420adf.json b/docs/outstanding-issues-inbox/21b6525a-dd2d-4dcc-95d5-c3c777420adf.json new file mode 100644 index 0000000000..dd2f90d2c5 --- /dev/null +++ b/docs/outstanding-issues-inbox/21b6525a-dd2d-4dcc-95d5-c3c777420adf.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "21b6525a-dd2d-4dcc-95d5-c3c777420adf", + "createdOn": "2026-08-14", + "action": "update", + "payload": { + "id": "#333", + "detail": "PARTIAL 2026-08-14 (PR #1951): the regeneration half of this row's Next step is done. Ran npm run medications:lexicon-report, confirmed by whitespace-normalised byte comparison that it is a pure re-wrap and not a content change needing clinical review (all 28 catalogue terms identical), and committed it. The diagnosis in #331 is now closed out: it was not a staleness-comparison bug, it was a genuinely stale committed file that prettier could not flag because proseWrap: preserve accepts both wrappings. STILL OPEN, and the more important half: the check is in the local verify:pr-local chain but in no CI job, so it fails every local preflight while every required check stays green. Decide one way or the other — wire it into CI so it cannot silently rot again, or drop it from verify:pr-local so it stops failing preflights it does not gate. Caveat on this PR: #1951 carries the regeneration alongside a workflow test rather than strictly on its own as this row asked, because the session was constrained to a single designated branch; the regeneration is its own revertible commit." + } +} diff --git a/docs/outstanding-issues-inbox/3d0adf39-ec7a-4fa3-9309-057a193410de.json b/docs/outstanding-issues-inbox/3d0adf39-ec7a-4fa3-9309-057a193410de.json new file mode 100644 index 0000000000..0249dbcdc8 --- /dev/null +++ b/docs/outstanding-issues-inbox/3d0adf39-ec7a-4fa3-9309-057a193410de.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "3d0adf39-ec7a-4fa3-9309-057a193410de", + "createdOn": "2026-08-14", + "action": "update", + "payload": { + "id": "#316", + "detail": "Combined 2026-08-14 update, superseding the two partial requests cancelled in this same batch. PHASE 0 CLOSED including the forced-dispatch proof its definition of done required: live-drift dispatched on main (Actions run 31813064485) failed at the drift step, the always() capture step still ran, the migration-history step correctly skipped, and the separate drift-routing job then created issue #1963 \"Live drift check failing\" carrying the label, run URL, job result, trigger and the full findings block. Routing is now also covered offline by tests/live-drift-workflow.test.ts, mutation-verified. INCIDENT REPAIR, owner-approved in-session: the two retrieval-critical indexes documents_title_trgm_idx and document_chunks_content_trgm_idx were restored with CREATE INDEX CONCURRENTLY plus ANALYZE, both indisvalid and indisready at 648 kB and 68 MB, re-verified afterwards by an independent read-only query. Before and after supabase_rpc_latency_ms 31610 to 1535 on the text fast path and 8519 hybrid, with match_document_chunks_text_v2 at 14 ms. No repo schema change was needed because the definitions were already codified. CORRECTED FIGURES measured 2026-08-14, superseding the 2026-08-09 numbers this row was opened with: 10 match_* def_hash mismatches (unchanged), 20 missing_live indexes rather than 21, and the same 2 unexpected_live. ATTRIBUTION STILL OPEN: migration 20260705180000 recorded 14 executed statements so it was not mark-applied, and the 20260804110240 guard validates four other indexes and never checks this pair, so it gives no existence bound for 2026-08-04. The drop window is therefore 2026-07-05 to 2026-08-02 and the dashboard audit-history pairing remains owner action; #248 stays open. NEXT: Phase 3 RPC reconciliation before Phase 4, per the plan's ordering that the change which can alter clinical answers precedes the ones that only speed them up. Evidence: docs/audit/live-drift-forensics-2026-08.md." + } +} diff --git a/docs/outstanding-issues-inbox/3dda04ff-1ae9-4153-80ed-ad081931e396.json b/docs/outstanding-issues-inbox/3dda04ff-1ae9-4153-80ed-ad081931e396.json new file mode 100644 index 0000000000..14a45f2ac1 --- /dev/null +++ b/docs/outstanding-issues-inbox/3dda04ff-1ae9-4153-80ed-ad081931e396.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "3dda04ff-1ae9-4153-80ed-ad081931e396", + "createdOn": "2026-08-14", + "action": "cancel", + "payload": { + "requestId": "9792c896-78d8-46ab-b194-6a52b7fd7cab", + "reason": "Superseded by the single combined #316 update queued in the same batch, which merges this request's dispatch proof and corrected drift figures with the incident-session findings from request bbf21714 so the row carries one coherent account instead of two partial ones." + } +} diff --git a/docs/outstanding-issues-inbox/3f8797e0-dffa-4d26-8019-15147b9af397.json b/docs/outstanding-issues-inbox/3f8797e0-dffa-4d26-8019-15147b9af397.json new file mode 100644 index 0000000000..fa7ca93a2f --- /dev/null +++ b/docs/outstanding-issues-inbox/3f8797e0-dffa-4d26-8019-15147b9af397.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "3f8797e0-dffa-4d26-8019-15147b9af397", + "createdOn": "2026-08-14", + "action": "update", + "payload": { + "id": "#292", + "detail": "Recurred 2026-08-14 on the database remediation plan, this time with two assistants building Phase 0: PR #1938 and PR #1939 both implemented live-drift failure routing and the post-migration trigger, merged four hours apart. Both landed and no harm resulted — #1939 built on #1938's commit and improved it, moving the findings capture after the migration-history step so a migration-history failure is visible instead of a clean drift result being published as its explanation. The cost was still two full authoring sessions and two CI cycles for one deliverable. This matters more for the phases still ahead than it did here: Phase 1 consumes an approved read-only production window, and Phases 3 and 4 consume approved mutation windows and live eval-canary budget, so a duplicate there wastes an operator-gated resource rather than just tokens. Concrete ask for the remediation work specifically: check the open-PR list for the surface before starting any of Phases 1-5, per docs/database-remediation-playbook.md." + } +} diff --git a/docs/outstanding-issues-inbox/5db479fa-07fc-4e9e-a377-93b4e27797ee.json b/docs/outstanding-issues-inbox/5db479fa-07fc-4e9e-a377-93b4e27797ee.json new file mode 100644 index 0000000000..5dc89e4fb9 --- /dev/null +++ b/docs/outstanding-issues-inbox/5db479fa-07fc-4e9e-a377-93b4e27797ee.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "5db479fa-07fc-4e9e-a377-93b4e27797ee", + "createdOn": "2026-08-14", + "action": "cancel", + "payload": { + "requestId": "21b6525a-dd2d-4dcc-95d5-c3c777420adf", + "reason": "Superseded by f3b63187-c295-4f6a-8ecd-602f348835c4, which closes duplicate #333 and preserves its evidence on #331." + } +} diff --git a/docs/outstanding-issues-inbox/6b09c1df-5f7c-4103-af3c-aead33eafb00.json b/docs/outstanding-issues-inbox/6b09c1df-5f7c-4103-af3c-aead33eafb00.json new file mode 100644 index 0000000000..82fe278626 --- /dev/null +++ b/docs/outstanding-issues-inbox/6b09c1df-5f7c-4103-af3c-aead33eafb00.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "6b09c1df-5f7c-4103-af3c-aead33eafb00", + "createdOn": "2026-08-14", + "action": "update", + "payload": { + "id": "#316", + "detail": "MISSING" + } +} diff --git a/docs/outstanding-issues-inbox/9792c896-78d8-46ab-b194-6a52b7fd7cab.json b/docs/outstanding-issues-inbox/9792c896-78d8-46ab-b194-6a52b7fd7cab.json new file mode 100644 index 0000000000..d24e272a62 --- /dev/null +++ b/docs/outstanding-issues-inbox/9792c896-78d8-46ab-b194-6a52b7fd7cab.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "9792c896-78d8-46ab-b194-6a52b7fd7cab", + "createdOn": "2026-08-14", + "action": "update", + "payload": { + "id": "#316", + "detail": "Phase 0 CLOSED 2026-08-14, including the forced-dispatch proof its definition of done required. Dispatched live-drift on main (run 31813064485): the drift job failed as expected, the always() capture step still ran, and the separate drift-routing job then created issue #1963 'Live drift check failing' automatically, carrying the run URL, job result, trigger and the full findings block. Failure routing is therefore proven end-to-end against the real repository, not just offline; PR #1951 adds a mutation-verified contract test so it stays proven. That run also supersedes the stale 2026-08-09 drift numbers this row was opened with. Measured now: 10 match_* function def_hash mismatches (UNCHANGED - Phase 3 is entirely outstanding and remains the highest-stakes unknown), 20 missing_live indexes (not 21), and the same 2 unexpected_live indexes (document_table_facts_document_id_idx, storage_cleanup_jobs_owner_id_idx). documents_title_trgm_idx and document_chunks_content_trgm_idx are no longer in the missing list, independently confirmed by direct read-only query against sjrfecxgysukkwxsowpy (both indisvalid/indisready, 648 kB and 68 MB), which corroborates the restoration recorded in PR #1960. Next: Phase 3 (RPC reconciliation) before Phase 4, per the plan's ordering — the change that can alter clinical answers goes before the ones that only speed them up." + } +} diff --git a/docs/outstanding-issues-inbox/c3d91fce-52e5-41ed-8648-b2bf5e95b32c.json b/docs/outstanding-issues-inbox/c3d91fce-52e5-41ed-8648-b2bf5e95b32c.json new file mode 100644 index 0000000000..308af5387a --- /dev/null +++ b/docs/outstanding-issues-inbox/c3d91fce-52e5-41ed-8648-b2bf5e95b32c.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "c3d91fce-52e5-41ed-8648-b2bf5e95b32c", + "createdOn": "2026-08-14", + "action": "done", + "payload": { + "id": "#331", + "outcome": "Not a comparison bug. Investigated 2026-08-14 in PR #1951: the staleness was real and wrapping-only. Normalising whitespace on the committed and regenerated docs/medication-interaction-lexicon-review.md leaves them byte-identical across all 28 catalogue terms, so no content changed, but the committed line wrapping genuinely differed from the generator's. The reason three branches saw it with 'zero diff on their branch' is that it was already stale on main, so every branch inherited the failure regardless of what it touched — git diff origin/main was the wrong instrument, since main itself carried the stale copy. It also escaped npm run format because prettier runs proseWrap: preserve, which accepts both wrappings. Fixed by regenerating with npm run medications:lexicon-report followed by prettier (the order the check expects). The remaining open question — that no CI job runs this check — is tracked in #333 rather than here." + } +} diff --git a/docs/outstanding-issues-inbox/c63161f6-21b8-40ec-927c-684e22791066.json b/docs/outstanding-issues-inbox/c63161f6-21b8-40ec-927c-684e22791066.json new file mode 100644 index 0000000000..04ba762901 --- /dev/null +++ b/docs/outstanding-issues-inbox/c63161f6-21b8-40ec-927c-684e22791066.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "c63161f6-21b8-40ec-927c-684e22791066", + "createdOn": "2026-08-14", + "action": "cancel", + "payload": { + "requestId": "c3d91fce-52e5-41ed-8648-b2bf5e95b32c", + "reason": "Superseded by fbfe982f-cd2f-49c9-a94e-908a99efa6b8, the current #331 update consolidating the authoritative remaining work." + } +} diff --git a/docs/outstanding-issues-inbox/fd548180-f031-44d8-bd70-24c3b03c5f21.json b/docs/outstanding-issues-inbox/fd548180-f031-44d8-bd70-24c3b03c5f21.json new file mode 100644 index 0000000000..2963919720 --- /dev/null +++ b/docs/outstanding-issues-inbox/fd548180-f031-44d8-bd70-24c3b03c5f21.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "id": "fd548180-f031-44d8-bd70-24c3b03c5f21", + "createdOn": "2026-08-14", + "action": "cancel", + "payload": { + "requestId": "6b09c1df-5f7c-4103-af3c-aead33eafb00", + "reason": "Queued in error: a shell substitution resolved to the placeholder text MISSING instead of the intended detail. Replaced by the correct combined update in the same batch." + } +} diff --git a/package.json b/package.json index dae68aafb9..aa5bc15901 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:coverage": "node scripts/run-vitest.mjs run --coverage", "test:coverage:node": "node scripts/run-vitest.mjs run --project=node --coverage", "test:coverage:ui": "node scripts/run-vitest.mjs run --project=jsdom --coverage", - "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/codex-run-pr-operator-workflow.test.ts tests/eval-canary-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/live-web-vitals-inputs.test.ts tests/offline-release-profile.test.ts", + "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/codex-run-pr-operator-workflow.test.ts tests/eval-canary-workflow.test.ts tests/live-drift-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/live-web-vitals-inputs.test.ts tests/offline-release-profile.test.ts", "test:e2e": "node scripts/run-playwright.mjs", "test:e2e:all": "node scripts/run-playwright.mjs", "test:e2e:accessibility": "node scripts/run-playwright.mjs tests/ui-accessibility.spec.ts --project=chromium", diff --git a/tests/live-drift-workflow.test.ts b/tests/live-drift-workflow.test.ts new file mode 100644 index 0000000000..66c3ac8ef5 --- /dev/null +++ b/tests/live-drift-workflow.test.ts @@ -0,0 +1,311 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const workflowPath = path.join(repoRoot, ".github", "workflows", "live-drift.yml"); +const workflow = readFileSync(workflowPath, "utf8").replace(/\r\n/g, "\n"); + +type Issue = { number: number; title: string }; +type RepositoryCoordinates = { owner: string; repo: string }; + +type ScriptFunction = ( + github: Record, + context: Record, + core: { + info: (message: string) => void; + warning: (message: string) => void; + }, +) => Promise; + +const AsyncFunction = Object.getPrototypeOf(async () => undefined).constructor as new ( + ...args: string[] +) => ScriptFunction; + +// Same extraction shape as tests/codex-autofix-workflow.test.ts: the block is +// `script: |` at ten spaces, so its body is everything indented twelve or more. +function extractWorkflowScripts(source: string) { + const scriptMarker = " script: |\n"; + const scripts: string[] = []; + let searchFrom = 0; + + while (true) { + const scriptStart = source.indexOf(scriptMarker, searchFrom); + if (scriptStart === -1) break; + + const scriptLines = source.slice(scriptStart + scriptMarker.length).split("\n"); + const extractedLines: string[] = []; + + for (const line of scriptLines) { + if (line.length === 0) { + extractedLines.push(""); + continue; + } + if (!line.startsWith(" ")) break; + extractedLines.push(line.slice(12)); + } + + scripts.push(extractedLines.join("\n")); + searchFrom = scriptStart + scriptMarker.length; + } + + return scripts; +} + +const [routingScriptSource, ...extraScripts] = extractWorkflowScripts(workflow); +if (!routingScriptSource) { + throw new Error("Expected a github-script block for drift routing in .github/workflows/live-drift.yml."); +} +if (extraScripts.length > 0) { + throw new Error("Expected exactly one github-script block in live-drift.yml; update this test if that changes."); +} + +const routingScript = new AsyncFunction("github", "context", "core", routingScriptSource); + +type Calls = { + closed: Array; + comments: Array; + created: Array; + listed: Array; + updatedBodies: Array; + warnings: string[]; +}; + +async function runRoutingScript(options: { findings?: string; openIssues?: Issue[]; result: string }) { + const calls: Calls = { closed: [], comments: [], created: [], listed: [], updatedBodies: [], warnings: [] }; + + const github = { + rest: { + issues: { + create: async (request: RepositoryCoordinates & { body: string; labels: string[]; title: string }) => { + calls.created.push({ + body: request.body, + labels: request.labels, + owner: request.owner, + repo: request.repo, + title: request.title, + }); + return { data: { number: 4242 } }; + }, + createComment: async (request: RepositoryCoordinates & { body: string; issue_number: number }) => { + calls.comments.push({ + body: request.body, + issue_number: request.issue_number, + owner: request.owner, + repo: request.repo, + }); + }, + listForRepo: async (request: RepositoryCoordinates & { labels: string; state: string }) => { + calls.listed.push({ labels: request.labels, owner: request.owner, repo: request.repo, state: request.state }); + return { data: options.openIssues ?? [] }; + }, + update: async ( + request: RepositoryCoordinates & { + body?: string; + issue_number: number; + state?: string; + state_reason?: string; + }, + ) => { + if (request.state) { + calls.closed.push({ + issue_number: request.issue_number, + owner: request.owner, + repo: request.repo, + state: request.state, + state_reason: request.state_reason, + }); + } + if (typeof request.body === "string") { + calls.updatedBodies.push({ + body: request.body, + issue_number: request.issue_number, + owner: request.owner, + repo: request.repo, + }); + } + }, + }, + }, + }; + + const context = { + eventName: "schedule", + repo: { owner: "BigSimmo", repo: "Database" }, + runId: 99, + serverUrl: "https://github.com", + }; + + const core = { + info: () => undefined, + warning: (message: string) => { + calls.warnings.push(message); + }, + }; + + const previous = { findings: process.env.DRIFT_FINDINGS, result: process.env.DRIFT_RESULT }; + process.env.DRIFT_RESULT = options.result; + process.env.DRIFT_FINDINGS = options.findings ?? ""; + try { + await routingScript(github, context, core); + } finally { + if (previous.result === undefined) delete process.env.DRIFT_RESULT; + else process.env.DRIFT_RESULT = previous.result; + if (previous.findings === undefined) delete process.env.DRIFT_FINDINGS; + else process.env.DRIFT_FINDINGS = previous.findings; + } + + return calls; +} + +const pinnedIssue: Issue = { number: 1234, title: "Live drift check failing" }; +const sampleFindings = "UNEXPECTED DRIFT (2):\n ! [indexes] missing_live documents_title_trgm_idx"; +const repositoryCoordinates = { owner: "BigSimmo", repo: "Database" }; + +function workflowJobPermissionMaps(source: string) { + const jobsStart = source.indexOf("jobs:\n"); + if (jobsStart < 0) throw new Error("Expected a jobs map in .github/workflows/live-drift.yml."); + + const jobsSection = source.slice(jobsStart); + const jobHeaders = [...jobsSection.matchAll(/^ ([a-z][\w-]*):$/gm)]; + const jobs = Object.fromEntries( + jobHeaders.map((header, index) => { + const bodyStart = (header.index ?? 0) + header[0].length + 1; + const bodyEnd = jobHeaders[index + 1]?.index ?? jobsSection.length; + const body = jobsSection.slice(bodyStart, bodyEnd); + const permissions = Object.fromEntries( + [...body.matchAll(/^ permissions:\n((?: [^\n]+\n?)*)/gm)].flatMap((permissionsMatch) => + [...permissionsMatch[1].matchAll(/^ ([\w-]+): ([\w-]+)$/gm)].map((entry) => [entry[1], entry[2]]), + ), + ); + return [header[1], { permissions }]; + }), + ); + + return { jobs }; +} + +describe("live-drift workflow triggers and privileges", () => { + it("keeps the weekly schedule and manual dispatch", () => { + expect(workflow).toContain("workflow_dispatch:"); + expect(workflow).toContain('- cron: "30 18 * * 0"'); + }); + + it("also runs once a schema change reaches main, and never on pull requests", () => { + expect(workflow).toContain("branches: [main]"); + expect(workflow).toContain('- "supabase/migrations/**"'); + expect(workflow).toContain('- "supabase/schema.sql"'); + expect(workflow).not.toMatch(/^on:[\s\S]*?^\s{2}pull_request/m); + }); + + it("never cancels an in-flight drift run", () => { + expect(workflow).toContain("group: live-drift-check"); + expect(workflow).toContain("cancel-in-progress: false"); + }); + + it("keeps the secret preflight so a missing key fails loudly rather than silently passing", () => { + expect(workflow).toContain("Preflight required secrets"); + expect(workflow).toContain("Live drift check cannot run - missing repo secrets:"); + }); + + it("grants issues: write only to the routing job", () => { + // Workflow-level permissions stay read-only, so no job inherits issue writes. + expect(workflow).toMatch(/^permissions:\n {2}contents: read\n/m); + + const parsed = workflowJobPermissionMaps(workflow); + expect(parsed.jobs["drift-routing"]?.permissions).toEqual({ contents: "read", issues: "write" }); + for (const [jobName, job] of Object.entries(parsed.jobs)) { + if (jobName !== "drift-routing") expect(job.permissions.issues).toBeUndefined(); + } + }); + + it("keeps the service-role key out of the job that can write issues", () => { + const routingStart = workflow.indexOf(" drift-routing:"); + expect(workflow.slice(routingStart)).not.toContain("SUPABASE_SERVICE_ROLE_KEY"); + }); + + it("pins github-script to the reviewed immutable commit", () => { + expect(workflow).toContain("uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0"); + }); + + it("captures findings even when the drift step failed", () => { + expect(workflow).toContain("set -o pipefail"); + expect(workflow).toMatch(/id: findings\n\s+if: always\(\)/); + }); + + it("still routes when the drift job fails", () => { + expect(workflow).toContain("if: ${{ !cancelled() }}"); + }); +}); + +describe("live-drift failure routing", () => { + it("opens one labelled issue when the check fails and none is open", async () => { + const calls = await runRoutingScript({ findings: sampleFindings, result: "failure" }); + + expect(calls.listed).toEqual([{ ...repositoryCoordinates, labels: "live-drift-failure", state: "open" }]); + expect(calls.created).toHaveLength(1); + expect(calls.created[0].title).toBe("Live drift check failing"); + expect(calls.created[0].labels).toEqual(["live-drift-failure"]); + expect(calls.created[0]).toMatchObject(repositoryCoordinates); + expect(calls.created[0].body).toContain("https://github.com/BigSimmo/Database/actions/runs/99"); + expect(calls.created[0].body).toContain("documents_title_trgm_idx"); + expect(calls.closed).toHaveLength(0); + }); + + it("updates the same issue on a repeat failure instead of stacking a second one", async () => { + const calls = await runRoutingScript({ + findings: sampleFindings, + openIssues: [pinnedIssue], + result: "failure", + }); + + expect(calls.created).toHaveLength(0); + expect(calls.updatedBodies).toEqual([expect.objectContaining({ ...repositoryCoordinates, issue_number: 1234 })]); + expect(calls.comments).toHaveLength(1); + expect(calls.comments[0].issue_number).toBe(1234); + expect(calls.comments[0]).toMatchObject(repositoryCoordinates); + expect(calls.comments[0].body).toContain("Still failing"); + expect(calls.closed).toHaveLength(0); + expect(calls.warnings.join(" ")).toContain("1234"); + }); + + it("does not present a run that died before the comparison as a clean schema", async () => { + const calls = await runRoutingScript({ findings: "", result: "failure" }); + + expect(calls.created).toHaveLength(1); + expect(calls.created[0]).toMatchObject(repositoryCoordinates); + expect(calls.created[0].body).toContain("not** evidence of a clean schema"); + expect(calls.created[0].body).not.toContain("UNEXPECTED DRIFT"); + }); + + it("comments the resolution and closes the issue on the next green run", async () => { + const calls = await runRoutingScript({ openIssues: [pinnedIssue], result: "success" }); + + expect(calls.comments).toHaveLength(1); + expect(calls.comments[0].body).toContain("Resolved"); + expect(calls.comments[0].body).toContain("https://github.com/BigSimmo/Database/actions/runs/99"); + expect(calls.closed).toEqual([ + { ...repositoryCoordinates, issue_number: 1234, state: "closed", state_reason: "completed" }, + ]); + expect(calls.created).toHaveLength(0); + expect(calls.listed).toEqual([{ ...repositoryCoordinates, labels: "live-drift-failure", state: "open" }]); + }); + + it("writes nothing when the check is green and no issue is open", async () => { + const calls = await runRoutingScript({ result: "success" }); + + expect(calls.created).toHaveLength(0); + expect(calls.comments).toHaveLength(0); + expect(calls.closed).toHaveLength(0); + expect(calls.updatedBodies).toHaveLength(0); + }); + + it("treats an unknown job result as a failure rather than closing the issue", async () => { + const calls = await runRoutingScript({ openIssues: [pinnedIssue], result: "" }); + + expect(calls.closed).toHaveLength(0); + expect(calls.updatedBodies).toEqual([expect.objectContaining({ ...repositoryCoordinates, issue_number: 1234 })]); + }); +});