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
106 changes: 87 additions & 19 deletions apps/cockpit/cockpit-capability-wiring.spec.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
import { cockpitManifest } from '@threadplane/cockpit-registry';
import { capabilities } from './scripts/capability-registry';
import {
buildNavigationTree,
capabilityModules,
} from './src/lib/route-resolution';
cockpitManifest,
} from '@threadplane/cockpit-registry';
import { capabilities } from './scripts/capability-registry';
import { buildNavigationTree } from '@threadplane/cockpit-shell';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';

/**
* The cockpit site is assembled from three lists that nothing forced to agree:
*
* - `apps/cockpit/scripts/capability-registry.ts` — what serve/build/deploy know about;
* - `libs/cockpit-registry` `cockpitManifest` — what the Next route can resolve;
* - `capabilityModules` in `route-resolution.ts` — what supplies a page's assets.
* - registry-owned `capabilityModules` — what supplies a page's assets.
*
* When the `runtimes` product shipped, only the first list learned about it, so
* `/runtimes/core-capabilities/<topic>/overview/<lang>` threw
* "No manifest entry found …" and every runtime page 500'd in production while
* the whole suite stayed green. These assertions are the missing coupling.
*/
describe('cockpit capability wiring', () => {
const manifestKey = (e: { product: string; section: string; topic: string }) =>
`${e.product}/${e.section}/${e.topic}`;
const resolveCockpitConfig = (fileName: string): string => {
const workspaceConfigPath = resolve(
process.cwd(),
'apps/cockpit',
fileName
);
return existsSync(workspaceConfigPath)
? workspaceConfigPath
: resolve(process.cwd(), fileName);
};

const manifestKey = (e: {
product: string;
section: string;
topic: string;
}) => `${e.product}/${e.section}/${e.topic}`;

it('gives every registered capability a resolvable manifest entry', () => {
const manifestKeys = new Set(cockpitManifest.map(manifestKey));

const unroutable = capabilities
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
.map(
(capability) =>
`${capability.product}/core-capabilities/${capability.topic}`
)
.filter((key) => !manifestKeys.has(key));

expect(unroutable).toEqual([]);
});

it('gives every registered capability a cockpit module in route-resolution', () => {
it('gives every registered capability a registry-owned content descriptor', () => {
const moduleKeys = new Set(
capabilityModules.map((module) => manifestKey(module.manifestIdentity))
);

const unwired = capabilities
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
.map(
(capability) =>
`${capability.product}/core-capabilities/${capability.topic}`
)
.filter((key) => !moduleKeys.has(key));

expect(unwired).toEqual([]);
Expand All @@ -46,7 +68,8 @@ describe('cockpit capability wiring', () => {
it('points every cockpit module at a capability that still exists', () => {
const capabilityKeys = new Set(
capabilities.map(
(capability) => `${capability.product}/core-capabilities/${capability.topic}`
(capability) =>
`${capability.product}/core-capabilities/${capability.topic}`
)
);

Expand All @@ -58,29 +81,74 @@ describe('cockpit capability wiring', () => {
});

it('surfaces every manifest product in the navigation tree', () => {
const manifestProducts = [...new Set(cockpitManifest.map((entry) => entry.product))];
const manifestProducts = [
...new Set(cockpitManifest.map((entry) => entry.product)),
];
const navigationProducts = buildNavigationTree(cockpitManifest).map(
(product) => product.product
);

expect([...manifestProducts].sort()).toEqual([...navigationProducts].sort());
expect([...manifestProducts].sort()).toEqual(
[...navigationProducts].sort()
);

for (const product of buildNavigationTree(cockpitManifest)) {
const entries = product.sections.flatMap((section) => section.entries);
expect({ product: product.product, empty: entries.length === 0 }).toEqual({
product: product.product,
empty: false,
});
expect({ product: product.product, empty: entries.length === 0 }).toEqual(
{
product: product.product,
empty: false,
}
);
}
});

it('keeps every registry product inside the CockpitProduct union', () => {
// `cockpitManifest` is typed `CockpitManifestEntry[]`, so a product that is
// not in the union cannot appear here — the runtime check is that the
// registry's products are all representable in the manifest.
const manifestProducts = new Set<string>(cockpitManifest.map((entry) => entry.product));
const manifestProducts = new Set<string>(
cockpitManifest.map((entry) => entry.product)
);
const registryProducts = [...new Set(capabilities.map((c) => c.product))];

expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual([]);
expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual(
[]
);
});

it('has no direct project references to capability example lanes', () => {
const tsconfig = JSON.parse(
readFileSync(resolveCockpitConfig('tsconfig.json'), 'utf8')
) as { references?: Array<{ path: string }> };

expect(
tsconfig.references?.filter((reference) =>
reference.path.startsWith('../../cockpit/')
)
).toEqual([]);
});

it('includes external capability content assets in the Cockpit build inputs', () => {
const project = JSON.parse(
readFileSync(resolveCockpitConfig('project.json'), 'utf8')
) as {
targets: { build: { inputs: string[] } };
namedInputs: Record<string, string[]>;
};

expect(project.targets.build.inputs).toEqual([
'default',
'deploymentConfig',
'contentAssets',
'^default',
]);
expect(project.namedInputs['contentAssets']).toEqual([
'{workspaceRoot}/cockpit/**/prompts/**',
'{workspaceRoot}/cockpit/**/angular/src/**',
'{workspaceRoot}/cockpit/**/python/src/**',
'{workspaceRoot}/cockpit/**/docs/**',
'{workspaceRoot}/deployments/ag-ui-mastra/*.mjs',
]);
});
});
1 change: 1 addition & 0 deletions apps/cockpit/cockpit-e2e-wiring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import { capabilities } from './scripts/capability-registry';
// @ts-expect-error — .mjs ES module without .d.ts; the e2e tsconfig uses
// allowJs:true but this top-level test file doesn't go through that config.
// eslint-disable-next-line @nx/enforce-module-boundaries -- repo-root port registry is intentionally outside an Nx project.
import { portsFor } from '../../cockpit/ports.mjs';

interface E2eWiring {
Expand Down
47 changes: 41 additions & 6 deletions apps/cockpit/e2e/control-plane.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test, type Page } from '@playwright/test';

const route = '/langgraph/core-capabilities/streaming/overview/python';
const RUN_RAIL_ITEM = /^Run(?:,|$)/;

declare global {
interface Window {
Expand Down Expand Up @@ -108,12 +109,43 @@ test.describe('Cockpit operational control plane', () => {
await expect(desktopNavigation).toBeVisible();
await expect(mobileTrigger).toBeHidden();
await expect(
page.getByRole('button', { name: 'Runtime', exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Activity' }).click();
await expect(
page.getByRole('heading', { name: 'Activity' })
desktopNavigation.getByRole('button', { name: RUN_RAIL_ITEM })
).toBeVisible();
if (viewport.width >= 1024) {
await expect(
page.getByRole('button', { name: 'Runtime', exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Activity' }).click();
await expect(
page.getByRole('heading', { name: 'Activity' })
).toBeVisible();
} else {
const contextTrigger = page.getByRole('button', {
name: 'Open context',
});
await expect(contextTrigger).toBeVisible();
await contextTrigger.click();
const contextDialog = page.getByRole('dialog', {
name: 'Cockpit control plane context',
});
await expect(
contextDialog.getByRole('button', {
name: 'Runtime',
exact: true,
})
).toBeVisible();
await page.keyboard.press('Escape');
await expect(contextDialog).toBeHidden();
await expect(contextTrigger).toBeFocused();

await desktopNavigation
.getByRole('button', { name: 'Activity' })
.click();
await expect(contextDialog).toBeVisible();
await expect(
contextDialog.getByRole('heading', { name: 'Activity' })
).toBeVisible();
}
} else {
await expect(desktopNavigation).toBeHidden();
await expect(mobileTrigger).toBeVisible();
Expand All @@ -131,13 +163,16 @@ test.describe('Cockpit operational control plane', () => {
''
);
await expect(
dialog.getByRole('button', { name: 'Runtime', exact: true })
dialog.getByRole('button', { name: RUN_RAIL_ITEM })
).toBeVisible();
await dialog.getByRole('button', { name: 'Activity' }).click();
await expect(
dialog.getByRole('heading', { name: 'Activity' })
).toBeVisible();
await dialog.getByRole('button', { name: 'Close Activity' }).click();
await expect(
dialog.getByRole('button', { name: RUN_RAIL_ITEM })
).toBeVisible();
await expect(
dialog.getByRole('button', { name: 'Runtime', exact: true })
).toBeVisible();
Expand Down
31 changes: 31 additions & 0 deletions apps/cockpit/e2e/production-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from '@playwright/test';
import { capabilities } from '../scripts/capability-registry';
import {
getRedirectDisabledProbePath,
getRegistryWebsiteDestinations,
} from '../scripts/deploy-smoke';

/**
* Production smoke test: verifies the deployed cockpit shell and deployed
Expand All @@ -20,6 +24,7 @@ const COCKPIT_URL = process.env['BASE_URL'] ?? 'https://cockpit.threadplane.ai';
const EXAMPLES_URL =
process.env['EXAMPLES_URL'] ?? 'https://examples.threadplane.ai';
const DEMO_URL = process.env['DEMO_URL'] ?? 'https://demo.threadplane.ai';
const WEBSITE_URL = process.env['WEBSITE_URL'] ?? 'https://threadplane.ai';

const CHAT_CAPABILITIES = [
'langgraph/streaming',
Expand Down Expand Up @@ -85,6 +90,20 @@ const AG_UI_TOPICS = capabilities
.sort();

const SEND_RECEIVE_TIMEOUT_MS = 30_000;
const WEBSITE_DESTINATIONS = getRegistryWebsiteDestinations();

test.describe('Production: registry-owned Website destinations load', () => {
for (const destination of WEBSITE_DESTINATIONS) {
test(`${destination} is reachable`, async ({ request }) => {
const response = await request.get(
new URL(destination, WEBSITE_URL).toString()
);

expect(response.status()).toBeLessThan(400);
});
}
});

test.describe('Production: Angular chat example apps load', () => {
for (const cap of CHAT_CAPABILITIES) {
test(`${cap} loads at examples URL`, async ({ page }) => {
Expand Down Expand Up @@ -176,6 +195,18 @@ test.describe('Production: cockpit shell loads', () => {

expect(response.status()).toBeLessThan(400);
});

test('legacy workspace redirects remain disabled before opt-in activation', async ({
request,
}) => {
const response = await request.get(
new URL(getRedirectDisabledProbePath(), COCKPIT_URL).toString(),
{ maxRedirects: 0 }
);

expect(response.status()).toBe(200);
expect(response.headers()['location']).toBeUndefined();
});
});

test.describe('Production: canonical demo sends runtime telemetry', () => {
Expand Down
3 changes: 1 addition & 2 deletions apps/cockpit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
"private": true,
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@threadplane/workspace-react": "*",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"marked": "^15.0.0",
"next": "~16.1.6",
"posthog-js": "^1.372.6",
"react": "^19.0.0",
Expand Down
12 changes: 12 additions & 0 deletions apps/cockpit/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"inputs": [
"default",
"deploymentConfig",
"contentAssets",
"^default"
]
},
Expand Down Expand Up @@ -58,6 +59,10 @@
"configFile": "apps/cockpit/vite.config.mts"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"e2e": {
"executor": "@nx/playwright:playwright",
"options": {
Expand Down Expand Up @@ -164,6 +169,13 @@
}
},
"namedInputs": {
"contentAssets": [
"{workspaceRoot}/cockpit/**/prompts/**",
"{workspaceRoot}/cockpit/**/angular/src/**",
"{workspaceRoot}/cockpit/**/python/src/**",
"{workspaceRoot}/cockpit/**/docs/**",
"{workspaceRoot}/deployments/ag-ui-mastra/*.mjs"
],
"deploymentConfig": [
"{workspaceRoot}/vercel.cockpit.json",
"{workspaceRoot}/vercel.examples.json",
Expand Down
Loading
Loading