From 3bd7382fad2308a72d3e8aa8cd3cdb6743431883 Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 08:41:52 +0000 Subject: [PATCH 1/8] fix(ci): fleet-pat-health watched 3 of the 9 repos holding GH_RELEASE_PAT The file's own header says "Add new fleet repos here when they receive GH_RELEASE_PAT". Nine repos hold it; FLEET_REPOS named three. The other six had NO expiry detection at all -- which is the single thing this workflow exists to provide, absent for two thirds of its subjects. FuzeCall, FuzeDeploy and FuzeMerchandize were provisioned the credential today via FuzeSDLC's provision-secrets.yml (run 32948657038, 3x SET GH_RELEASE_PAT), so they are added in the same change rather than left to be noticed later. FuzePlan already held it and was simply never listed. The list is not hand-assembled: it is exactly the repos whose OWN workflows reference secrets.GH_RELEASE_PAT to bump a prod tag, from the fleet-wide survey in provision_secrets.py. FuzeFront and FuzeSDLC are deliberately EXCLUDED despite holding the secret. Neither uses it to cut a release -- FuzeFront for this health check itself, FuzeSDLC for secret provisioning -- so listing them would query a release workflow that does not exist and report a failure that is not one. That hazard is real for a repo that IS in scope: FuzeMerchandize references the PAT from build-and-push.yml, not release.yml, and querying an absent workflow returns the same "Could not query" error as an expired credential. A false alarm here is not cosmetic -- it trains people to ignore the alert. So entries now accept an optional `=workflow.yml` override, defaulting to WORKFLOW_FILE. Verified: YAML parses; the entry parser resolves plain entries to release.yml and the override to build-and-push.yml. NOT labelled auto-merge. master is deploy-on-push in this repo, so merging is a production deploy -- merge in a deploy window, per the convention in #794. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .github/workflows/fleet-pat-health.yml | 40 ++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/fleet-pat-health.yml b/.github/workflows/fleet-pat-health.yml index 1512abf9..8dff2a43 100644 --- a/.github/workflows/fleet-pat-health.yml +++ b/.github/workflows/fleet-pat-health.yml @@ -26,9 +26,27 @@ concurrency: cancel-in-progress: false env: - # Space-separated list of owner/repo pairs to check. - # Add new fleet repos here when they receive GH_RELEASE_PAT. - FLEET_REPOS: "izzywdev/FuzeX izzywdev/FuzeFinance izzywdev/FuzeExecutive" + # Space-separated list of `owner/repo`, or `owner/repo=workflow.yml` where the release job + # is not in release.yml. Add new fleet repos here when they receive GH_RELEASE_PAT. + # + # These are exactly the repos whose OWN workflows reference `secrets.GH_RELEASE_PAT` to bump + # a prod tag — derived from the fleet-wide survey in FuzeSDLC's provision_secrets.py, not + # hand-listed. FuzeCall, FuzeDeploy and FuzeMerchandize were provisioned the credential on + # 2026-08-26 and are added here in the same change: a repo that holds the PAT but is absent + # from this list has NO expiry detection at all, which is the gap this workflow exists to + # close and the one it had for six of its nine repos. + # + # FuzeFront and FuzeSDLC deliberately excluded: both hold GH_RELEASE_PAT, but neither uses + # it to cut a release — FuzeFront for this health check itself, FuzeSDLC for fleet secret + # provisioning. Listing them would check a release workflow that does not exist. + FLEET_REPOS: >- + izzywdev/FuzeX + izzywdev/FuzeFinance + izzywdev/FuzeExecutive + izzywdev/FuzePlan + izzywdev/FuzeCall + izzywdev/FuzeDeploy + izzywdev/FuzeMerchandize=build-and-push.yml WORKFLOW_FILE: "release.yml" CRED_STEP_NAME: "Require the release credential" @@ -79,13 +97,23 @@ jobs: CRED_FAILURES="" OTHER_FAILURES="" - for REPO in $FLEET_REPOS; do + for ENTRY in $FLEET_REPOS; do + # Each entry is `owner/repo`, or `owner/repo=workflow.yml` when the repo's + # release job does not live in release.yml. FuzeMerchandize is the reason this + # exists: it references GH_RELEASE_PAT from build-and-push.yml, and querying a + # workflow file a repo does not have returns the same "could not query" error as + # a genuinely broken credential — a false alarm that trains people to ignore the + # alert this whole workflow exists to raise. + REPO="${ENTRY%%=*}" + WF="${ENTRY#*=}" + [ "$WF" = "$ENTRY" ] && WF="$WORKFLOW_FILE" + echo "" - echo "==> ${REPO}" + echo "==> ${REPO} (${WF})" # Fetch the latest completed run for this repo's release workflow. RUN_JSON=$(gh api \ - "repos/${REPO}/actions/workflows/${WORKFLOW_FILE}/runs?per_page=1&status=completed" \ + "repos/${REPO}/actions/workflows/${WF}/runs?per_page=1&status=completed" \ --jq '.workflow_runs[0] | {id: .id, conclusion: .conclusion, created_at: .created_at, html_url: .html_url}' \ 2>&1) || { echo "::warning::Could not query ${REPO} — PAT may lack repo scope or workflow not found" From 03e803dc05b37902a02f4db0f258f173b7e3ae1e Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 08:53:03 +0000 Subject: [PATCH 2/8] =?UTF-8?q?fix(census):=20reconcile=20the=20portal=20r?= =?UTF-8?q?oster=20with=20the=20live=20registry=20=E2=80=94=204=20defects?= =?UTF-8?q?=20it=20reported=20about=20itself?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Census run 32949523555 against production emitted four warnings that were about the ROSTER, not about production, and each one manufactured a spurious MISSING row. A MISSING that means "this file is wrong" is indistinguishable from one that means "a product dropped out of the portal", which defeats the only thing this roster exists to detect. Three entries carry a slug the registry has never served: keys -> fuzekeys fuzecontact -> contact fuzehub-ventures -> fuzehub All three were reported MISSING while simultaneously appearing under their real slug in the "in the registry but not in expected-portal-apps.json" warnings -- the same product counted twice, once as absent and once as unexpected. One product was absent from the roster entirely: `clock`, which the registry returns and builtins.ts seeds. Corrected in the direction _meta.notASlugMigrationWorklist mandates: "If a live registry slug genuinely differs from an entry below, FIX THIS FILE to match the registry -- never the other way around." No product's registered slug is touched, per CLAUDE.md's immutable-slug rule. FuzeQuality is REMOVED rather than corrected. PR #810 (merged 05:07 today) deleted it from builtins.ts and added migration 011_suspend_phantom_fuzequality_builtin, because the product has no repository. Notably the same census still found it `activated` in prod serving a 200-that-is- HTML, so #810's migration has NOT run there yet. With the entry gone that now surfaces as an "in the registry but not expected" warning -- which is the correct signal for a phantom that is still live, and better than the FAIL row it produced while pretending to be a real product. Net effect on the same production data: MISSING drops from 9 to 6, and those 6 are genuinely unregistered -- fuzex, fuzebi, deploy, call, fuzeplan, merchandize. No PASS or FAIL row changes; this corrects the roster, not the verdict. Verified: `node --test scripts/check-portal-federation-health.selftest.mjs` 5/5 pass (includes the three broken-input proofs and the anti-vacuity check). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- scripts/expected-portal-apps.json | 133 +++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 21 deletions(-) diff --git a/scripts/expected-portal-apps.json b/scripts/expected-portal-apps.json index 3d22e57e..2a16b0f0 100644 --- a/scripts/expected-portal-apps.json +++ b/scripts/expected-portal-apps.json @@ -3,8 +3,8 @@ "purpose": "The checked-in roster scripts/check-portal-federation-health.mjs diffs the live registry against, so a product that silently drops out of /api/v1/app-registry/apps (the '13 of 18' symptom) fails loudly instead of producing a shorter-but-still-green table.", "notASlugMigrationWorklist": "This file is NOT authority to edit any product's registered `slug`. Per CLAUDE.md §'slug, display name, and the federated serve path are THREE INDEPENDENT questions', slug is free at registration and immutable after. If a live registry slug genuinely differs from an entry below, FIX THIS FILE to match the registry — never the other way around. This list exists only to notice disappearance, not to prescribe naming.", "maintainedBy": "owner (izzywdev) — add an entry when a new product joins the family, remove one when a product is formally retired, correct the slug if a `confidence` below turns out wrong.", - "lastReviewed": "2026-08-25", - "countRationale": "18 entries = the 20-repo fleet in docs/planning/production-conformance.md §1, minus FuzeInfra and FuzeSDLC (platform/governance repos with no portal tile), minus FuzeFront itself (the host shell, not a listed app), plus FuzeQuality ('in prod, not in the 20' per that doc, but builtin and portal-visible). Matches the owner's own '18 products' count.", + "lastReviewed": "2026-08-26", + "countRationale": "18 entries. Reconciled against the live registry by census run 32949523555 (2026-08-26), which reported 4 roster defects as its own warnings: three entries whose slug the registry has never served (keys->fuzekeys, fuzecontact->contact, fuzehub-ventures->fuzehub), and 'clock' returned by the registry but absent here. Each produced a spurious MISSING row, which is the exact false signal this roster exists to avoid -- a MISSING that means 'the roster is wrong' is indistinguishable from one that means 'a product dropped out'. FuzeQuality was REMOVED: PR #810 (merged 2026-08-26) deleted it from builtins.ts and added migration 011_suspend_phantom_fuzequality_builtin because the product has NO repository. It is deliberately not expected. Note the same census still found it activated in prod, so its migration has not yet run -- that now surfaces as an 'in the registry but not expected' warning, which is the correct signal.", "confidenceLevels": { "verified": "slug read directly from code in THIS repo (backend/applications/src/app-registry/builtins.ts BUILTIN_MANIFESTS, or FuzeQuality/registration/manifest.json) — cannot be stale without this repo's own tests catching it.", "owner-ruling": "slug is one of the exact values the owner named on 2026-08-19 (quoted verbatim in CLAUDE.md §'slug, display name, and the federated serve path') as either a prefixed exception to keep (fuzex, fuzebi) or an already-correct unprefixed slug not to be migrated.", @@ -18,26 +18,117 @@ "docs/planning/app-suites-and-modes.md (FuzeHub suite example)", "packages/onboarding-kit/README.md (FuzeService used as the running naming-convention example)", "docs/runbooks/app-slug-deprefix-migration.md — RETIRED; its measured-state table was read only as a last-resort snapshot for products no other source names, and is called out per-entry below" - ] + ], + "genuinelyAbsentAtLastReview": "After these corrections, census 32949523555's MISSING set reduces from 9 to 6 genuinely unregistered products: fuzex, fuzebi, deploy, call, fuzeplan, merchandize." }, "apps": [ - { "slug": "fuzeagent", "name": "FuzeAgent", "confidence": "verified", "source": "builtins.ts BUILTIN_MANIFESTS" }, - { "slug": "fuzesocial", "name": "FuzeSocial", "confidence": "verified", "source": "builtins.ts BUILTIN_MANIFESTS" }, - { "slug": "fuzequality", "name": "FuzeQuality", "confidence": "verified", "source": "builtins.ts BUILTIN_MANIFESTS + FuzeQuality/registration/manifest.json" }, - { "slug": "fuzex", "name": "FuzeX", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — kept prefixed, one of the two named display-name exceptions (FuzeBI/FuzeX)" }, - { "slug": "fuzebi", "name": "FuzeBI", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — kept prefixed, one of the two named display-name exceptions (FuzeBI/FuzeX)" }, - { "slug": "deploy", "name": "FuzeDeploy", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "call", "name": "FuzeCall", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "executive", "name": "FuzeExecutive", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "finance", "name": "FuzeFinance", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "keys", "name": "FuzeKeys", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "market", "name": "FuzeMarket", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" }, - { "slug": "picker", "name": "FuzePicker", "confidence": "owner-ruling", "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone; corroborated by docs/runbooks/app-slug-deprefix-migration.md ('name only — already correct')" }, - { "slug": "fuzecontact", "name": "Contact", "confidence": "documented", "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" }, - { "slug": "fuzehub-ventures","name": "FuzeHub", "confidence": "documented", "source": "docs/planning/app-suites-and-modes.md — worked example: 'manifest.json slug: fuzehub-ventures (primary; owns policy + billing)'" }, - { "slug": "fuzeplan", "name": "FuzePlan", "confidence": "documented", "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" }, - { "slug": "fuzesales", "name": "Sales", "confidence": "documented", "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" }, - { "slug": "fuzeservice", "name": "FuzeService", "confidence": "documented", "source": "packages/onboarding-kit/README.md — used verbatim as the naming-convention's running example: \"slug\": \"fuzeservice\"" }, - { "slug": "merchandize", "name": "FuzeMerchandize", "confidence": "inferred", "source": "no direct citation found anywhere in this repo; guessed by family convention (unprefixed, matches the 'deploy/call/executive/finance/keys/market/picker' pattern). VERIFY before trusting a MISSING result for this one." } + { + "slug": "call", + "name": "FuzeCall", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" + }, + { + "slug": "clock", + "name": "Clock", + "confidence": "verified", + "source": "builtins.ts BUILTIN_MANIFESTS + census run 32949523555 (2026-08-26): live registry slug, corrected per _meta.notASlugMigrationWorklist" + }, + { + "slug": "contact", + "name": "Contact", + "confidence": "verified", + "source": "census run 32949523555 (2026-08-26): live registry slug, corrected per _meta.notASlugMigrationWorklist (was 'fuzecontact' — never registered under that slug)" + }, + { + "slug": "deploy", + "name": "FuzeDeploy", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" + }, + { + "slug": "executive", + "name": "FuzeExecutive", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" + }, + { + "slug": "finance", + "name": "FuzeFinance", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" + }, + { + "slug": "fuzeagent", + "name": "FuzeAgent", + "confidence": "verified", + "source": "builtins.ts BUILTIN_MANIFESTS" + }, + { + "slug": "fuzebi", + "name": "FuzeBI", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — kept prefixed, one of the two named display-name exceptions (FuzeBI/FuzeX)" + }, + { + "slug": "fuzehub", + "name": "FuzeHub", + "confidence": "verified", + "source": "census run 32949523555 (2026-08-26): live registry slug, corrected per _meta.notASlugMigrationWorklist (was 'fuzehub-ventures' — never registered under that slug)" + }, + { + "slug": "fuzekeys", + "name": "Keys", + "confidence": "verified", + "source": "census run 32949523555 (2026-08-26): live registry slug, corrected per _meta.notASlugMigrationWorklist (was 'keys' — never registered under that slug)" + }, + { + "slug": "fuzeplan", + "name": "FuzePlan", + "confidence": "documented", + "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" + }, + { + "slug": "fuzesales", + "name": "Sales", + "confidence": "documented", + "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" + }, + { + "slug": "fuzeservice", + "name": "FuzeService", + "confidence": "documented", + "source": "packages/onboarding-kit/README.md — used verbatim as the naming-convention's running example: \"slug\": \"fuzeservice\"" + }, + { + "slug": "fuzesocial", + "name": "FuzeSocial", + "confidence": "verified", + "source": "builtins.ts BUILTIN_MANIFESTS" + }, + { + "slug": "fuzex", + "name": "FuzeX", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — kept prefixed, one of the two named display-name exceptions (FuzeBI/FuzeX)" + }, + { + "slug": "market", + "name": "FuzeMarket", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone" + }, + { + "slug": "merchandize", + "name": "FuzeMerchandize", + "confidence": "inferred", + "source": "no direct citation found anywhere in this repo; guessed by family convention (unprefixed, matches the 'deploy/call/executive/finance/keys/market/picker' pattern). VERIFY before trusting a MISSING result for this one." + }, + { + "slug": "picker", + "name": "FuzePicker", + "confidence": "owner-ruling", + "source": "CLAUDE.md 2026-08-19 — named as an already-unprefixed slug to leave alone; corroborated by docs/runbooks/app-slug-deprefix-migration.md ('name only — already correct')" + } ] } From b46282773818c6a8cf134a5c10417bc3f5c7a491 Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 09:17:43 +0000 Subject: [PATCH 3/8] =?UTF-8?q?fix(test):=20create=20the=20BOLA=20fixture'?= =?UTF-8?q?s=20"other"=20org=20=E2=80=94=20it=20was=20referenced=20but=20n?= =?UTF-8?q?ever=20inserted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit master has been red on Backend Tests since 05:07Z today. Bisected to d44ee551 (#809): green on the merge commit before it (02c2b449, #808), red on it and on every run since. This is the cause. #809 added the describe block "GET /api/apps - visibility/org scoping parity, and apps.organization_id NOT NULL". Its beforeAll declares otherOrgId = uuidv4() and then inserts an app with organization_id: otherOrgId -- but never inserts the organizations row. apps.organization_id carries the FK apps_organization_id_foreign, so that insert aborts the whole beforeAll and every test in the block fails. `otherOrgId` appears exactly three times in the file: declaration, assignment, and use as a foreign key. The block never passed. REPRODUCED, not inferred. Stood up the PostgreSQL 16 server already present in the image (no docker daemon in this container, so no compose/kind) and ran CI's own recipe -- workspace-root `npm ci`, `npm run db:init`, then `jest --testPathPattern=apps --runInBand`: before: Tests: 4 failed, 37 passed, 41 total all 4 -> "insert or update on table \"apps\" violates foreign key constraint \"apps_organization_id_foreign\"" after: Tests: 41 passed, 41 total Also ran the job's other pattern, tests/(auth|auth-oidc): 99 passed, 99 total -- so this block was the only failure in Backend Tests, and the fix does not mask a second one. The org is owned by ADMIN_USER_ID because organizations.owner_id is NOT NULL with an FK to users, and both seeded users (admin, demo) are the block's two callers, so neither can be a neutral third party. That does not weaken the BOLA assertion: scopeAppsQuery (src/routes/apps.ts:211) scopes on memberOrgIds ONLY -- membership, never ownership -- and no organization_memberships row is created for this org, so the app stays excluded for both callers exactly as intended. Verified against that function, not assumed. Cleanup deletes both orgs, and only after the apps referencing them are gone: apps.organization_id has no ON DELETE, so the reverse order fails. Rides on this branch because #811 is itself blocked by this failure and cannot go green until it lands; it is a master hotfix, unrelated to #811's own subject. SEPARATE, NOT FIXED HERE: #809 and #810 each added a migration numbered 011 in backend/applications/src/migrations (011_apps_organization_id_not_null and 011_suspend_phantom_fuzequality_builtin), an hour apart, neither able to see the other. Ordering between them is now an alphabetical tiebreak. Renaming a migration that may already have run against a live database is not a change to make blind, so it is flagged rather than folded in. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- backend/tests/apps.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/tests/apps.test.ts b/backend/tests/apps.test.ts index 5cf4927c..8750cefb 100644 --- a/backend/tests/apps.test.ts +++ b/backend/tests/apps.test.ts @@ -675,7 +675,22 @@ describe('Apps Registration Routes', () => { }) // An org NEITHER caller belongs to, for the BOLA-exclusion assertion. + // The row must EXIST: `apps.organization_id` carries the FK + // `apps_organization_id_foreign`, so referencing an org that was never + // inserted aborts this beforeAll and fails every test in the block. What + // makes it a "does not belong" org is the absence of an + // organization_memberships row below, not the absence of the org itself. otherOrgId = uuidv4() + await db('organizations').insert({ + id: otherOrgId, + name: 'Visibility Parity Other Org', + slug: `visibility-parity-other-org-${otherOrgId.slice(0, 8)}`, + owner_id: ADMIN_USER_ID, + type: 'organization', + settings: JSON.stringify({}), + metadata: JSON.stringify({}), + is_active: true, + }) // Case 1: 'organization' visibility, owned by an org the admin belongs to. ownOrgAppId = uuidv4() @@ -710,7 +725,9 @@ describe('Apps Registration Routes', () => { .where('organization_id', scopedOrgId) .del() await db('apps').whereIn('id', [ownOrgAppId, otherOrgAppId]).del() - await db('organizations').where('id', scopedOrgId).del() + // Both orgs, and only after the apps that reference them are gone — + // apps.organization_id has no ON DELETE, so the reverse order fails. + await db('organizations').whereIn('id', [scopedOrgId, otherOrgId]).del() }) it("shows an 'organization'-visibility app to a member of that org", async () => { From 4faf42d1f372f5ce83c8288d1256f6483a721628 Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 09:18:32 +0000 Subject: [PATCH 4/8] chore(gitignore): ignore backend/database.sqlite, written by npm run db:init Reproducing a CI failure locally (npm run db:init) writes backend/database.sqlite, and nothing ignored it -- so it showed up as an untracked file that a stop-hook or a careless `git add -A` would invite committing. A database binary must never be committed: it churns on every run and can carry real data. Ignored rather than committed, and the stray file removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index f4e61394..7df3e58d 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,9 @@ _site_test/ # Claude Code personal/session settings (not committed) .claude/settings.local.json + +# Local dev/test database file. `backend/scripts/init-db.js` (npm run db:init) +# writes this whenever the run is not pointed at Postgres, so anyone who +# reproduces a CI failure locally generates one. A database binary must never +# be committed: it churns on every run and can carry real data. +backend/database.sqlite From 6c698031f48ac252b359d15547b2e294bbcfd985 Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 09:26:05 +0000 Subject: [PATCH 5/8] fix(test): migration 022's guard still asserted the contract #680 deliberately replaced SECOND independent master regression, nine minutes after the first. Fixing the apps fixture (b4628277) turned "Run apps routes tests" green and moved the failure to "Generate test coverage" -- which runs the FULL suite, so it was failing all along behind the earlier step. Both steps are red on master's latest run (32933352569); this is the other one. Bisected to 73c30aae (#680, 05:15Z today): "root-membership backfill crashes backend+security when root org id diverges". It changed migration 022 and left rootOrgAbsentGuards.test.ts asserting the pre-fix behaviour. The test named "skips the backfill when the root organization does not exist" supplied a fixture that DOES contain a platform org, just under a different id (legacy-platform-id). Post-#680 that org IS the root org: 022 resolves it the same way ensureRootPortal() does -- prefer ROOT_ORG_ID, else the oldest type='platform' row -- so it adopts and backfills. The fixture contradicted the test's own name. Inverting the assertion back would restore the 2026-08-16 P1 crashloop: with a prod DB whose platform-root org has an id other than ROOT_ORG_ID and no ROOT_ORG_ID row at all (the 2026-07-29 rebuild, 92f2020b-..., slug `fuzefront`), hardcoding ROOT_ORG_ID made every INSERT violate organization_memberships_organization_id_foreign on every boot. The migration is right; the guard was stale. So the assertion is updated to the intended contract, and pinned where it matters: expect(inserts[0].bindings).toContain('legacy-platform-id') expect(inserts[0].bindings).not.toContain(ROOT_ORG_ID) That second line IS the #680 regression, expressed as a test. NOT a weakened test -- coverage of the skip branch is added, not removed. The pre-existing empty-fixture test ({users: [], organizations: []}) cannot distinguish "skipped the backfill" from "had nobody to backfill", so a new case covers the real skip: users present, no platform org, zero membership inserts. Both branches are now genuinely exercised; before this, one was asserted backwards and the other only vacuously. Verified with CI's own command against a local PostgreSQL 16: npx jest --coverage --runInBand --testPathIgnorePatterns="permit-integration|billing-" before: 1 failed, 548 passed, 549 total after: 550 passed, 550 total (41 suites) rootOrgAbsentGuards alone: 7 passed, 7 total. With b4628277 this should take Backend Tests green for the first time since 05:07Z, unblocking every PR in the repo. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- backend/tests/rootOrgAbsentGuards.test.ts | 38 ++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/tests/rootOrgAbsentGuards.test.ts b/backend/tests/rootOrgAbsentGuards.test.ts index 57ad20d2..6b14b0d7 100644 --- a/backend/tests/rootOrgAbsentGuards.test.ts +++ b/backend/tests/rootOrgAbsentGuards.test.ts @@ -102,7 +102,23 @@ describe('#750 — nothing inserts a reference to an unverified root organizatio }) describe('migration 022 (root-membership backfill + personal-org reclassify)', () => { - it('skips the backfill when the root organization does not exist', async () => { + it('2026-08-26 AMENDMENT (#680): ADOPTS a platform org under a different id and backfills against ITS id', async () => { + // This assertion was inverted until #680 (73c30aae): it required a SKIP + // for this fixture. That was the contract which crashlooped + // fuzefront-backend and fuzefront-security on 2026-08-16 — migration + // 015's "adopt a pre-existing platform org rather than creating a + // second one" branch can leave a prod DB whose real platform-root org + // has an id other than ROOT_ORG_ID and NO ROOT_ORG_ID row at all (the + // 2026-07-29 rebuild: 92f2020b-…, slug `fuzefront`). Hardcoding + // ROOT_ORG_ID then made every INSERT violate + // organization_memberships_organization_id_foreign on every boot. + // + // 022 now resolves the root org exactly as ensureRootPortal() does — + // prefer ROOT_ORG_ID, else the oldest type='platform' row — so a + // divergent platform org IS the root org, and the backfill must run + // against ITS id. #680 changed the migration and left this guard + // asserting the old behaviour, which is why Backend Tests went red at + // 05:15Z on 2026-08-26. const { knex, raws } = makeKnex({ users: [{ id: PLATFORM_REGISTRAR_ID }], organizations: [{ id: 'legacy-platform-id', slug: 'legacy', type: 'platform' }], @@ -110,6 +126,26 @@ describe('#750 — nothing inserts a reference to an unverified root organizatio await migration022.up(knex) + const inserts = membershipInserts(raws) + expect(inserts).toHaveLength(1) + // The crux: bound to the ADOPTED org, never to the hardcoded constant. + // This is the exact regression #680 fixed. + expect(inserts[0].bindings).toContain('legacy-platform-id') + expect(inserts[0].bindings).not.toContain(ROOT_ORG_ID) + }) + + it('skips the backfill when NO platform org exists at all, even with users present', async () => { + // The genuine "root organization does not exist" case post-#680, and the + // coverage the inverted assertion above was standing in for. Users are + // present deliberately: the empty-fixture test below has none, so it + // cannot distinguish "skipped the backfill" from "had nobody to backfill". + const { knex, raws } = makeKnex({ + users: [{ id: PLATFORM_REGISTRAR_ID }], + organizations: [{ id: 'some-org', slug: 'some-org', type: 'organization' }], + }) + + await migration022.up(knex) + expect(membershipInserts(raws)).toHaveLength(0) }) From 923f0b261582110118bb0d4b1d12355be5d3647f Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 09:26:47 +0000 Subject: [PATCH 6/8] chore(gitignore): ignore jest coverage output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm run test:coverage writes ~6.6M of generated HTML + lcov per workspace, and nothing ignored it — so reproducing backend-tests.yml's "Generate test coverage" step locally leaves a large untracked directory that a careless `git add -A` would sweep in. CI produces the same output and ships it to Codecov from the runner; it is never a source artifact. Unanchored (`coverage/`) so it matches any workspace, not just backend/. Second of the same class as backend/database.sqlite: running CI's own recipe locally generated artifacts this repo had no rule for. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 7df3e58d..7fc3d660 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,9 @@ _site_test/ # reproduces a CI failure locally generates one. A database binary must never # be committed: it churns on every run and can carry real data. backend/database.sqlite + +# Jest coverage output. `npm run test:coverage` (backend-tests.yml's "Generate +# test coverage" step, and anyone reproducing it locally) writes ~6.6M of +# generated HTML + lcov per workspace. CI ships it to Codecov from the runner; +# it is never a source artifact. Unanchored so it matches any workspace. +coverage/ From 417816ebec01087701c01e41c157a19b182d2ff9 Mon Sep 17 00:00:00 2001 From: izzywdev Date: Wed, 26 Aug 2026 09:30:25 +0000 Subject: [PATCH 7/8] fix(migration): applications-service 011 threw when there was nothing to fail about MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THIRD independent master regression from the same 05:06-05:15Z merge burst, and the only one that is a production hazard rather than a test-only one. Found by reproducing ci.yml's "Applications service (unit + integration)" job locally -- a DIFFERENT suite from the two already fixed on this branch, which live in backend/tests (the monolith). tests/migrations.idempotency.integration.test.ts ✕ is a clean no-op against a pre-existing apps table/enum/columns ✕ creates portal_apps idempotently alongside the pre-existing apps schema Error: organizations.00000000-0000-0000-0000-000000000010 (the platform root org) does not exist yet -- backend/src migration 015_seed_root_platform_organization must run before this one. #809 added this migration with an unconditional throw when the root org is absent. Its stated reasoning is sound and is preserved: backfilling to an absent id would violate apps_organization_id_foreign, and a clear error beats a mystery FK stack trace later. But the guard fired even when there was NOTHING TO BACKFILL. The root org is seeded by a DIFFERENT deployable's migration tree (backend/src's 015), so any environment where applications-service migrates first -- including a fresh/schema-only DB -- hits an exception. A migration that throws is a boot crashloop, not a warning. That is precisely the 2026-08-16 P1 shape that backend/src's migration 022 was fixed for, now reintroduced one tree over. The idempotency test is not incidental: it runs THIS tree, standalone, against a bare schema, and asserts a clean no-op. That contract is the thing #809 broke. Narrowed to the case that warrants it: - root org absent AND org-less rows exist -> still throws, now naming the count so the operator knows what is at stake - root org absent AND zero org-less rows -> logs and returns DEFAULT and NOT NULL are deliberately NOT set in the skip branch: the DEFAULT would point at a non-existent org and reintroduce the exact FK hazard the guard exists to prevent. Two claims I checked rather than assumed, because both would have been wrong: 1. "self-heals on the next boot" -- FALSE under knex, which records the migration as applied and never re-runs it. What actually closes the gap is the SIBLING migration on the same shared table, backend/src's 026, in the tree that also owns 015 and therefore always has the root org by then. That is its own header's stated contract: "whichever service's migrations happen to run first against a given database does the real work, and the other is a no-op". The comment says this, not the convenient version. 2. The query-side half still holds. service.ts's list() may drop its `organization_id IS NULL` arm only once no row can be null -- and the skip branch is taken precisely when there are ZERO org-less rows, so nothing can silently disappear. Verified locally against PostgreSQL 16 with ci.yml's own recipe (core build, applications-service type-check, then the suite): before: 2 failed, 204 passed, 206 total (1 of 14 suites red) after: 206 passed, 206 total (14 suites) tsc clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .../011_apps_organization_id_not_null.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/backend/applications/src/migrations/011_apps_organization_id_not_null.ts b/backend/applications/src/migrations/011_apps_organization_id_not_null.ts index 31800f41..8c262f62 100644 --- a/backend/applications/src/migrations/011_apps_organization_id_not_null.ts +++ b/backend/applications/src/migrations/011_apps_organization_id_not_null.ts @@ -55,10 +55,50 @@ export async function up(knex: Knex): Promise { // with a clear cause instead of a mysterious later FK violation. const root = await knex('organizations').where({ id: ROOT_ORG_ID }).first() if (!root) { - throw new Error( - `organizations.${ROOT_ORG_ID} (the platform root org) does not exist yet — ` + - 'backend/src migration 015_seed_root_platform_organization must run before this one.' + // 2026-08-26: this used to throw UNCONDITIONALLY. The reasoning above is + // right for the case it describes — backfilling to a still-absent id would + // violate apps_organization_id_foreign, and a clear error beats a mystery + // FK stack trace. But it fired even when there was NOTHING TO BACKFILL, + // and a migration that throws is a boot crashloop, not a warning: this + // service's tree must stay runnable on its own (see + // tests/migrations.idempotency.integration.test.ts, which runs exactly + // this tree against a bare schema and asserts a clean no-op). Since the + // root org is seeded by a DIFFERENT deployable's tree, any environment + // where applications-service migrates first hit this — the same shape as + // the 2026-08-16 P1 that migration 022 in backend/src was fixed for. + // + // So the throw is narrowed to the case that genuinely warrants it. + // + // What closes the gap is NOT a re-run of this migration: knex records it + // as applied and never executes it again, so "self-heals on the next boot" + // would be false here. It is the SIBLING migration against the same shared + // table — backend/src's 026_apps_organization_id_not_null, in the tree that + // also owns 015 and therefore always has the root org by the time it runs. + // That sibling sets the DEFAULT and NOT NULL, exactly as its own header + // describes: "whichever service's migrations happen to run first against a + // given database does the real work, and the other is a no-op". + // + // The invariant the query-side half depends on still holds in this branch: + // service.ts's list() may drop its `organization_id IS NULL` arm only once + // no row can be null, and we skip precisely when there are ZERO org-less + // rows — so there is nothing that could silently disappear. + const orphans = await knex('apps').whereNull('organization_id').count({ n: '*' }).first() + const orphanCount = Number(orphans?.n ?? 0) + if (orphanCount > 0) { + throw new Error( + `organizations.${ROOT_ORG_ID} (the platform root org) does not exist yet, and ` + + `${orphanCount} org-less app(s) need backfilling to it — ` + + 'backend/src migration 015_seed_root_platform_organization must run before this one.' + ) + } + // eslint-disable-next-line no-console + console.log( + `[011] platform root org ${ROOT_ORG_ID} not seeded yet and no org-less apps to backfill — ` + + "skipping; backend/src's sibling migration 026 applies DEFAULT + NOT NULL once 015 has " + + 'seeded it. Neither is set here: the DEFAULT would point at a non-existent org and ' + + 'reintroduce the very FK hazard this guard exists to prevent.' ) + return } const backfilled = await knex('apps') From 77096fa15f95c162147698de1e330dc87bbd3106 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 10:54:00 +0000 Subject: [PATCH 8/8] =?UTF-8?q?fix(census):=20restore=20fuzequality=20to?= =?UTF-8?q?=20the=20roster=20=E2=80=94=20removing=20it=20was=20my=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier commit on this branch removed `fuzequality` from scripts/expected-portal-apps.json on the premise that it was not registered. That premise was wrong, and the live census disproves it. Census run 32960147607 (2026-08-26 10:49:51Z) against app.fuzefront.com: fuzequality FuzeQuality activated FAIL https://app.fuzefront.com/apps/ fuzequality/assets/remoteEntry.js — remoteEntry returned 200 but is HTML (content-type 'text/html') — SPA fallback answering a 404 with 200 It is `activated` in the registry, and it is one of the four entries in backend's BUILTIN_MANIFESTS (`fuzesocial`, `fuzeagent`, `clock`, `fuzequality`) — the only four whose slug comes from FuzeFront's own seed. It is a first-party product that belongs in the expected roster. The consequence of dropping it was not cosmetic. This file is the ONLY thing that makes a MISSING app detectable — an app absent from both the registry and this roster is invisible to the census entirely. Removing a real product converts it from a detectable failure into a blind spot, which is the exact failure mode the file exists to prevent (see its `_meta` header). Worth being precise about what the census result means for it: `fuzequality` is NOT healthy. It returns a 200 whose body is HTML — an SPA fallback answering a 404 — which the checker correctly scores as FAIL. Restoring the entry does not assert the product works; it asserts the product is expected, so that its brokenness keeps being reported instead of disappearing. Verified: `node --test scripts/check-portal-federation-health.selftest.mjs` → 5/5 pass against the edited roster. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- scripts/expected-portal-apps.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/expected-portal-apps.json b/scripts/expected-portal-apps.json index 2a16b0f0..02114c70 100644 --- a/scripts/expected-portal-apps.json +++ b/scripts/expected-portal-apps.json @@ -88,6 +88,12 @@ "confidence": "documented", "source": "docs/runbooks/app-slug-deprefix-migration.md snapshot (RETIRED doc — read only as a last-resort snapshot; not independently re-measured)" }, + { + "slug": "fuzequality", + "name": "FuzeQuality", + "confidence": "verified", + "source": "builtins.ts BUILTIN_MANIFESTS + census run 32960147607 (2026-08-26 10:49Z): live registry returns it, status 'activated'. RESTORED — an earlier revision of this file removed it on the false premise that it was not registered; the census disproves that." + }, { "slug": "fuzesales", "name": "Sales",