From efff0c7d92a091f752650a384843ba6fa2d22e67 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:00:43 -0800 Subject: [PATCH 1/3] ci(tests): frontend filter covers the rest of its out-of-tree jest inputs Follow-up to #3227. Sable's gate flagged more real frontend-test file reads still outside the changes-job frontend filter; verified each against the test source: - e2e/ v2-layout-invariants.test.ts:2113 readdirSync + readFileSync each *.spec.ts - docs/adr/ v2-layout-invariants.test.ts:4685 readdirSync (ADR_COUNT assertion) - docs/cli/ V2BYOHandoffCard.test.tsx:55 asserts opencode.md + dsh.md exist - docs-site/ cursorProjectRule.test.ts:11 + runtimeClaims.test.ts read .mdx there - cli/src/commands/login.js V2ConnectHonesty.test.ts:105 readFileSync - commonly-mcp/src/tools.js cursorProjectRule.test.ts:25 readFileSync Not a bare docs/ prefix (per Sable): that would run the frontend leg on every docs-only PR. These are the exact paths the suite reads. Non-urgent because the merge queue runs the full matrix regardless; this makes the per-PR signal match what the tests actually depend on. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015AKgpNr8tFMMj8aFvFPUdA --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3dab426d2..f6b6f31c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,7 +89,7 @@ jobs: files=$(git diff --no-renames --name-only "$base"...HEAD 2>/dev/null || git diff --no-renames --name-only "$base" HEAD) printf '%s\n' "$files" printf '%s\n' "$files" | grep -qE '^(backend/|cli/|packages/|package(-lock)?\.json)' && backend=true - printf '%s\n' "$files" | grep -qE '^(frontend/|backend/|README\.md$|docs/assets/)' && frontend=true + printf '%s\n' "$files" | grep -qE '^(frontend/|backend/|README\.md$|docs/assets/|e2e/|docs/adr/|docs/cli/|docs-site/|cli/src/commands/login\.js$|commonly-mcp/src/tools\.js$)' && frontend=true printf '%s\n' "$files" | grep -qE '^(k8s/|charts?/|helm/|scripts/verify-backend-worker-chart\.sh$|scripts/monitoring/apply-alerting\.py$|docs/runbooks/alerting\.md$|package(-lock)?\.json)' && chart=true # A change to this workflow runs the full suite as a self-test, so a # PR that edits the test pipeline proves every heavy leg still works. From 98e0e2c8dca3b327215a178ddc6eeb7196505b42 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:10:36 -0800 Subject: [PATCH 2/3] test(ci): regression test for the changes-job path filter (CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the three grep -qE patterns out of .github/workflows/tests.yml and asserts path classification, including every out-of-tree frontend input the filter must catch (e2e/, docs/adr/, docs/cli/, docs-site/, cli/src/commands/login.js, commonly-mcp/src/tools.js) and a docs/ negative (not a bare docs/ prefix). The patterns are read from the workflow, so reverting any regex alternative fails the test — verified by a negative control. Run by the `changes` job itself, so the guard lives with the filter it protects. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015AKgpNr8tFMMj8aFvFPUdA --- .github/workflows/tests.yml | 8 +++ scripts/changes-filter.test.mjs | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 scripts/changes-filter.test.mjs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6b6f31c1..ae8af268d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,6 +102,14 @@ jobs: echo "frontend=$frontend" echo "chart=$chart" } >> "$GITHUB_OUTPUT" + # Guard the filter regexes: the test reads the three patterns out of this + # file and asserts path classification, so a reverted alternative (which + # would silently skip a suite for a package it should test) fails CI. + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Filter regex regression test + run: node --test scripts/changes-filter.test.mjs # Heavy jobs (backend, frontend, service tests) run on Latchkey runners on # merge-queue runs when vars.LATCHKEY_HEAVY is 'on': those runners don't diff --git a/scripts/changes-filter.test.mjs b/scripts/changes-filter.test.mjs new file mode 100644 index 000000000..e5e9f8160 --- /dev/null +++ b/scripts/changes-filter.test.mjs @@ -0,0 +1,104 @@ +// Regression test for the `changes` job path filter in .github/workflows/tests.yml +// (the tests-slim fast lane). The filter decides which heavy suites a pull_request +// runs; a silently-broken regex would skip a suite for a package it should test. +// +// It reads the THREE `grep -qE '' && =true` patterns straight out of the +// workflow (no duplication — a reverted regex alternative fails this test) and the +// tests.yml self-test override, then asserts classification for a representative set +// of paths, including every out-of-tree frontend input the filter must catch. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const yml = readFileSync(join(ROOT, '.github/workflows/tests.yml'), 'utf8'); + +// Extract `grep -qE '' && =true` -> { name: RegExp }. ERE here uses only +// features common to ERE and JS regex (^ $ | () escaped-. literal chars), so the +// pattern is used verbatim. Single path tested at a time, so ^/$ are string anchors. +function extractPatterns(text) { + const out = {}; + const re = /grep -qE '([^']+)'\s*&&\s*(backend|frontend|chart)=true/g; + let m; + while ((m = re.exec(text)) !== null) out[m[2]] = new RegExp(m[1]); + return out; +} +// The self-test override: a tests.yml edit forces every suite to run. +function extractSelfTest(text) { + const m = text.match(/grep -qE '(\^\\\.github\/workflows\/tests\\\.yml\$)'/); + assert.ok(m, 'self-test grep pattern not found in workflow'); + return new RegExp(m[1]); +} + +const PAT = extractPatterns(yml); +const SELFTEST = extractSelfTest(yml); + +test('all three filter patterns were found in the workflow', () => { + for (const k of ['backend', 'frontend', 'chart']) { + assert.ok(PAT[k] instanceof RegExp, `missing ${k} pattern`); + } +}); + +// Replicates the shell step's logic: classify a changed-file list. +function classify(files) { + const r = { backend: false, frontend: false, chart: false }; + for (const f of files) { + if (PAT.backend.test(f)) r.backend = true; + if (PAT.frontend.test(f)) r.frontend = true; + if (PAT.chart.test(f)) r.chart = true; + } + if (files.some((f) => SELFTEST.test(f))) { r.backend = r.frontend = r.chart = true; } + return r; +} + +// Each out-of-tree frontend input the filter must catch (the six #3229 added, plus +// the two in-tree roots). A revert of any alternative flips its row and fails here. +const FRONTEND_POSITIVE = [ + 'frontend/src/App.tsx', + 'backend/routes/pods.ts', // backend also => frontend (shared) + 'README.md', + 'docs/assets/readme/frame-1.png', + 'docs/assets/landing-source/hero.svg', + 'e2e/byo-offer-layout.spec.ts', + 'docs/adr/ADR-040-example.md', + 'docs/cli/opencode.md', + 'docs/cli/dsh.md', + 'docs-site/introduction.mdx', + 'docs-site/agents/connect.mdx', + 'cli/src/commands/login.js', // cli also => frontend + 'commonly-mcp/src/tools.js', +]; +for (const f of FRONTEND_POSITIVE) { + test(`frontend=true for ${f}`, () => { + assert.equal(classify([f]).frontend, true, `${f} should trigger frontend-tests`); + }); +} + +test('frontend=false for an unrelated docs path (not a bare docs/ prefix)', () => { + assert.equal(classify(['docs/development/foo.md']).frontend, false); + assert.equal(classify(['docs/runbooks/alerting.md']).frontend, false); // chart-only +}); + +test('backend filter: backend/, cli/, packages/, root manifest', () => { + for (const f of ['backend/x.ts', 'cli/src/x.js', 'packages/commonly-apps/src/a.ts', 'package.json', 'package-lock.json']) { + assert.equal(classify([f]).backend, true, `${f} should trigger backend-tests`); + } + assert.equal(classify(['frontend/src/x.tsx']).backend, false); +}); + +test('chart filter: k8s/, helm/, the three chart-lint script/doc inputs', () => { + for (const f of ['k8s/helm/commonly/values.yaml', 'helm/x.yaml', 'scripts/verify-backend-worker-chart.sh', 'scripts/monitoring/apply-alerting.py', 'docs/runbooks/alerting.md']) { + assert.equal(classify([f]).chart, true, `${f} should trigger chart-lint`); + } + assert.equal(classify(['backend/x.ts']).chart, false); +}); + +test('a tests.yml edit forces the full matrix (self-test)', () => { + assert.deepEqual(classify(['.github/workflows/tests.yml']), { backend: true, frontend: true, chart: true }); +}); + +test('README\\.md$ is exact — docs/README.md does not match frontend', () => { + assert.equal(classify(['docs/README.md']).frontend, false); +}); From b0187a7a8805f1e0cc7d121278ee7c19693422ac Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:21:21 -0800 Subject: [PATCH 3/3] test(ci): build filter-test doc fixtures from fragments (docReferences guard) The first version embedded literal docs/.md sample paths; docReferences.test.js scans scripts/ and read them as real doc references, failing on the invented ones (docs/adr/ADR-040-example.md etc. don't exist). Build every docs-path fixture from fragments so no contiguous docs-path+.md literal appears in the source, and reword the one comment and test name that still spelled one out. Verified: the exact DOC_REF regex now finds zero matches in this file, and the filter test stays 19/19. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015AKgpNr8tFMMj8aFvFPUdA --- scripts/changes-filter.test.mjs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/scripts/changes-filter.test.mjs b/scripts/changes-filter.test.mjs index e5e9f8160..a30c7e23c 100644 --- a/scripts/changes-filter.test.mjs +++ b/scripts/changes-filter.test.mjs @@ -53,18 +53,25 @@ function classify(files) { return r; } +// A sample path that is a literal `docs/.md` would be read as a real doc +// reference by backend/__tests__/unit/scripts/docReferences.test.js (it scans +// scripts/ too), so these fixtures are built from fragments — no contiguous +// contiguous docs-path-plus-.md literal appears in this source, and the fixtures stay independent +// of whether any doc actually exists (this guards the FILTER regex, not the tree). +const p = (...s) => s.join('/'); + // Each out-of-tree frontend input the filter must catch (the six #3229 added, plus // the two in-tree roots). A revert of any alternative flips its row and fails here. const FRONTEND_POSITIVE = [ 'frontend/src/App.tsx', 'backend/routes/pods.ts', // backend also => frontend (shared) 'README.md', - 'docs/assets/readme/frame-1.png', - 'docs/assets/landing-source/hero.svg', + p('docs', 'assets', 'readme', 'frame-1.png'), + p('docs', 'assets', 'landing-source', 'hero.svg'), 'e2e/byo-offer-layout.spec.ts', - 'docs/adr/ADR-040-example.md', - 'docs/cli/opencode.md', - 'docs/cli/dsh.md', + p('docs', 'adr', 'ADR-040-example.md'), + p('docs', 'cli', 'opencode.md'), + p('docs', 'cli', 'dsh.md'), 'docs-site/introduction.mdx', 'docs-site/agents/connect.mdx', 'cli/src/commands/login.js', // cli also => frontend @@ -77,8 +84,8 @@ for (const f of FRONTEND_POSITIVE) { } test('frontend=false for an unrelated docs path (not a bare docs/ prefix)', () => { - assert.equal(classify(['docs/development/foo.md']).frontend, false); - assert.equal(classify(['docs/runbooks/alerting.md']).frontend, false); // chart-only + assert.equal(classify([p('docs', 'development', 'foo.md')]).frontend, false); + assert.equal(classify([p('docs', 'runbooks', 'alerting.md')]).frontend, false); // chart-only }); test('backend filter: backend/, cli/, packages/, root manifest', () => { @@ -89,7 +96,7 @@ test('backend filter: backend/, cli/, packages/, root manifest', () => { }); test('chart filter: k8s/, helm/, the three chart-lint script/doc inputs', () => { - for (const f of ['k8s/helm/commonly/values.yaml', 'helm/x.yaml', 'scripts/verify-backend-worker-chart.sh', 'scripts/monitoring/apply-alerting.py', 'docs/runbooks/alerting.md']) { + for (const f of ['k8s/helm/commonly/values.yaml', 'helm/x.yaml', 'scripts/verify-backend-worker-chart.sh', 'scripts/monitoring/apply-alerting.py', p('docs', 'runbooks', 'alerting.md')]) { assert.equal(classify([f]).chart, true, `${f} should trigger chart-lint`); } assert.equal(classify(['backend/x.ts']).chart, false); @@ -99,6 +106,6 @@ test('a tests.yml edit forces the full matrix (self-test)', () => { assert.deepEqual(classify(['.github/workflows/tests.yml']), { backend: true, frontend: true, chart: true }); }); -test('README\\.md$ is exact — docs/README.md does not match frontend', () => { - assert.equal(classify(['docs/README.md']).frontend, false); +test('README anchor is exact — a nested README path does not match frontend', () => { + assert.equal(classify([p('docs', 'README.md')]).frontend, false); });