Skip to content

Commit 78e800e

Browse files
bloveclaude
andcommitted
ci: wire the two dead cockpit spec files into targets CI actually runs
`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/<product>/*.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 <noreply@anthropic.com>
1 parent 44388f2 commit 78e800e

10 files changed

Lines changed: 164 additions & 95 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,12 @@ jobs:
226226
cache: npm
227227
- run: npm ci
228228
- run: npx nx build cockpit --skip-nx-cache
229-
- run: npx nx test cockpit --skip-nx-cache
229+
# cockpit-docs and cockpit-registry carry `test` targets that nothing in
230+
# CI invoked: `nx test` does not walk `^test`, and the `library` job runs
231+
# a hardcoded LIBS list that excludes both. Name them here so their specs
232+
# actually execute. All three share the `scope:cockpit` tag, so ci-scope
233+
# already gates this job correctly for changes under either library.
234+
- run: npx nx run-many -t test --projects=cockpit,cockpit-docs,cockpit-registry --skip-nx-cache
230235

231236
cockpit-examples-build:
232237
name: Cockpit — build all examples

apps/cockpit/vite.config.mts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export default defineConfig({
2525
// docsPath assertion drifted into asserting a URL shape the website has
2626
// never served. Run them here so `nx test cockpit` covers them.
2727
'../../cockpit/*/matrix.spec.ts',
28+
// Same story for the per-product footprint specs (chat, deep-agents,
29+
// render): they sit outside any project root, so no `test` target owned
30+
// them and the deep-agents one drifted into asserting a website docs
31+
// library that does not exist. Glob the whole family rather than naming
32+
// files, so a new `cockpit/<product>/footprint.spec.ts` is covered the
33+
// day it lands instead of joining the unrun pile.
34+
'../../cockpit/*/footprint.spec.ts',
2835
],
2936
setupFiles: ['./test-setup.ts'],
3037
},

cockpit/chat/footprint.spec.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
34
import { describe, expect, it } from 'vitest';
45

56
const topicNames = [
@@ -15,41 +16,33 @@ const topicNames = [
1516
'theming',
1617
] as const;
1718

18-
const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const;
19-
20-
const chatRoot = path.join(process.cwd(), 'cockpit', 'chat');
21-
const websiteDocsRoot = path.join(
22-
process.cwd(), 'apps', 'website', 'content', 'docs', 'chat'
23-
);
24-
19+
// Resolve from this file, not process.cwd(). These specs run under
20+
// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently
21+
// points at apps/cockpit/cockpit/... and turns every existence assertion into
22+
// a vacuous pass (or an unrelated ENOENT).
23+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
24+
const chatRoot = path.join(repoRoot, 'cockpit', 'chat');
25+
26+
// The per-topic website .mdx assertions this spec used to carry asserted the
27+
// five-segment docs shape #918 removed (see
28+
// libs/cockpit-registry/src/lib/docs-links.ts: it "produced a URL that 404s for
29+
// every product"). The website tree is organised by guide, not by cockpit
30+
// topic. That coupling is now a table checked against the website's real
31+
// content tree by apps/cockpit/src/lib/docs-links.spec.ts.
2532
describe('Chat footprint', () => {
26-
it('keeps the getting-started overview in place', () => {
27-
expect(
28-
fs.existsSync(
29-
path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx')
30-
)
31-
).toBe(true);
32-
});
33-
34-
it('creates the approved topic modules and docs pages', () => {
33+
it('creates the approved topic modules', () => {
3534
for (const topic of topicNames) {
3635
const moduleRoot = path.join(chatRoot, topic, 'python');
3736
const projectJsonPath = path.join(chatRoot, topic, 'angular', 'project.json');
3837

3938
expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true);
40-
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true);
39+
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(
40+
true
41+
);
4142
expect(fs.existsSync(projectJsonPath)).toBe(true);
4243

4344
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8'));
4445
expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands');
45-
46-
for (const page of pageNames) {
47-
expect(
48-
fs.existsSync(
49-
path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`)
50-
)
51-
).toBe(true);
52-
}
5346
}
5447
});
5548

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
34
import { describe, expect, it } from 'vitest';
45

56
const topicNames = [
@@ -10,63 +11,40 @@ const topicNames = [
1011
'skills',
1112
] as const;
1213

13-
const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const;
14+
// Resolve from this file, not process.cwd(). These specs run under
15+
// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently
16+
// points at apps/cockpit/cockpit/... and turns every existence assertion into
17+
// a vacuous pass (or an unrelated ENOENT).
18+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
19+
const deepAgentsRoot = path.join(repoRoot, 'cockpit', 'deep-agents');
1420

15-
const deepAgentsRoot = path.join(process.cwd(), 'cockpit', 'deep-agents');
16-
const websiteDocsRoot = path.join(
17-
process.cwd(),
18-
'apps',
19-
'website',
20-
'content',
21-
'docs',
22-
'deep-agents'
23-
);
24-
25-
describe('Deep Agents Phase 5 footprint', () => {
26-
it('keeps the getting-started overview in place', () => {
27-
expect(
28-
fs.existsSync(
29-
path.join(
30-
websiteDocsRoot,
31-
'getting-started',
32-
'overview',
33-
'python',
34-
'overview.mdx'
35-
)
36-
)
37-
).toBe(true);
38-
});
39-
40-
it('creates the approved topic modules and docs pages', () => {
21+
// This spec used to also assert a website docs page per topic at
22+
// content/docs/deep-agents/core-capabilities/<topic>/python/<page>.mdx. That
23+
// five-segment shape is the formula #918 removed: as
24+
// libs/cockpit-registry/src/lib/docs-links.ts records, it "produced a URL that
25+
// 404s for every product", and no `deep-agents` docs library exists on the
26+
// website at all. The real website coupling now lives in the docs-links table
27+
// and is checked against the website's actual content tree and nav config by
28+
// apps/cockpit/src/lib/docs-links.spec.ts. What is left here is what
29+
// "footprint" actually means: the cockpit modules exist and are runnable.
30+
describe('Deep Agents footprint', () => {
31+
it('creates the approved topic modules', () => {
4132
for (const topic of topicNames) {
4233
const moduleRoot = path.join(deepAgentsRoot, topic, 'python');
4334
const projectJson = JSON.parse(
4435
fs.readFileSync(path.join(moduleRoot, 'project.json'), 'utf8')
4536
);
4637

4738
expect(fs.existsSync(path.join(moduleRoot, 'package.json'))).toBe(true);
48-
expect(fs.existsSync(path.join(moduleRoot, 'project.json'))).toBe(true);
4939
expect(fs.existsSync(path.join(moduleRoot, 'tsconfig.json'))).toBe(true);
5040
expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true);
51-
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true);
41+
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(
42+
true
43+
);
5244
expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands');
5345
expect(projectJson.targets?.smoke?.options?.command).toContain(
5446
'src/index.ts'
5547
);
56-
57-
for (const page of pageNames) {
58-
expect(
59-
fs.existsSync(
60-
path.join(
61-
websiteDocsRoot,
62-
'core-capabilities',
63-
topic,
64-
'python',
65-
`${page}.mdx`
66-
)
67-
)
68-
).toBe(true);
69-
}
7048
}
7149
});
7250
});

cockpit/render/footprint.spec.ts

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
34
import { describe, expect, it } from 'vitest';
45

56
const topicNames = [
@@ -11,41 +12,38 @@ const topicNames = [
1112
'computed-functions',
1213
] as const;
1314

14-
const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const;
15-
16-
const renderRoot = path.join(process.cwd(), 'cockpit', 'render');
17-
const websiteDocsRoot = path.join(
18-
process.cwd(), 'apps', 'website', 'content', 'docs', 'render'
19-
);
20-
15+
// Resolve from this file, not process.cwd(). These specs run under
16+
// `nx test cockpit`, whose cwd is apps/cockpit — a cwd-relative root silently
17+
// points at apps/cockpit/cockpit/... and turns every existence assertion into
18+
// a vacuous pass (or an unrelated ENOENT).
19+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
20+
const renderRoot = path.join(repoRoot, 'cockpit', 'render');
21+
22+
// The per-topic website .mdx assertions this spec used to carry asserted the
23+
// five-segment docs shape #918 removed (see
24+
// libs/cockpit-registry/src/lib/docs-links.ts: it "produced a URL that 404s for
25+
// every product"). The website tree is organised by guide, not by cockpit
26+
// topic. That coupling is now a table checked against the website's real
27+
// content tree by apps/cockpit/src/lib/docs-links.spec.ts.
2128
describe('Render footprint', () => {
22-
it('keeps the getting-started overview in place', () => {
23-
expect(
24-
fs.existsSync(
25-
path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx')
26-
)
27-
).toBe(true);
28-
});
29-
30-
it('creates the approved topic modules and docs pages', () => {
29+
it('creates the approved topic modules', () => {
3130
for (const topic of topicNames) {
3231
const moduleRoot = path.join(renderRoot, topic, 'python');
33-
const projectJsonPath = path.join(renderRoot, topic, 'angular', 'project.json');
32+
const projectJsonPath = path.join(
33+
renderRoot,
34+
topic,
35+
'angular',
36+
'project.json'
37+
);
3438

3539
expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true);
36-
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true);
40+
expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(
41+
true
42+
);
3743
expect(fs.existsSync(projectJsonPath)).toBe(true);
3844

3945
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8'));
4046
expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands');
41-
42-
for (const page of pageNames) {
43-
expect(
44-
fs.existsSync(
45-
path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`)
46-
)
47-
).toBe(true);
48-
}
4947
}
5048
});
5149
});

libs/cockpit-docs/project.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
"main": "libs/cockpit-docs/src/index.ts",
1818
"tsConfig": "libs/cockpit-docs/tsconfig.lib.json"
1919
}
20+
},
21+
"test": {
22+
"executor": "@nx/vitest:test",
23+
"options": {
24+
"configFile": "libs/cockpit-docs/vite.config.mts"
25+
}
2026
}
2127
},
2228
"tags": [

libs/cockpit-docs/vite.config.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from 'vite';
2+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
3+
4+
export default defineConfig({
5+
plugins: [nxViteTsPaths()],
6+
test: {
7+
environment: 'node',
8+
globals: true,
9+
include: ['src/**/*.spec.ts'],
10+
},
11+
});

scripts/ci-scope.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ const LINT_ONLY_FILES = new Set(['eslint.config.mjs']);
6969
* when a LINT_ONLY_FILES entry changes. */
7070
const LINT_SCOPE_KEYS = ['library', 'cockpit', 'website', 'examples_chat'];
7171

72+
/** The per-product `matrix.spec.ts` / `footprint.spec.ts` files sit at
73+
* cockpit/<product>/, which is outside every project root. `nx affected`
74+
* therefore attributes them to the `root` project, and `root` carries no
75+
* `scope:` tag — so a PR touching only these specs produced an empty scope
76+
* and skipped the very job that runs them. They execute under
77+
* `nx test cockpit` (see apps/cockpit/vite.config.mts), so map them onto the
78+
* cockpit scope by path. */
79+
const COCKPIT_ROOTLESS_SPEC = /^cockpit\/[^/]+\/[^/]+\.spec\.ts$/;
80+
7281
export function emptyScope() {
7382
return Object.fromEntries(SCOPE_KEYS.map((k) => [k, false]));
7483
}
@@ -125,6 +134,9 @@ export function classifyFromAffected(changedFiles, affectedProjects) {
125134
if (changedFiles.some((f) => LINT_ONLY_FILES.has(f))) {
126135
for (const key of LINT_SCOPE_KEYS) scope[key] = true;
127136
}
137+
if (changedFiles.some((f) => COCKPIT_ROOTLESS_SPEC.test(f))) {
138+
scope.cockpit = true;
139+
}
128140
if (isAngularCompatibilityChange(changedFiles)) {
129141
scope.angular_compatibility = true;
130142
}

scripts/ci-scope.spec.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,47 @@ describe('classifyFromAffected — lint-only files', () => {
125125
});
126126
});
127127

128+
describe('classifyFromAffected — rootless cockpit specs', () => {
129+
// These specs live at cockpit/<product>/ — outside every project root — so
130+
// `nx affected` reports only the untagged `root` project for them. Without
131+
// the path rule they produced an empty scope and skipped the cockpit job
132+
// that actually runs them.
133+
for (const file of [
134+
'cockpit/deep-agents/footprint.spec.ts',
135+
'cockpit/chat/footprint.spec.ts',
136+
'cockpit/render/footprint.spec.ts',
137+
'cockpit/chat/matrix.spec.ts',
138+
'cockpit/langgraph/matrix.spec.ts',
139+
]) {
140+
it(`${file} flips the cockpit scope even when nx reports only \`root\``, () => {
141+
const scope = classifyFromAffected(
142+
[file],
143+
[{ name: 'root', tags: ['npm:private'] }]
144+
);
145+
assert.equal(scope.cockpit, true);
146+
});
147+
}
148+
149+
it('does not flip cockpit for specs that already live inside a project root', () => {
150+
const scope = classifyFromAffected(
151+
['cockpit/chat/messages/angular/e2e/c-messages.spec.ts'],
152+
[{ name: 'root', tags: ['npm:private'] }]
153+
);
154+
assert.equal(scope.cockpit, false);
155+
});
156+
157+
it('leaves the e2e / smoke / deploy scopes alone', () => {
158+
const scope = classifyFromAffected(
159+
['cockpit/deep-agents/footprint.spec.ts'],
160+
[{ name: 'root', tags: ['npm:private'] }]
161+
);
162+
assert.equal(scope.cockpit_e2e, false);
163+
assert.equal(scope.cockpit_smoke, false);
164+
assert.equal(scope.cockpit_deploy_smoke, false);
165+
assert.equal(scope.cockpit_examples, false);
166+
});
167+
});
168+
128169
describe('classifyFromAffected — publishable lib broadcast', () => {
129170
it('publishable lib triggers its existing scopes plus angular compatibility', () => {
130171
const scope = classifyFromAffected(

scripts/ci-workflow.spec.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ describe('CI workflow', () => {
368368
}
369369
});
370370

371+
it('runs the cockpit sibling libraries that own vitest specs', async () => {
372+
// `nx test cockpit` does not walk `^test`, and the `library` job runs a
373+
// hardcoded LIBS list that excludes both of these. If they are dropped
374+
// from this run-many their specs stop executing silently.
375+
const cockpitJob = readJobBlock(await readWorkflow(), 'cockpit');
376+
const runMany = cockpitJob.match(/npx nx run-many -t test --projects=(\S+)/);
377+
378+
assert.ok(runMany, 'cockpit job should run tests via nx run-many');
379+
380+
const projects = runMany[1].split(',');
381+
for (const project of ['cockpit', 'cockpit-docs', 'cockpit-registry']) {
382+
assert.ok(
383+
projects.includes(project),
384+
`cockpit job should run \`nx test ${project}\``
385+
);
386+
}
387+
});
388+
371389
it('lets the cockpit e2e summary inspect CI scope outputs', async () => {
372390
const cockpitE2eSummaryJob = await readCockpitE2eSummaryJob();
373391

0 commit comments

Comments
 (0)