From c61e839c9b4a89040496369e6afd46f1859d8d3d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 21 May 2026 10:31:04 -0700 Subject: [PATCH] test(cockpit-e2e-wiring): drift-guard for scope:* tags on cap projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 3 of 3 for the ci-scope thin-shim migration. PR 1 (#503) added scope:* tags to every CI-participating project; PR 2 (#507) rewrote ci-scope.mjs to read them via `nx show projects --affected --json`. This drift-guard asserts every cockpit cap project (angular + python under cockpit///) declares the expected tags: - scope:cockpit-e2e + scope:cockpit-examples on every cap project. - scope:cockpit-smoke on python caps that have a smoke target. Closes the silent-failure mode: future contributors adding a new cap can't forget the tags — the test names which project + which tag is missing. Excludes cockpit/ag-ui/* (no python sibling; different tag rules). See spec: docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/cockpit/cockpit-e2e-wiring.spec.ts | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/cockpit/cockpit-e2e-wiring.spec.ts b/apps/cockpit/cockpit-e2e-wiring.spec.ts index 0fe5a900..8c586edc 100644 --- a/apps/cockpit/cockpit-e2e-wiring.spec.ts +++ b/apps/cockpit/cockpit-e2e-wiring.spec.ts @@ -198,4 +198,47 @@ describe('cockpit e2e wiring', () => { expect(errors).toEqual([]); }); + + it('every cockpit cap project declares the expected scope:* tags', () => { + // Drift guard for the ci-scope thin-shim migration (PR #503/#507). + // The shim reads scope:* tags off projects nx considers affected to + // decide which CI gates to fire. A future contributor adding a new + // cap could silently forget the tags — their changes would + // underfire CI. This test catches that at build/test time. + const errors: string[] = []; + + const capProjects = listProjectJsonFiles(join(repoRoot, 'cockpit')) + .filter((p) => !p.includes('/ag-ui/')) // ag-ui has no python; out of scope + .filter((p) => p.includes('/angular/') || p.includes('/python/')) + .map((p) => ({ + path: p, + project: JSON.parse(readFileSync(p, 'utf8')) as { + name?: string; + tags?: string[]; + targets?: Record; + }, + })); + + for (const { path: p, project } of capProjects) { + const tags = new Set(project.tags ?? []); + const relPath = relative(repoRoot, p); + + // Every cap project (angular or python) must trigger cockpit_e2e + // + cockpit_examples. + for (const required of ['scope:cockpit-e2e', 'scope:cockpit-examples']) { + if (!tags.has(required)) { + errors.push(`${relPath}: missing required tag ${required}`); + } + } + + // Python caps with a smoke target must also trigger cockpit_smoke. + if (p.includes('/python/') && project.targets?.['smoke']) { + if (!tags.has('scope:cockpit-smoke')) { + errors.push(`${relPath}: has smoke target but missing scope:cockpit-smoke`); + } + } + } + + expect(errors).toEqual([]); + }); });