Skip to content

Commit b805eec

Browse files
test(dev): cover absent nested route roots (#717) (#720)
1 parent fa7430b commit b805eec

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

‎packages/agent-bundle/tests/dev-workbench.test.ts‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { expect, it } from '@rstest/core';
77

88
import { TargetRegistry } from '../src/adapters/registry.ts';
99
import type { TargetAdapter } from '../src/adapters/types.ts';
10+
import { parseArtifactManifest } from '../src/build/manifest.ts';
1011
import type { NormalizedPlugin } from '../src/core/types.ts';
1112
import { runCli } from '../src/cli.ts';
1213
import { captureCliTerminal } from './support/cli-terminal.ts';
@@ -502,6 +503,114 @@ it('normalizes a relative project root once before constructing every dev servic
502503
}
503504
}, 30_000);
504505

506+
it('publishes routes created and deleted below source roots absent at startup', async () => {
507+
const project = await createProjectFixture({
508+
config: [
509+
"import { defineConfig } from 'agent-bundle/config';",
510+
'',
511+
'export default defineConfig({',
512+
" plugin: { name: 'watcher-route-roots', version: '1.0.0' },",
513+
" targets: ['claude', 'codex'],",
514+
'});',
515+
'',
516+
].join('\n'),
517+
files: {
518+
'package.json': JSON.stringify({
519+
dependencies: {
520+
'@agent-bundle/runtime': 'workspace:*',
521+
'@modelcontextprotocol/server': '2.0.0',
522+
},
523+
name: 'watcher-route-roots',
524+
type: 'module',
525+
version: '1.0.0',
526+
}),
527+
},
528+
});
529+
const publications = Array.from({ length: 5 }, () => Promise.withResolvers<string>());
530+
const publication = (index: number): Promise<string> =>
531+
within(publications[index]!.promise, 20_000 * timeScale);
532+
let publicationCount = 0;
533+
let server: Awaited<ReturnType<typeof startDevServer>> | undefined;
534+
await symlink(join(process.cwd(), 'examples', 'audiobook-curator', 'node_modules'), join(project.root, 'node_modules'), 'dir');
535+
try {
536+
server = await startDevServer({
537+
open: false,
538+
port: 0,
539+
root: project.root,
540+
testing: {
541+
startForegroundServer: async (options) => {
542+
const subscription = options.eventHub.subscribe((event) => {
543+
if (event.type !== 'artifact.available') return;
544+
publications[publicationCount]?.resolve(event.epochId);
545+
publicationCount += 1;
546+
});
547+
await options.coordinator.start();
548+
return {
549+
close: async () => {
550+
subscription.unsubscribe();
551+
await options.coordinator.close();
552+
},
553+
url: 'http://127.0.0.1:49124',
554+
};
555+
},
556+
},
557+
});
558+
const activeManifest = async () => {
559+
const artifact = server!.status().artifact;
560+
if (artifact.state !== 'active') throw new Error('Expected an active development artifact.');
561+
return parseArtifactManifest(await readFile(artifact.activeEpoch.manifestPath, 'utf8'));
562+
};
563+
await publication(0);
564+
expect((await activeManifest()).routes).toMatchObject({ events: [], servers: [] });
565+
566+
const eventPath = join(project.root, 'src', 'events', 'session', 'start.ts');
567+
await mkdir(join(project.root, 'src', 'events', 'session'), { recursive: true });
568+
await writeFile(eventPath, [
569+
"export const config = { runtime: 'standalone' };",
570+
"export default async function SessionStart() { return { outcome: 'continue' }; }",
571+
'',
572+
].join('\n'));
573+
await publication(1);
574+
const withEvent = await activeManifest();
575+
expect(withEvent.routes.events.map(({ id }) => id)).toEqual(['event:session/start']);
576+
expect(withEvent.executables.hooks.map(({ event, host, kind }) => ({ event, host, kind }))).toEqual([
577+
{ event: 'sessionStart', host: 'claude', kind: 'event-route' },
578+
{ event: 'sessionStart', host: 'codex', kind: 'event-route' },
579+
]);
580+
581+
await rm(eventPath);
582+
await publication(2);
583+
const withoutEvent = await activeManifest();
584+
expect(withoutEvent.routes.events).toEqual([]);
585+
expect(withoutEvent.executables.hooks).toEqual([]);
586+
587+
const toolPath = join(project.root, 'src', 'mcp', 'probe', 'tools', 'status.ts');
588+
await mkdir(join(project.root, 'src', 'mcp', 'probe', 'tools'), { recursive: true });
589+
await writeFile(
590+
toolPath,
591+
'export const inputSchema = {}; export const resultSchema = {}; export default async () => undefined;\n',
592+
);
593+
await publication(3);
594+
const withTool = await activeManifest();
595+
expect(withTool.routes.servers.map(({ id, routes }) => ({ id, routes: routes.map(({ id }) => id) }))).toEqual([
596+
{ id: 'mcp:probe', routes: ['tool:probe/status'] },
597+
]);
598+
expect(withTool.executables.mcpServers).toEqual([
599+
expect.objectContaining({ hosts: ['claude', 'codex'], name: 'probe' }),
600+
]);
601+
602+
await rm(toolPath);
603+
await publication(4);
604+
const withoutTool = await activeManifest();
605+
expect(withoutTool.routes.servers).toEqual([]);
606+
expect(withoutTool.executables.mcpServers).toEqual([]);
607+
} finally {
608+
await server?.close().catch(() => undefined);
609+
await removeProjectFixture(project.root);
610+
}
611+
expect(publicationCount).toBe(5);
612+
}, 120_000);
613+
505614
it('latches a runtime declaration added to an ordinary Workbench session as restart-required', async () => {
506615
const project = await createProjectFixture();
507616
const assetsRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-workbench-runtime-topology-'));

0 commit comments

Comments
 (0)