From 78e800e51bf880786db836ae86f56feba7f5e119 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 13:41:28 -0700 Subject: [PATCH] ci: wire the two dead cockpit spec files into targets CI actually runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cockpit/deep-agents/footprint.spec.ts` and `libs/cockpit-docs/src/lib/docs-bundle.spec.ts` were executed by nothing: no vitest project included the first, and `cockpit-docs` had no `test` target at all. Both are wired in here, following #916 (give the orphan a target) and #918 (glob out-of-project cockpit specs into `nx test cockpit`). Wiring surfaced three further gaps, all fixed: - `cockpit-docs` and `cockpit-registry` were both unreachable. `nx test` does not walk `^test`, and the `library` job runs a hardcoded LIBS list that excludes them, so cockpit-registry's three specs had never run either. The cockpit job now uses `nx run-many` over all three projects. - The footprint specs resolved paths from `process.cwd()`, which under `nx test cockpit` is `apps/cockpit`. Left as-is they would have asserted against `apps/cockpit/cockpit/...` and passed vacuously. They now resolve from `import.meta.url`. - `cockpit//*.spec.ts` sits outside every project root, so `nx affected` attributes it to the untagged `root` project and a PR touching only those specs skipped the job that runs them. ci-scope now maps them onto the cockpit scope by path. The website `.mdx` assertions in all three footprint specs asserted the five-segment docs shape #918 deleted — the one docs-links.ts records as having "produced a URL that 404s for every product". They are removed, not weakened: that coupling is a table checked against the website's real content tree by apps/cockpit/src/lib/docs-links.spec.ts. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 7 ++- apps/cockpit/vite.config.mts | 7 +++ cockpit/chat/footprint.spec.ts | 43 ++++++++---------- cockpit/deep-agents/footprint.spec.ts | 64 +++++++++------------------ cockpit/render/footprint.spec.ts | 50 ++++++++++----------- libs/cockpit-docs/project.json | 6 +++ libs/cockpit-docs/vite.config.mts | 11 +++++ scripts/ci-scope.mjs | 12 +++++ scripts/ci-scope.spec.mjs | 41 +++++++++++++++++ scripts/ci-workflow.spec.mjs | 18 ++++++++ 10 files changed, 164 insertions(+), 95 deletions(-) create mode 100644 libs/cockpit-docs/vite.config.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a6d741a..22d1e3ce9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -226,7 +226,12 @@ jobs: cache: npm - run: npm ci - run: npx nx build cockpit --skip-nx-cache - - run: npx nx test cockpit --skip-nx-cache + # cockpit-docs and cockpit-registry carry `test` targets that nothing in + # CI invoked: `nx test` does not walk `^test`, and the `library` job runs + # a hardcoded LIBS list that excludes both. Name them here so their specs + # actually execute. All three share the `scope:cockpit` tag, so ci-scope + # already gates this job correctly for changes under either library. + - run: npx nx run-many -t test --projects=cockpit,cockpit-docs,cockpit-registry --skip-nx-cache cockpit-examples-build: name: Cockpit — build all examples diff --git a/apps/cockpit/vite.config.mts b/apps/cockpit/vite.config.mts index 2bef33cf5..d91a2f2c5 100644 --- a/apps/cockpit/vite.config.mts +++ b/apps/cockpit/vite.config.mts @@ -25,6 +25,13 @@ export default defineConfig({ // docsPath assertion drifted into asserting a URL shape the website has // never served. Run them here so `nx test cockpit` covers them. '../../cockpit/*/matrix.spec.ts', + // Same story for the per-product footprint specs (chat, deep-agents, + // render): they sit outside any project root, so no `test` target owned + // them and the deep-agents one drifted into asserting a website docs + // library that does not exist. Glob the whole family rather than naming + // files, so a new `cockpit//footprint.spec.ts` is covered the + // day it lands instead of joining the unrun pile. + '../../cockpit/*/footprint.spec.ts', ], setupFiles: ['./test-setup.ts'], }, diff --git a/cockpit/chat/footprint.spec.ts b/cockpit/chat/footprint.spec.ts index d35fa568b..a49386711 100644 --- a/cockpit/chat/footprint.spec.ts +++ b/cockpit/chat/footprint.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const topicNames = [ @@ -15,41 +16,33 @@ const topicNames = [ 'theming', ] as const; -const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const; - -const chatRoot = path.join(process.cwd(), 'cockpit', 'chat'); -const websiteDocsRoot = path.join( - process.cwd(), 'apps', 'website', 'content', 'docs', 'chat' -); - +// Resolve from this file, not process.cwd(). These specs run under +// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently +// points at apps/cockpit/cockpit/... and turns every existence assertion into +// a vacuous pass (or an unrelated ENOENT). +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const chatRoot = path.join(repoRoot, 'cockpit', 'chat'); + +// The per-topic website .mdx assertions this spec used to carry asserted the +// five-segment docs shape #918 removed (see +// libs/cockpit-registry/src/lib/docs-links.ts: it "produced a URL that 404s for +// every product"). The website tree is organised by guide, not by cockpit +// topic. That coupling is now a table checked against the website's real +// content tree by apps/cockpit/src/lib/docs-links.spec.ts. describe('Chat footprint', () => { - it('keeps the getting-started overview in place', () => { - expect( - fs.existsSync( - path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx') - ) - ).toBe(true); - }); - - it('creates the approved topic modules and docs pages', () => { + it('creates the approved topic modules', () => { for (const topic of topicNames) { const moduleRoot = path.join(chatRoot, topic, 'python'); const projectJsonPath = path.join(chatRoot, topic, 'angular', 'project.json'); expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true); - expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true); + expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe( + true + ); expect(fs.existsSync(projectJsonPath)).toBe(true); const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands'); - - for (const page of pageNames) { - expect( - fs.existsSync( - path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`) - ) - ).toBe(true); - } } }); diff --git a/cockpit/deep-agents/footprint.spec.ts b/cockpit/deep-agents/footprint.spec.ts index bdca60932..fc6b51f85 100644 --- a/cockpit/deep-agents/footprint.spec.ts +++ b/cockpit/deep-agents/footprint.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const topicNames = [ @@ -10,34 +11,24 @@ const topicNames = [ 'skills', ] as const; -const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const; +// Resolve from this file, not process.cwd(). These specs run under +// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently +// points at apps/cockpit/cockpit/... and turns every existence assertion into +// a vacuous pass (or an unrelated ENOENT). +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const deepAgentsRoot = path.join(repoRoot, 'cockpit', 'deep-agents'); -const deepAgentsRoot = path.join(process.cwd(), 'cockpit', 'deep-agents'); -const websiteDocsRoot = path.join( - process.cwd(), - 'apps', - 'website', - 'content', - 'docs', - 'deep-agents' -); - -describe('Deep Agents Phase 5 footprint', () => { - it('keeps the getting-started overview in place', () => { - expect( - fs.existsSync( - path.join( - websiteDocsRoot, - 'getting-started', - 'overview', - 'python', - 'overview.mdx' - ) - ) - ).toBe(true); - }); - - it('creates the approved topic modules and docs pages', () => { +// This spec used to also assert a website docs page per topic at +// content/docs/deep-agents/core-capabilities//python/.mdx. That +// five-segment shape is the formula #918 removed: as +// libs/cockpit-registry/src/lib/docs-links.ts records, it "produced a URL that +// 404s for every product", and no `deep-agents` docs library exists on the +// website at all. The real website coupling now lives in the docs-links table +// and is checked against the website's actual content tree and nav config by +// apps/cockpit/src/lib/docs-links.spec.ts. What is left here is what +// "footprint" actually means: the cockpit modules exist and are runnable. +describe('Deep Agents footprint', () => { + it('creates the approved topic modules', () => { for (const topic of topicNames) { const moduleRoot = path.join(deepAgentsRoot, topic, 'python'); const projectJson = JSON.parse( @@ -45,28 +36,15 @@ describe('Deep Agents Phase 5 footprint', () => { ); expect(fs.existsSync(path.join(moduleRoot, 'package.json'))).toBe(true); - expect(fs.existsSync(path.join(moduleRoot, 'project.json'))).toBe(true); expect(fs.existsSync(path.join(moduleRoot, 'tsconfig.json'))).toBe(true); expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true); - expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true); + expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe( + true + ); expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands'); expect(projectJson.targets?.smoke?.options?.command).toContain( 'src/index.ts' ); - - for (const page of pageNames) { - expect( - fs.existsSync( - path.join( - websiteDocsRoot, - 'core-capabilities', - topic, - 'python', - `${page}.mdx` - ) - ) - ).toBe(true); - } } }); }); diff --git a/cockpit/render/footprint.spec.ts b/cockpit/render/footprint.spec.ts index 5a7bcac97..a05faf397 100644 --- a/cockpit/render/footprint.spec.ts +++ b/cockpit/render/footprint.spec.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const topicNames = [ @@ -11,41 +12,38 @@ const topicNames = [ 'computed-functions', ] as const; -const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const; - -const renderRoot = path.join(process.cwd(), 'cockpit', 'render'); -const websiteDocsRoot = path.join( - process.cwd(), 'apps', 'website', 'content', 'docs', 'render' -); - +// Resolve from this file, not process.cwd(). These specs run under +// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently +// points at apps/cockpit/cockpit/... and turns every existence assertion into +// a vacuous pass (or an unrelated ENOENT). +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const renderRoot = path.join(repoRoot, 'cockpit', 'render'); + +// The per-topic website .mdx assertions this spec used to carry asserted the +// five-segment docs shape #918 removed (see +// libs/cockpit-registry/src/lib/docs-links.ts: it "produced a URL that 404s for +// every product"). The website tree is organised by guide, not by cockpit +// topic. That coupling is now a table checked against the website's real +// content tree by apps/cockpit/src/lib/docs-links.spec.ts. describe('Render footprint', () => { - it('keeps the getting-started overview in place', () => { - expect( - fs.existsSync( - path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx') - ) - ).toBe(true); - }); - - it('creates the approved topic modules and docs pages', () => { + it('creates the approved topic modules', () => { for (const topic of topicNames) { const moduleRoot = path.join(renderRoot, topic, 'python'); - const projectJsonPath = path.join(renderRoot, topic, 'angular', 'project.json'); + const projectJsonPath = path.join( + renderRoot, + topic, + 'angular', + 'project.json' + ); expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true); - expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true); + expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe( + true + ); expect(fs.existsSync(projectJsonPath)).toBe(true); const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands'); - - for (const page of pageNames) { - expect( - fs.existsSync( - path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`) - ) - ).toBe(true); - } } }); }); diff --git a/libs/cockpit-docs/project.json b/libs/cockpit-docs/project.json index 67feff423..23d60ae53 100644 --- a/libs/cockpit-docs/project.json +++ b/libs/cockpit-docs/project.json @@ -17,6 +17,12 @@ "main": "libs/cockpit-docs/src/index.ts", "tsConfig": "libs/cockpit-docs/tsconfig.lib.json" } + }, + "test": { + "executor": "@nx/vitest:test", + "options": { + "configFile": "libs/cockpit-docs/vite.config.mts" + } } }, "tags": [ diff --git a/libs/cockpit-docs/vite.config.mts b/libs/cockpit-docs/vite.config.mts new file mode 100644 index 000000000..6edeadec8 --- /dev/null +++ b/libs/cockpit-docs/vite.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + plugins: [nxViteTsPaths()], + test: { + environment: 'node', + globals: true, + include: ['src/**/*.spec.ts'], + }, +}); diff --git a/scripts/ci-scope.mjs b/scripts/ci-scope.mjs index 946de51da..29306692c 100644 --- a/scripts/ci-scope.mjs +++ b/scripts/ci-scope.mjs @@ -69,6 +69,15 @@ const LINT_ONLY_FILES = new Set(['eslint.config.mjs']); * when a LINT_ONLY_FILES entry changes. */ const LINT_SCOPE_KEYS = ['library', 'cockpit', 'website', 'examples_chat']; +/** The per-product `matrix.spec.ts` / `footprint.spec.ts` files sit at + * cockpit//, which is outside every project root. `nx affected` + * therefore attributes them to the `root` project, and `root` carries no + * `scope:` tag — so a PR touching only these specs produced an empty scope + * and skipped the very job that runs them. They execute under + * `nx test cockpit` (see apps/cockpit/vite.config.mts), so map them onto the + * cockpit scope by path. */ +const COCKPIT_ROOTLESS_SPEC = /^cockpit\/[^/]+\/[^/]+\.spec\.ts$/; + export function emptyScope() { return Object.fromEntries(SCOPE_KEYS.map((k) => [k, false])); } @@ -125,6 +134,9 @@ export function classifyFromAffected(changedFiles, affectedProjects) { if (changedFiles.some((f) => LINT_ONLY_FILES.has(f))) { for (const key of LINT_SCOPE_KEYS) scope[key] = true; } + if (changedFiles.some((f) => COCKPIT_ROOTLESS_SPEC.test(f))) { + scope.cockpit = true; + } if (isAngularCompatibilityChange(changedFiles)) { scope.angular_compatibility = true; } diff --git a/scripts/ci-scope.spec.mjs b/scripts/ci-scope.spec.mjs index de8ecfa85..c6f056b73 100644 --- a/scripts/ci-scope.spec.mjs +++ b/scripts/ci-scope.spec.mjs @@ -125,6 +125,47 @@ describe('classifyFromAffected — lint-only files', () => { }); }); +describe('classifyFromAffected — rootless cockpit specs', () => { + // These specs live at cockpit// — outside every project root — so + // `nx affected` reports only the untagged `root` project for them. Without + // the path rule they produced an empty scope and skipped the cockpit job + // that actually runs them. + for (const file of [ + 'cockpit/deep-agents/footprint.spec.ts', + 'cockpit/chat/footprint.spec.ts', + 'cockpit/render/footprint.spec.ts', + 'cockpit/chat/matrix.spec.ts', + 'cockpit/langgraph/matrix.spec.ts', + ]) { + it(`${file} flips the cockpit scope even when nx reports only \`root\``, () => { + const scope = classifyFromAffected( + [file], + [{ name: 'root', tags: ['npm:private'] }] + ); + assert.equal(scope.cockpit, true); + }); + } + + it('does not flip cockpit for specs that already live inside a project root', () => { + const scope = classifyFromAffected( + ['cockpit/chat/messages/angular/e2e/c-messages.spec.ts'], + [{ name: 'root', tags: ['npm:private'] }] + ); + assert.equal(scope.cockpit, false); + }); + + it('leaves the e2e / smoke / deploy scopes alone', () => { + const scope = classifyFromAffected( + ['cockpit/deep-agents/footprint.spec.ts'], + [{ name: 'root', tags: ['npm:private'] }] + ); + assert.equal(scope.cockpit_e2e, false); + assert.equal(scope.cockpit_smoke, false); + assert.equal(scope.cockpit_deploy_smoke, false); + assert.equal(scope.cockpit_examples, false); + }); +}); + describe('classifyFromAffected — publishable lib broadcast', () => { it('publishable lib triggers its existing scopes plus angular compatibility', () => { const scope = classifyFromAffected( diff --git a/scripts/ci-workflow.spec.mjs b/scripts/ci-workflow.spec.mjs index fc9cce118..d4a9ecabb 100644 --- a/scripts/ci-workflow.spec.mjs +++ b/scripts/ci-workflow.spec.mjs @@ -368,6 +368,24 @@ describe('CI workflow', () => { } }); + it('runs the cockpit sibling libraries that own vitest specs', async () => { + // `nx test cockpit` does not walk `^test`, and the `library` job runs a + // hardcoded LIBS list that excludes both of these. If they are dropped + // from this run-many their specs stop executing silently. + const cockpitJob = readJobBlock(await readWorkflow(), 'cockpit'); + const runMany = cockpitJob.match(/npx nx run-many -t test --projects=(\S+)/); + + assert.ok(runMany, 'cockpit job should run tests via nx run-many'); + + const projects = runMany[1].split(','); + for (const project of ['cockpit', 'cockpit-docs', 'cockpit-registry']) { + assert.ok( + projects.includes(project), + `cockpit job should run \`nx test ${project}\`` + ); + } + }); + it('lets the cockpit e2e summary inspect CI scope outputs', async () => { const cockpitE2eSummaryJob = await readCockpitE2eSummaryJob();