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
86 changes: 86 additions & 0 deletions apps/cockpit/cockpit-capability-wiring.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { cockpitManifest } from '@threadplane/cockpit-registry';
import { capabilities } from './scripts/capability-registry';
import {
buildNavigationTree,
capabilityModules,
} from './src/lib/route-resolution';

/**
* 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.
*
* 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}`;

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}`)
.filter((key) => !manifestKeys.has(key));

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

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

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

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

it('points every cockpit module at a capability that still exists', () => {
const capabilityKeys = new Set(
capabilities.map(
(capability) => `${capability.product}/core-capabilities/${capability.topic}`
)
);

const orphans = capabilityModules
.map((module) => manifestKey(module.manifestIdentity))
.filter((key) => !capabilityKeys.has(key));

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

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

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,
});
}
});

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 registryProducts = [...new Set(capabilities.map((c) => c.product))];

expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual([]);
});
});
3 changes: 2 additions & 1 deletion apps/cockpit/src/lib/navigation-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ export const PRODUCT_LABELS: Record<string, string> = {
'langgraph': 'LangGraph',
'render': 'Render',
'chat': 'Chat',
'runtimes': 'Runtimes',
};

export function stripProductPrefix(title: string): string {
const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat '];
const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat ', 'Runtimes '];
for (const p of prefixes) {
if (title.startsWith(p)) return title.slice(p.length);
}
Expand Down
86 changes: 71 additions & 15 deletions apps/cockpit/src/lib/route-resolution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,78 @@ describe('buildNavigationTree', () => {
it('groups manifest entries by product and section', () => {
const tree = buildNavigationTree(cockpitManifest);

expect(tree).toHaveLength(5);
expect(tree[0]).toMatchObject({
product: 'deep-agents',
});
expect(tree[1]).toMatchObject({
product: 'langgraph',
});
expect(tree[2]).toMatchObject({
product: 'ag-ui',
});
expect(tree[3]).toMatchObject({
product: 'render',
});
expect(tree[4]).toMatchObject({
product: 'chat',
expect(tree.map((product) => product.product)).toEqual([
'deep-agents',
'langgraph',
'ag-ui',
'render',
'chat',
'runtimes',
]);
});

it('lists every runtimes topic under core-capabilities', () => {
const runtimes = buildNavigationTree(cockpitManifest).find(
(product) => product.product === 'runtimes'
);
const coreCapabilities = runtimes?.sections.find(
(section) => section.section === 'core-capabilities'
);

expect(coreCapabilities?.entries.map((entry) => entry.topic)).toEqual([
'microsoft-agent-framework',
'aws-strands',
'mastra',
]);
});
});

describe('runtimes capability presentation', () => {
const resolveRuntime = (topic: string, language: 'python' | 'typescript' = 'python') =>
resolveCockpitEntry({
manifest: cockpitManifest,
product: 'runtimes',
section: 'core-capabilities',
topic,
page: 'overview',
language,
});

it('resolves each runtime topic instead of throwing', () => {
for (const topic of ['microsoft-agent-framework', 'aws-strands', 'mastra']) {
expect(resolveRuntime(topic)).toMatchObject({
product: 'runtimes',
topic,
entryKind: 'capability',
});
}
});

it('serves Python-lane runtimes from their registered module assets', () => {
const presentation = getCapabilityPresentation(resolveRuntime('aws-strands'));

expect(presentation.kind).toBe('capability');
if (presentation.kind !== 'capability') return;
expect(presentation.runtimeUrl).toBe('runtimes/aws-strands');
expect(presentation.backendAssetPaths).toContain(
'cockpit/runtimes/aws-strands/python/src/agent.py'
);
});

it('falls back to the Angular-lane module for a runtime with no Python lane', () => {
const presentation = getCapabilityPresentation(resolveRuntime('mastra'));

expect(presentation.kind).toBe('capability');
if (presentation.kind !== 'capability') return;
// The manifest entry's language is 'python' (the canonical URL lane) but
// Mastra's only descriptor is the Angular one — the lookup must still find
// it rather than falling through to non-existent cockpit/runtimes/mastra/python paths.
expect(presentation.codeAssetPaths).toContain(
'cockpit/runtimes/mastra/angular/src/app/mastra.component.ts'
);
expect(presentation.promptAssetPaths).toEqual([
'cockpit/runtimes/mastra/angular/prompts/mastra.md',
]);
});
});

Expand Down
71 changes: 61 additions & 10 deletions apps/cockpit/src/lib/route-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { agUiToolViewsPythonModule } from '../../../../cockpit/ag-ui/tool-views/
import { agUiJsonRenderPythonModule } from '../../../../cockpit/ag-ui/json-render/python/src/index';
import { agUiClientToolsPythonModule } from '../../../../cockpit/ag-ui/client-tools/python/src/index';
import { agUiA2uiPythonModule } from '../../../../cockpit/ag-ui/a2ui/python/src/index';
import { agUiSubagentsPythonModule } from '../../../../cockpit/ag-ui/subagents/python/src/index';
import { deepAgentsMemoryPythonModule } from '../../../../cockpit/deep-agents/memory/python/src/index';
import { deepAgentsPlanningPythonModule } from '../../../../cockpit/deep-agents/planning/python/src/index';
import { deepAgentsFilesystemPythonModule } from '../../../../cockpit/deep-agents/filesystem/python/src/index';
Expand All @@ -41,6 +42,11 @@ import { chatGenerativeUiPythonModule } from '../../../../cockpit/chat/generativ
import { chatDebugPythonModule } from '../../../../cockpit/chat/debug/python/src/index';
import { chatThemingPythonModule } from '../../../../cockpit/chat/theming/python/src/index';
import { chatA2uiPythonModule } from '../../../../cockpit/chat/a2ui/python/src/index';
import { runtimesMicrosoftAgentFrameworkPythonModule } from '../../../../cockpit/runtimes/microsoft-agent-framework/python/src/index';
import { runtimesAwsStrandsPythonModule } from '../../../../cockpit/runtimes/aws-strands/python/src/index';
// Mastra has no Python lane — its backend is the Node AG-UI service
// deployments/ag-ui-mastra — so its descriptor lives beside the Angular app.
import { runtimesMastraAngularModule } from '../../../../cockpit/runtimes/mastra/angular/src/index';

export interface ResolveCockpitEntryOptions {
manifest: CockpitManifestEntry[];
Expand Down Expand Up @@ -79,7 +85,33 @@ export type CapabilityPresentation =
devPort?: number;
};

const capabilityModules = [
/**
* Shape a `cockpit/**\/src/index.ts` descriptor must satisfy to be wired into
* the cockpit. Each example declares its own structural copy of this interface
* (standalone-examples rule), so the fields diverge: the Angular lane carries
* no backend/docs assets. Declaring the element type here keeps the registry
* heterogeneous without widening every reader to a union.
*/
export interface RegisteredCapabilityModule {
id: string;
manifestIdentity: {
product: string;
section: string;
topic: string;
page: string;
language: string;
};
title: string;
docsPath: string;
promptAssetPaths: string[];
codeAssetPaths: string[];
backendAssetPaths?: string[];
docsAssetPaths?: string[];
runtimeUrl?: string;
devPort?: number;
}

export const capabilityModules: RegisteredCapabilityModule[] = [
langgraphStreamingPythonModule,
langgraphPersistencePythonModule,
langgraphInterruptsPythonModule,
Expand All @@ -95,6 +127,7 @@ const capabilityModules = [
agUiJsonRenderPythonModule,
agUiClientToolsPythonModule,
agUiA2uiPythonModule,
agUiSubagentsPythonModule,
deepAgentsMemoryPythonModule,
deepAgentsPlanningPythonModule,
deepAgentsFilesystemPythonModule,
Expand All @@ -118,6 +151,9 @@ const capabilityModules = [
chatDebugPythonModule,
chatThemingPythonModule,
chatA2uiPythonModule,
runtimesMicrosoftAgentFrameworkPythonModule,
runtimesAwsStrandsPythonModule,
runtimesMastraAngularModule,
];

export const toCockpitPath = (entry: CockpitManifestEntry): string =>
Expand Down Expand Up @@ -183,7 +219,14 @@ export const resolveCockpitEntry = ({
export const buildNavigationTree = (
manifest: CockpitManifestEntry[]
): NavigationProduct[] => {
const products: CockpitManifestEntry['product'][] = ['deep-agents', 'langgraph', 'ag-ui', 'render', 'chat'];
const products: CockpitManifestEntry['product'][] = [
'deep-agents',
'langgraph',
'ag-ui',
'render',
'chat',
'runtimes',
];
const sections: CockpitManifestEntry['section'][] = [
'getting-started',
'core-capabilities',
Expand Down Expand Up @@ -221,14 +264,22 @@ export const getCapabilityPresentation = (
};
}

const module = capabilityModules.find(
(candidate) =>
candidate.manifestIdentity.product === entry.product &&
candidate.manifestIdentity.section === entry.section &&
candidate.manifestIdentity.topic === entry.topic &&
candidate.manifestIdentity.page === entry.page &&
candidate.manifestIdentity.language === entry.language
);
const matchesIdentity = (candidate: RegisteredCapabilityModule): boolean =>
candidate.manifestIdentity.product === entry.product &&
candidate.manifestIdentity.section === entry.section &&
candidate.manifestIdentity.topic === entry.topic &&
candidate.manifestIdentity.page === entry.page;

// Prefer the module whose lane matches the requested language. Fall back to
// the topic's only module when no lane matches: a topic with no Python lane
// (runtimes/mastra) still resolves to its real assets instead of silently
// falling through to the manifest's generic, non-existent Python paths.
const module =
capabilityModules.find(
(candidate) =>
matchesIdentity(candidate) &&
candidate.manifestIdentity.language === entry.language
) ?? capabilityModules.find(matchesIdentity);

return {
kind: 'capability',
Expand Down
3 changes: 2 additions & 1 deletion libs/cockpit-registry/src/lib/manifest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('cockpitManifest', () => {
'ag-ui/getting-started/overview',
'render/getting-started/overview',
'chat/getting-started/overview',
'runtimes/getting-started/overview',
]);
});

Expand All @@ -105,7 +106,7 @@ describe('cockpitManifest', () => {
(entry) => entry.entryKind === 'capability'
);

expect(capabilityEntries).toHaveLength(36);
expect(capabilityEntries).toHaveLength(42);

for (const entry of capabilityEntries) {
expect(entry.supportedLanguages).toEqual(['python']);
Expand Down
Loading
Loading