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
6 changes: 6 additions & 0 deletions .changeset/fix-standalone-hook-worker-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"agent-bundle": patch
---

Keep standalone event-hook worker URLs runtime-relative so generated wrappers
compile without Rspack attempting to bundle the separately emitted worker.
2 changes: 1 addition & 1 deletion packages/agent-bundle/src/adapters/hook-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ const eventRouteHookWrapperSource = (
...(standalone
? [
'const renderStandalone = async (invocation, signal) => {',
' const worker = new Worker(new URL("./hooks-flight.mjs", import.meta.url), { stderr: true, stdout: true });',
' const worker = new Worker(new URL(/* webpackIgnore: true */ "./hooks-flight.mjs", import.meta.url), { stderr: true, stdout: true });',
" worker.stdout?.on('data', (chunk) => process.stderr.write(chunk));",
" worker.stderr?.on('data', (chunk) => process.stderr.write(chunk));",
' let sequence = 0;',
Expand Down
8 changes: 5 additions & 3 deletions packages/agent-bundle/tests/examples-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ it('serves the routed Audiobook Curator artifact through a real MCP client', { r
const compiled = await build({ output, root, targets: ['claude'] });
await rm(join(root, 'src'), { force: true, recursive: true });
const server = compiled.model.mcpServers.find((candidate) => candidate.name === 'curator');
expect(server?.generatedRoutes).toHaveLength(17);
expect(server?.generatedRoutes).toHaveLength(18);
const entry = join(output, 'claude', server!.args![0]!);
client = new Client({ name: 'audiobook-route-contract', version: '1.0.0' });
await client.connect(new StdioClientTransport({ args: [entry], command: process.execPath, stderr: 'pipe' }));
Expand All @@ -314,11 +314,13 @@ it('serves the routed Audiobook Curator artifact through a real MCP client', { r
expect(tools.tools.map((tool) => tool.name)).toContain('inspect_sources');
const inspectResult = await client.callTool({ arguments: { root }, name: 'inspect_sources' });
expect(inspectResult).toMatchObject({
content: [{ type: 'text' }],
content: expect.arrayContaining([expect.objectContaining({ type: 'text' })]),
structuredContent: { operation: 'inspect', root },
});
await expect(client.listResources()).resolves.toMatchObject({
resources: [expect.objectContaining({ uri: 'audiobook-curator://catalog' })],
resources: expect.arrayContaining([
expect.objectContaining({ uri: 'audiobook-curator://catalog' }),
]),
});
await expect(client.readResource({ uri: 'audiobook-curator://catalog' })).resolves.toMatchObject({
contents: [expect.objectContaining({ mimeType: 'application/json', uri: 'audiobook-curator://catalog' })],
Expand Down
62 changes: 25 additions & 37 deletions packages/agent-bundle/tests/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { nativeHookWrapperSource, type TargetHookWrapper } from '../src/adapters
import { build } from './support/build.ts';
import { runNodeScript } from './support/run-node-script.ts';
import { writeHookIndex } from '../src/build/emit.ts';
import { generatedMetaModulePath, metaModuleSpecifier } from '../src/build/meta.ts';
import { compileHooks } from '../src/build/entries.ts';
import { generatedMetaModulePath, metaModuleSpecifier, projectMeta } from '../src/build/meta.ts';
import { buildWithRslib } from '../src/build/rslib.ts';
import type { AgentBundleMeta } from '../src/meta.ts';
import { HookService, isHookSimulationCancellation } from '../src/services/hook-service.ts';
Expand Down Expand Up @@ -1035,10 +1036,13 @@ it('runs the embedded Codex and Claude native codecs through their published wra
}, 15_000);

it('runs the Cursor workspace/open lifecycle starter through a generated wrapper with empty stdout', async () => {
const root = await mkdtemp(join(tmpdir(), 'agent-bundle-cursor-workspace-open-'));
const buildRoot = join(process.cwd(), 'examples', 'audiobook-curator');
const fixtureParent = join(buildRoot, 'node_modules', '.cache');
await mkdir(fixtureParent, { recursive: true });
const root = await mkdtemp(join(fixtureParent, 'agent-bundle-cursor-workspace-open-'));
const sourceRoot = join(root, 'src', 'events', 'workspace');
const packageRoot = join(root, 'node_modules', 'agent-bundle');
const wrapper = join(root, 'event-route-workspace-open.mjs');
const outputRoot = join(root, 'dist', 'cursor');
const wrapper = join(outputRoot, 'hooks', 'event-route-workspace-open.mjs');
const base = hookModel(root);
const model: NormalizedPlugin = {
...base,
Expand All @@ -1060,46 +1064,30 @@ it('runs the Cursor workspace/open lifecycle starter through a generated wrapper
};

try {
await Promise.all([
mkdir(sourceRoot, { recursive: true }),
mkdir(packageRoot, { recursive: true }),
]);
await Promise.all([
writeFile(join(root, 'agent-bundle.config.ts'), 'export default {};\n'),
writeFile(join(root, 'package.json'), '{"type":"module"}\n'),
writeFile(join(packageRoot, 'package.json'), JSON.stringify({
exports: {
'./event-ipc': './event-ipc.mjs',
'./event-project': './event-project.mjs',
},
type: 'module',
})),
writeFile(
join(packageRoot, 'event-ipc.mjs'),
`export * from ${JSON.stringify(pathToFileURL(join(process.cwd(), 'packages/agent-bundle/dist/event-ipc.js')).href)};\n`,
),
writeFile(
join(packageRoot, 'event-project.mjs'),
`export * from ${JSON.stringify(pathToFileURL(join(process.cwd(), 'packages/agent-bundle/dist/event-project.js')).href)};\n`,
),
writeFile(join(sourceRoot, 'open.mjs'), [
`import { Agent } from ${JSON.stringify(pathToFileURL(join(process.cwd(), 'packages/rsc-runtime/dist/index.js')).href)};`,
`import { createElement } from ${JSON.stringify(pathToFileURL(join(process.cwd(), 'packages/agent-bundle/node_modules/react/index.js')).href)};`,
'',
'export default async function WorkspaceOpen() {',
' return createElement(Agent.Result);',
'}',
'',
].join('\n')),
]);
await mkdir(sourceRoot, { recursive: true });
await writeFile(join(sourceRoot, 'open.mjs'), [
"import { Agent } from '@agent-bundle/runtime';",
"import { createElement } from 'react';",
'',
'export default async function WorkspaceOpen() {',
' return createElement(Agent.Result);',
'}',
'',
].join('\n'));
const targetRegistry = createDefaultRegistry();
const plan = targetRegistry.get('cursor').plan(model);
const generated = plan.hookEntries?.find((entry) => entry.relativePath === 'hooks/event-route-workspace-open.mjs');
const starter = targetRegistry.hookContract('cursor')?.nativeEventStarter?.('workspace/open');

expect(plan.diagnostics).toEqual([]);
expect(generated).toBeDefined();
await writeFile(wrapper, generated!.virtualSource);
await compileHooks(plan.hookEntries ?? [], {
artifactEpoch: 'cursor-workspace-open-test',
cwd: buildRoot,
meta: projectMeta(model.metadata),
outDir: outputRoot,
plugin: { name: model.metadata.name, version: model.metadata.version },
});
expect(starter).toEqual({
cursor_version: 'lifecycle-replay',
hook_event_name: 'workspaceOpen',
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/tests/target-hook-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ it('plans a thin epoch-bound event-route client and keeps standalone execution e
eventRoute: { event: 'tool/after', fallback: 'standalone', runtime: 'shared' },
};
const degradedSource = planHooks(planningModel([degraded]), 'synthetic', contract).hookEntries[0]!.virtualSource;
expect(degradedSource).toContain('new URL("./hooks-flight.mjs", import.meta.url)');
expect(degradedSource).toContain('new URL(/* webpackIgnore: true */ "./hooks-flight.mjs", import.meta.url)');
expect(degradedSource).toContain('createAgentRenderDispatcher');
expect(degradedSource).toContain('projectEventDocument');
expect(degradedSource).toContain('error.code === "runtime-unavailable"');
Expand Down
Loading