Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apps/cockpit/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<product>/footprint.spec.ts` is covered the
// day it lands instead of joining the unrun pile.
'../../cockpit/*/footprint.spec.ts',
],
setupFiles: ['./test-setup.ts'],
},
Expand Down
43 changes: 18 additions & 25 deletions cockpit/chat/footprint.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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);
}
}
});

Expand Down
64 changes: 21 additions & 43 deletions cockpit/deep-agents/footprint.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -10,63 +11,40 @@ 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/<topic>/python/<page>.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(
fs.readFileSync(path.join(moduleRoot, 'project.json'), 'utf8')
);

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);
}
}
});
});
50 changes: 24 additions & 26 deletions cockpit/render/footprint.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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);
}
}
});
});
6 changes: 6 additions & 0 deletions libs/cockpit-docs/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
11 changes: 11 additions & 0 deletions libs/cockpit-docs/vite.config.mts
Original file line number Diff line number Diff line change
@@ -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'],
},
});
12 changes: 12 additions & 0 deletions scripts/ci-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<product>/, 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]));
}
Expand Down Expand Up @@ -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;
}
Expand Down
41 changes: 41 additions & 0 deletions scripts/ci-scope.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,47 @@ describe('classifyFromAffected — lint-only files', () => {
});
});

describe('classifyFromAffected — rootless cockpit specs', () => {
// These specs live at cockpit/<product>/ — 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(
Expand Down
18 changes: 18 additions & 0 deletions scripts/ci-workflow.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading