Skip to content

Commit a96fc43

Browse files
committed
feat: unify docs and cockpit workspace
1 parent e8d996c commit a96fc43

149 files changed

Lines changed: 13298 additions & 3601 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,65 @@
1-
import { cockpitManifest } from '@threadplane/cockpit-registry';
2-
import { capabilities } from './scripts/capability-registry';
31
import {
4-
buildNavigationTree,
52
capabilityModules,
6-
} from './src/lib/route-resolution';
3+
cockpitManifest,
4+
} from '@threadplane/cockpit-registry';
5+
import { capabilities } from './scripts/capability-registry';
6+
import { buildNavigationTree } from '@threadplane/cockpit-shell';
7+
import { existsSync, readFileSync } from 'node:fs';
8+
import { resolve } from 'node:path';
79

810
/**
911
* The cockpit site is assembled from three lists that nothing forced to agree:
1012
*
1113
* - `apps/cockpit/scripts/capability-registry.ts` — what serve/build/deploy know about;
1214
* - `libs/cockpit-registry` `cockpitManifest` — what the Next route can resolve;
13-
* - `capabilityModules` in `route-resolution.ts` — what supplies a page's assets.
15+
* - registry-owned `capabilityModules` — what supplies a page's assets.
1416
*
1517
* When the `runtimes` product shipped, only the first list learned about it, so
1618
* `/runtimes/core-capabilities/<topic>/overview/<lang>` threw
1719
* "No manifest entry found …" and every runtime page 500'd in production while
1820
* the whole suite stayed green. These assertions are the missing coupling.
1921
*/
2022
describe('cockpit capability wiring', () => {
21-
const manifestKey = (e: { product: string; section: string; topic: string }) =>
22-
`${e.product}/${e.section}/${e.topic}`;
23+
const resolveCockpitConfig = (fileName: string): string => {
24+
const workspaceConfigPath = resolve(
25+
process.cwd(),
26+
'apps/cockpit',
27+
fileName
28+
);
29+
return existsSync(workspaceConfigPath)
30+
? workspaceConfigPath
31+
: resolve(process.cwd(), fileName);
32+
};
33+
34+
const manifestKey = (e: {
35+
product: string;
36+
section: string;
37+
topic: string;
38+
}) => `${e.product}/${e.section}/${e.topic}`;
2339

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

2743
const unroutable = capabilities
28-
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
44+
.map(
45+
(capability) =>
46+
`${capability.product}/core-capabilities/${capability.topic}`
47+
)
2948
.filter((key) => !manifestKeys.has(key));
3049

3150
expect(unroutable).toEqual([]);
3251
});
3352

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

3958
const unwired = capabilities
40-
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
59+
.map(
60+
(capability) =>
61+
`${capability.product}/core-capabilities/${capability.topic}`
62+
)
4163
.filter((key) => !moduleKeys.has(key));
4264

4365
expect(unwired).toEqual([]);
@@ -46,7 +68,8 @@ describe('cockpit capability wiring', () => {
4668
it('points every cockpit module at a capability that still exists', () => {
4769
const capabilityKeys = new Set(
4870
capabilities.map(
49-
(capability) => `${capability.product}/core-capabilities/${capability.topic}`
71+
(capability) =>
72+
`${capability.product}/core-capabilities/${capability.topic}`
5073
)
5174
);
5275

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

6083
it('surfaces every manifest product in the navigation tree', () => {
61-
const manifestProducts = [...new Set(cockpitManifest.map((entry) => entry.product))];
84+
const manifestProducts = [
85+
...new Set(cockpitManifest.map((entry) => entry.product)),
86+
];
6287
const navigationProducts = buildNavigationTree(cockpitManifest).map(
6388
(product) => product.product
6489
);
6590

66-
expect([...manifestProducts].sort()).toEqual([...navigationProducts].sort());
91+
expect([...manifestProducts].sort()).toEqual(
92+
[...navigationProducts].sort()
93+
);
6794

6895
for (const product of buildNavigationTree(cockpitManifest)) {
6996
const entries = product.sections.flatMap((section) => section.entries);
70-
expect({ product: product.product, empty: entries.length === 0 }).toEqual({
71-
product: product.product,
72-
empty: false,
73-
});
97+
expect({ product: product.product, empty: entries.length === 0 }).toEqual(
98+
{
99+
product: product.product,
100+
empty: false,
101+
}
102+
);
74103
}
75104
});
76105

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

84-
expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual([]);
115+
expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual(
116+
[]
117+
);
118+
});
119+
120+
it('has no direct project references to capability example lanes', () => {
121+
const tsconfig = JSON.parse(
122+
readFileSync(resolveCockpitConfig('tsconfig.json'), 'utf8')
123+
) as { references?: Array<{ path: string }> };
124+
125+
expect(
126+
tsconfig.references?.filter((reference) =>
127+
reference.path.startsWith('../../cockpit/')
128+
)
129+
).toEqual([]);
130+
});
131+
132+
it('includes external capability content assets in the Cockpit build inputs', () => {
133+
const project = JSON.parse(
134+
readFileSync(resolveCockpitConfig('project.json'), 'utf8')
135+
) as {
136+
targets: { build: { inputs: string[] } };
137+
namedInputs: Record<string, string[]>;
138+
};
139+
140+
expect(project.targets.build.inputs).toEqual([
141+
'default',
142+
'deploymentConfig',
143+
'contentAssets',
144+
'^default',
145+
]);
146+
expect(project.namedInputs['contentAssets']).toEqual([
147+
'{workspaceRoot}/cockpit/**/prompts/**',
148+
'{workspaceRoot}/cockpit/**/angular/src/**',
149+
'{workspaceRoot}/cockpit/**/python/src/**',
150+
'{workspaceRoot}/cockpit/**/docs/**',
151+
'{workspaceRoot}/deployments/ag-ui-mastra/*.mjs',
152+
]);
85153
});
86154
});

apps/cockpit/cockpit-e2e-wiring.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
44
import { capabilities } from './scripts/capability-registry';
55
// @ts-expect-error — .mjs ES module without .d.ts; the e2e tsconfig uses
66
// allowJs:true but this top-level test file doesn't go through that config.
7+
// eslint-disable-next-line @nx/enforce-module-boundaries -- repo-root port registry is intentionally outside an Nx project.
78
import { portsFor } from '../../cockpit/ports.mjs';
89

910
interface E2eWiring {

apps/cockpit/e2e/control-plane.spec.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test, type Page } from '@playwright/test';
22

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

56
declare global {
67
interface Window {
@@ -108,12 +109,43 @@ test.describe('Cockpit operational control plane', () => {
108109
await expect(desktopNavigation).toBeVisible();
109110
await expect(mobileTrigger).toBeHidden();
110111
await expect(
111-
page.getByRole('button', { name: 'Runtime', exact: true })
112-
).toBeVisible();
113-
await page.getByRole('button', { name: 'Activity' }).click();
114-
await expect(
115-
page.getByRole('heading', { name: 'Activity' })
112+
desktopNavigation.getByRole('button', { name: RUN_RAIL_ITEM })
116113
).toBeVisible();
114+
if (viewport.width >= 1024) {
115+
await expect(
116+
page.getByRole('button', { name: 'Runtime', exact: true })
117+
).toBeVisible();
118+
await page.getByRole('button', { name: 'Activity' }).click();
119+
await expect(
120+
page.getByRole('heading', { name: 'Activity' })
121+
).toBeVisible();
122+
} else {
123+
const contextTrigger = page.getByRole('button', {
124+
name: 'Open context',
125+
});
126+
await expect(contextTrigger).toBeVisible();
127+
await contextTrigger.click();
128+
const contextDialog = page.getByRole('dialog', {
129+
name: 'Cockpit control plane context',
130+
});
131+
await expect(
132+
contextDialog.getByRole('button', {
133+
name: 'Runtime',
134+
exact: true,
135+
})
136+
).toBeVisible();
137+
await page.keyboard.press('Escape');
138+
await expect(contextDialog).toBeHidden();
139+
await expect(contextTrigger).toBeFocused();
140+
141+
await desktopNavigation
142+
.getByRole('button', { name: 'Activity' })
143+
.click();
144+
await expect(contextDialog).toBeVisible();
145+
await expect(
146+
contextDialog.getByRole('heading', { name: 'Activity' })
147+
).toBeVisible();
148+
}
117149
} else {
118150
await expect(desktopNavigation).toBeHidden();
119151
await expect(mobileTrigger).toBeVisible();
@@ -131,13 +163,16 @@ test.describe('Cockpit operational control plane', () => {
131163
''
132164
);
133165
await expect(
134-
dialog.getByRole('button', { name: 'Runtime', exact: true })
166+
dialog.getByRole('button', { name: RUN_RAIL_ITEM })
135167
).toBeVisible();
136168
await dialog.getByRole('button', { name: 'Activity' }).click();
137169
await expect(
138170
dialog.getByRole('heading', { name: 'Activity' })
139171
).toBeVisible();
140172
await dialog.getByRole('button', { name: 'Close Activity' }).click();
173+
await expect(
174+
dialog.getByRole('button', { name: RUN_RAIL_ITEM })
175+
).toBeVisible();
141176
await expect(
142177
dialog.getByRole('button', { name: 'Runtime', exact: true })
143178
).toBeVisible();

apps/cockpit/e2e/production-smoke.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect, test } from '@playwright/test';
22
import { capabilities } from '../scripts/capability-registry';
3+
import {
4+
getRedirectDisabledProbePath,
5+
getRegistryWebsiteDestinations,
6+
} from '../scripts/deploy-smoke';
37

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

2429
const CHAT_CAPABILITIES = [
2530
'langgraph/streaming',
@@ -85,6 +90,20 @@ const AG_UI_TOPICS = capabilities
8590
.sort();
8691

8792
const SEND_RECEIVE_TIMEOUT_MS = 30_000;
93+
const WEBSITE_DESTINATIONS = getRegistryWebsiteDestinations();
94+
95+
test.describe('Production: registry-owned Website destinations load', () => {
96+
for (const destination of WEBSITE_DESTINATIONS) {
97+
test(`${destination} is reachable`, async ({ request }) => {
98+
const response = await request.get(
99+
new URL(destination, WEBSITE_URL).toString()
100+
);
101+
102+
expect(response.status()).toBeLessThan(400);
103+
});
104+
}
105+
});
106+
88107
test.describe('Production: Angular chat example apps load', () => {
89108
for (const cap of CHAT_CAPABILITIES) {
90109
test(`${cap} loads at examples URL`, async ({ page }) => {
@@ -176,6 +195,18 @@ test.describe('Production: cockpit shell loads', () => {
176195

177196
expect(response.status()).toBeLessThan(400);
178197
});
198+
199+
test('legacy workspace redirects remain disabled before opt-in activation', async ({
200+
request,
201+
}) => {
202+
const response = await request.get(
203+
new URL(getRedirectDisabledProbePath(), COCKPIT_URL).toString(),
204+
{ maxRedirects: 0 }
205+
);
206+
207+
expect(response.status()).toBe(200);
208+
expect(response.headers()['location']).toBeUndefined();
209+
});
179210
});
180211

181212
test.describe('Production: canonical demo sends runtime telemetry', () => {

apps/cockpit/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
"private": true,
55
"dependencies": {
66
"@radix-ui/react-slot": "^1.1.0",
7-
"@radix-ui/react-tabs": "^1.1.0",
7+
"@threadplane/workspace-react": "*",
88
"class-variance-authority": "^0.7.0",
99
"clsx": "^2.1.1",
10-
"marked": "^15.0.0",
1110
"next": "~16.1.6",
1211
"posthog-js": "^1.372.6",
1312
"react": "^19.0.0",

apps/cockpit/project.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"inputs": [
3131
"default",
3232
"deploymentConfig",
33+
"contentAssets",
3334
"^default"
3435
]
3536
},
@@ -58,6 +59,10 @@
5859
"configFile": "apps/cockpit/vite.config.mts"
5960
}
6061
},
62+
"lint": {
63+
"executor": "@nx/eslint:lint",
64+
"outputs": ["{options.outputFile}"]
65+
},
6166
"e2e": {
6267
"executor": "@nx/playwright:playwright",
6368
"options": {
@@ -164,6 +169,13 @@
164169
}
165170
},
166171
"namedInputs": {
172+
"contentAssets": [
173+
"{workspaceRoot}/cockpit/**/prompts/**",
174+
"{workspaceRoot}/cockpit/**/angular/src/**",
175+
"{workspaceRoot}/cockpit/**/python/src/**",
176+
"{workspaceRoot}/cockpit/**/docs/**",
177+
"{workspaceRoot}/deployments/ag-ui-mastra/*.mjs"
178+
],
167179
"deploymentConfig": [
168180
"{workspaceRoot}/vercel.cockpit.json",
169181
"{workspaceRoot}/vercel.examples.json",

0 commit comments

Comments
 (0)