built-scope-carrier.e2e.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * Built-artifact guard for the scope carrier shared by `dsh-subagent` and
  3. * `dsh-jsonrpc`. The carrier registry is module-local, so both bundles must
  4. * externalize `dsh-scope`; source-mode tests cannot expose an accidentally
  5. * inlined second registry. This test runs the real `lib/index.js` bundles in a
  6. * plain Node subprocess, disposes the child before settlement, and requires the
  7. * SDK completion notification to retain the delegating parent.
  8. */
  9. import { execFile } from 'node:child_process'
  10. import { existsSync } from 'node:fs'
  11. import { fileURLToPath } from 'node:url'
  12. import { promisify } from 'node:util'
  13. import { describe, expect, it } from 'vitest'
  14. const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
  15. const jsonrpcBundle = fileURLToPath(new URL('../lib/index.js', import.meta.url))
  16. const execFileAsync = promisify(execFile)
  17. const builtRuntimeProbe = String.raw`
  18. import { mkdtemp, rm } from "node:fs/promises";
  19. import { tmpdir } from "node:os";
  20. import { join, resolve } from "node:path";
  21. import { pathToFileURL } from "node:url";
  22. const load = (path) => import(pathToFileURL(resolve(path)).href);
  23. const [
  24. { Context },
  25. agentCore,
  26. { default: SubagentService },
  27. { default: SessionPersistenceJsonl },
  28. { HarnessSdkServer },
  29. { SessionId },
  30. ] = await Promise.all([
  31. load("vendor/cordis/lib/index.js"),
  32. load("packages/examples/agent-spine-demo/lib/index.js"),
  33. load("packages/subagent/subagent/lib/index.js"),
  34. load("packages/session-persistence/session-persistence-jsonl/lib/index.js"),
  35. load("packages/ui/jsonrpc/lib/index.js"),
  36. load("packages/core/session/lib/index.js"),
  37. ]);
  38. const storageRoot = await mkdtemp(join(tmpdir(), "jsonrpc-built-scope-"));
  39. const ctx = new Context();
  40. try {
  41. await ctx.plugin(agentCore, { workspaceContext: false });
  42. await ctx.plugin(SubagentService);
  43. await ctx.plugin(SessionPersistenceJsonl, { root: storageRoot });
  44. await new Promise((ready) => setTimeout(ready, 50));
  45. const notifications = [];
  46. const server = new HarnessSdkServer(ctx, {
  47. request() { return Promise.reject(new Error("unexpected host request")); },
  48. notify(method, params) { notifications.push({ method, params }); },
  49. });
  50. const parent = await ctx.agents.create({
  51. sessionId: SessionId("built-parent"),
  52. meta: { cwd: storageRoot },
  53. agentOptions: { model: "test" },
  54. });
  55. const child = await parent.agent.ctx.agents.create({
  56. sessionId: SessionId("built-child"),
  57. meta: { cwd: storageRoot, parentSession: SessionId("built-parent") },
  58. agentOptions: { model: "test" },
  59. });
  60. const result = Promise.withResolvers();
  61. const unregister = ctx.subagents.registerProvider({
  62. name: "built-local",
  63. capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
  64. inheritsParentContext: false,
  65. start() {
  66. return Promise.resolve({
  67. id: child.agent.id,
  68. localAgent: child.agent,
  69. result: result.promise,
  70. dispose() { return Promise.resolve(); },
  71. });
  72. },
  73. });
  74. const run = await ctx.subagents.start("built-local", {
  75. parent: parent.agent,
  76. prompt: [],
  77. signal: new AbortController().signal,
  78. });
  79. await child.dispose();
  80. result.resolve({ output: [], stopReason: "completed" });
  81. await run.result;
  82. await Promise.resolve();
  83. console.log(JSON.stringify(notifications.filter(({ method }) => method === "subagent.finished")));
  84. await run.dispose();
  85. unregister();
  86. await parent.dispose();
  87. await server.shutdown();
  88. } finally {
  89. await ctx.fiber.dispose();
  90. await rm(storageRoot, { recursive: true, force: true });
  91. }
  92. `
  93. describe.skipIf(!existsSync(jsonrpcBundle))('dsh-jsonrpc BUILT scope carrier', () => {
  94. it('preserves parent-scoped completion after child disposal', async () => {
  95. const { stdout, stderr } = await execFileAsync(process.execPath, ['--input-type=module', '-e', builtRuntimeProbe], {
  96. cwd: repoRoot,
  97. timeout: 15_000,
  98. })
  99. expect(stderr).not.toContain('listener threw')
  100. expect(JSON.parse(stdout) as unknown).toEqual([{
  101. method: 'subagent.finished',
  102. params: {
  103. provider: 'built-local',
  104. agentId: 'built-child',
  105. parentSessionId: 'built-parent',
  106. childSessionId: 'built-child',
  107. status: 'ok',
  108. stopReason: 'completed',
  109. lastAssistantMessage: [],
  110. },
  111. }])
  112. })
  113. })