@ai-devkit/agent-manager 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +14 -0
  2. package/dist/__tests__/adapters/GrokCliAdapter.test.js +403 -0
  3. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -0
  4. package/dist/__tests__/print/ClaudeCliProbe.test.js +53 -0
  5. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -0
  6. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +69 -0
  7. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -0
  8. package/dist/__tests__/print/ClaudePrintAgentService.test.js +108 -0
  9. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -0
  10. package/dist/__tests__/print/ClaudePrintRunner.test.js +187 -0
  11. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -0
  12. package/dist/__tests__/print/PrintAgent.test.js +17 -0
  13. package/dist/__tests__/print/PrintAgent.test.js.map +1 -0
  14. package/dist/__tests__/print/PrintAgentStore.test.js +307 -0
  15. package/dist/__tests__/print/PrintAgentStore.test.js.map +1 -0
  16. package/dist/__tests__/terminal/TmuxManager.test.js +9 -0
  17. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -1
  18. package/dist/__tests__/utils/agents.test.js +6 -0
  19. package/dist/__tests__/utils/agents.test.js.map +1 -1
  20. package/dist/adapters/AgentAdapter.d.ts +1 -1
  21. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  22. package/dist/adapters/AgentAdapter.js.map +1 -1
  23. package/dist/adapters/GrokCliAdapter.d.ts +79 -0
  24. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -0
  25. package/dist/adapters/GrokCliAdapter.js +306 -0
  26. package/dist/adapters/GrokCliAdapter.js.map +1 -0
  27. package/dist/adapters/index.d.ts +1 -0
  28. package/dist/adapters/index.d.ts.map +1 -1
  29. package/dist/adapters/index.js +1 -0
  30. package/dist/adapters/index.js.map +1 -1
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/print/ClaudeCliProbe.d.ts +20 -0
  36. package/dist/print/ClaudeCliProbe.d.ts.map +1 -0
  37. package/dist/print/ClaudeCliProbe.js +57 -0
  38. package/dist/print/ClaudeCliProbe.js.map +1 -0
  39. package/dist/print/ClaudePrintAgentService.d.ts +44 -0
  40. package/dist/print/ClaudePrintAgentService.d.ts.map +1 -0
  41. package/dist/print/ClaudePrintAgentService.js +66 -0
  42. package/dist/print/ClaudePrintAgentService.js.map +1 -0
  43. package/dist/print/ClaudePrintRunner.d.ts +32 -0
  44. package/dist/print/ClaudePrintRunner.d.ts.map +1 -0
  45. package/dist/print/ClaudePrintRunner.js +128 -0
  46. package/dist/print/ClaudePrintRunner.js.map +1 -0
  47. package/dist/print/PrintAgent.d.ts +57 -0
  48. package/dist/print/PrintAgent.d.ts.map +1 -0
  49. package/dist/print/PrintAgent.js +42 -0
  50. package/dist/print/PrintAgent.js.map +1 -0
  51. package/dist/print/PrintAgentStore.d.ts +69 -0
  52. package/dist/print/PrintAgentStore.d.ts.map +1 -0
  53. package/dist/print/PrintAgentStore.js +484 -0
  54. package/dist/print/PrintAgentStore.js.map +1 -0
  55. package/dist/terminal/TmuxManager.d.ts +2 -2
  56. package/dist/terminal/TmuxManager.d.ts.map +1 -1
  57. package/dist/terminal/TmuxManager.js +5 -7
  58. package/dist/terminal/TmuxManager.js.map +1 -1
  59. package/dist/utils/agents.d.ts +1 -1
  60. package/dist/utils/agents.d.ts.map +1 -1
  61. package/dist/utils/agents.js +4 -0
  62. package/dist/utils/agents.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/__tests__/adapters/GrokCliAdapter.test.ts +307 -0
  65. package/src/__tests__/fixtures/fake-claude.cjs +24 -0
  66. package/src/__tests__/print/ClaudeCliProbe.test.ts +32 -0
  67. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +56 -0
  68. package/src/__tests__/print/ClaudePrintAgentService.test.ts +46 -0
  69. package/src/__tests__/print/ClaudePrintRunner.test.ts +105 -0
  70. package/src/__tests__/print/PrintAgent.test.ts +21 -0
  71. package/src/__tests__/print/PrintAgentStore.test.ts +192 -0
  72. package/src/__tests__/terminal/TmuxManager.test.ts +10 -0
  73. package/src/__tests__/utils/agents.test.ts +7 -0
  74. package/src/adapters/AgentAdapter.ts +1 -1
  75. package/src/adapters/GrokCliAdapter.ts +394 -0
  76. package/src/adapters/index.ts +1 -0
  77. package/src/index.ts +40 -0
  78. package/src/print/ClaudeCliProbe.ts +58 -0
  79. package/src/print/ClaudePrintAgentService.ts +94 -0
  80. package/src/print/ClaudePrintRunner.ts +139 -0
  81. package/src/print/PrintAgent.ts +86 -0
  82. package/src/print/PrintAgentStore.ts +503 -0
  83. package/src/terminal/TmuxManager.ts +5 -7
  84. package/src/utils/agents.ts +2 -1
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ describe('ClaudePrintAgentService', ()=>{
3
+ it('validates before create and does not run Claude', async ()=>{
4
+ const api = await import('../../index.js');
5
+ expect(api).toHaveProperty('ClaudePrintAgentService');
6
+ const probe = {
7
+ validate: vi.fn().mockResolvedValue({
8
+ executable: 'claude',
9
+ version: '2.1.220'
10
+ })
11
+ };
12
+ const store = {
13
+ create: vi.fn().mockResolvedValue({
14
+ id: 'agent-id',
15
+ name: 'reviewer'
16
+ })
17
+ };
18
+ const runner = {
19
+ run: vi.fn()
20
+ };
21
+ const Service = api.ClaudePrintAgentService;
22
+ await expect(new Service({
23
+ store,
24
+ probe,
25
+ runner
26
+ }).create({
27
+ name: 'reviewer',
28
+ cwd: '/project'
29
+ })).resolves.toMatchObject({
30
+ id: 'agent-id'
31
+ });
32
+ expect(probe.validate).toHaveBeenCalledOnce();
33
+ expect(store.create).toHaveBeenCalledWith({
34
+ name: 'reviewer',
35
+ cwd: '/project'
36
+ });
37
+ expect(runner.run).not.toHaveBeenCalled();
38
+ });
39
+ it('runs first and resumed sends and records provider identity/results', async ()=>{
40
+ const api = await import('../../index.js');
41
+ const base = {
42
+ id: 'id',
43
+ name: 'reviewer',
44
+ providerSessionId: 'session',
45
+ sessionHealth: 'uninitialized'
46
+ };
47
+ const store = {
48
+ resolve: vi.fn().mockResolvedValue(base),
49
+ acquireRun: vi.fn().mockResolvedValueOnce({
50
+ agent: base,
51
+ token: 'one'
52
+ }).mockResolvedValueOnce({
53
+ agent: {
54
+ ...base,
55
+ sessionHealth: 'healthy'
56
+ },
57
+ token: 'two'
58
+ }),
59
+ recordProviderProcess: vi.fn(),
60
+ completeRun: vi.fn().mockResolvedValue({})
61
+ };
62
+ const runner = {
63
+ run: vi.fn().mockImplementation(async (request)=>{
64
+ await request.onSpawn({
65
+ pid: 42,
66
+ startedAt: 'start'
67
+ });
68
+ return {
69
+ sessionId: 'session',
70
+ result: 'answer',
71
+ exitCode: 0
72
+ };
73
+ })
74
+ };
75
+ const Service = api.ClaudePrintAgentService;
76
+ const service = new Service({
77
+ store,
78
+ probe: {
79
+ validate: vi.fn()
80
+ },
81
+ runner,
82
+ executable: 'fake-claude'
83
+ });
84
+ await service.send('reviewer', 'first');
85
+ await service.send('id', 'later');
86
+ expect(runner.run.mock.calls[0][0]).toMatchObject({
87
+ prompt: 'first',
88
+ firstRun: true,
89
+ executable: 'fake-claude'
90
+ });
91
+ expect(runner.run.mock.calls[1][0]).toMatchObject({
92
+ prompt: 'later',
93
+ firstRun: false,
94
+ executable: 'fake-claude'
95
+ });
96
+ expect(store.recordProviderProcess).toHaveBeenCalledWith('id', 'one', {
97
+ pid: 42,
98
+ startedAt: 'start'
99
+ });
100
+ expect(store.completeRun).toHaveBeenCalledWith('id', 'one', expect.objectContaining({
101
+ status: 'succeeded',
102
+ exitCode: 0,
103
+ sessionHealth: 'healthy'
104
+ }));
105
+ });
106
+ });
107
+
108
+ //# sourceMappingURL=ClaudePrintAgentService.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/print/ClaudePrintAgentService.test.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest';\n\ndescribe('ClaudePrintAgentService', () => {\n it('validates before create and does not run Claude', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n expect(api).toHaveProperty('ClaudePrintAgentService');\n const probe = { validate: vi.fn().mockResolvedValue({ executable: 'claude', version: '2.1.220' }) };\n const store = { create: vi.fn().mockResolvedValue({ id: 'agent-id', name: 'reviewer' }) };\n const runner = { run: vi.fn() };\n const Service = api.ClaudePrintAgentService as new (options: unknown) => any;\n\n await expect(new Service({ store, probe, runner }).create({ name: 'reviewer', cwd: '/project' }))\n .resolves.toMatchObject({ id: 'agent-id' });\n expect(probe.validate).toHaveBeenCalledOnce();\n expect(store.create).toHaveBeenCalledWith({ name: 'reviewer', cwd: '/project' });\n expect(runner.run).not.toHaveBeenCalled();\n });\n\n it('runs first and resumed sends and records provider identity/results', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n const base = { id: 'id', name: 'reviewer', providerSessionId: 'session', sessionHealth: 'uninitialized' };\n const store = {\n resolve: vi.fn().mockResolvedValue(base),\n acquireRun: vi.fn()\n .mockResolvedValueOnce({ agent: base, token: 'one' })\n .mockResolvedValueOnce({ agent: { ...base, sessionHealth: 'healthy' }, token: 'two' }),\n recordProviderProcess: vi.fn(), completeRun: vi.fn().mockResolvedValue({}),\n };\n const runner = { run: vi.fn().mockImplementation(async (request) => {\n await request.onSpawn({ pid: 42, startedAt: 'start' });\n return { sessionId: 'session', result: 'answer', exitCode: 0 };\n }) };\n const Service = api.ClaudePrintAgentService as new (options: unknown) => any;\n const service = new Service({ store, probe: { validate: vi.fn() }, runner, executable: 'fake-claude' });\n\n await service.send('reviewer', 'first');\n await service.send('id', 'later');\n\n expect(runner.run.mock.calls[0][0]).toMatchObject({ prompt: 'first', firstRun: true, executable: 'fake-claude' });\n expect(runner.run.mock.calls[1][0]).toMatchObject({ prompt: 'later', firstRun: false, executable: 'fake-claude' });\n expect(store.recordProviderProcess).toHaveBeenCalledWith('id', 'one', { pid: 42, startedAt: 'start' });\n expect(store.completeRun).toHaveBeenCalledWith('id', 'one', expect.objectContaining({\n status: 'succeeded', exitCode: 0, sessionHealth: 'healthy',\n }));\n });\n});\n"],"names":["describe","expect","it","vi","api","toHaveProperty","probe","validate","fn","mockResolvedValue","executable","version","store","create","id","name","runner","run","Service","ClaudePrintAgentService","cwd","resolves","toMatchObject","toHaveBeenCalledOnce","toHaveBeenCalledWith","not","toHaveBeenCalled","base","providerSessionId","sessionHealth","resolve","acquireRun","mockResolvedValueOnce","agent","token","recordProviderProcess","completeRun","mockImplementation","request","onSpawn","pid","startedAt","sessionId","result","exitCode","service","send","mock","calls","prompt","firstRun","objectContaining","status"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAElDH,SAAS,2BAA2B;IAChCE,GAAG,mDAAmD;QAClD,MAAME,MAAM,MAAM,MAAM,CAAC;QACzBH,OAAOG,KAAKC,cAAc,CAAC;QAC3B,MAAMC,QAAQ;YAAEC,UAAUJ,GAAGK,EAAE,GAAGC,iBAAiB,CAAC;gBAAEC,YAAY;gBAAUC,SAAS;YAAU;QAAG;QAClG,MAAMC,QAAQ;YAAEC,QAAQV,GAAGK,EAAE,GAAGC,iBAAiB,CAAC;gBAAEK,IAAI;gBAAYC,MAAM;YAAW;QAAG;QACxF,MAAMC,SAAS;YAAEC,KAAKd,GAAGK,EAAE;QAAG;QAC9B,MAAMU,UAAUd,IAAIe,uBAAuB;QAE3C,MAAMlB,OAAO,IAAIiB,QAAQ;YAAEN;YAAON;YAAOU;QAAO,GAAGH,MAAM,CAAC;YAAEE,MAAM;YAAYK,KAAK;QAAW,IACzFC,QAAQ,CAACC,aAAa,CAAC;YAAER,IAAI;QAAW;QAC7Cb,OAAOK,MAAMC,QAAQ,EAAEgB,oBAAoB;QAC3CtB,OAAOW,MAAMC,MAAM,EAAEW,oBAAoB,CAAC;YAAET,MAAM;YAAYK,KAAK;QAAW;QAC9EnB,OAAOe,OAAOC,GAAG,EAAEQ,GAAG,CAACC,gBAAgB;IAC3C;IAEAxB,GAAG,sEAAsE;QACrE,MAAME,MAAM,MAAM,MAAM,CAAC;QACzB,MAAMuB,OAAO;YAAEb,IAAI;YAAMC,MAAM;YAAYa,mBAAmB;YAAWC,eAAe;QAAgB;QACxG,MAAMjB,QAAQ;YACVkB,SAAS3B,GAAGK,EAAE,GAAGC,iBAAiB,CAACkB;YACnCI,YAAY5B,GAAGK,EAAE,GACZwB,qBAAqB,CAAC;gBAAEC,OAAON;gBAAMO,OAAO;YAAM,GAClDF,qBAAqB,CAAC;gBAAEC,OAAO;oBAAE,GAAGN,IAAI;oBAAEE,eAAe;gBAAU;gBAAGK,OAAO;YAAM;YACxFC,uBAAuBhC,GAAGK,EAAE;YAAI4B,aAAajC,GAAGK,EAAE,GAAGC,iBAAiB,CAAC,CAAC;QAC5E;QACA,MAAMO,SAAS;YAAEC,KAAKd,GAAGK,EAAE,GAAG6B,kBAAkB,CAAC,OAAOC;gBACpD,MAAMA,QAAQC,OAAO,CAAC;oBAAEC,KAAK;oBAAIC,WAAW;gBAAQ;gBACpD,OAAO;oBAAEC,WAAW;oBAAWC,QAAQ;oBAAUC,UAAU;gBAAE;YACjE;QAAG;QACH,MAAM1B,UAAUd,IAAIe,uBAAuB;QAC3C,MAAM0B,UAAU,IAAI3B,QAAQ;YAAEN;YAAON,OAAO;gBAAEC,UAAUJ,GAAGK,EAAE;YAAG;YAAGQ;YAAQN,YAAY;QAAc;QAErG,MAAMmC,QAAQC,IAAI,CAAC,YAAY;QAC/B,MAAMD,QAAQC,IAAI,CAAC,MAAM;QAEzB7C,OAAOe,OAAOC,GAAG,CAAC8B,IAAI,CAACC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE1B,aAAa,CAAC;YAAE2B,QAAQ;YAASC,UAAU;YAAMxC,YAAY;QAAc;QAC/GT,OAAOe,OAAOC,GAAG,CAAC8B,IAAI,CAACC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE1B,aAAa,CAAC;YAAE2B,QAAQ;YAASC,UAAU;YAAOxC,YAAY;QAAc;QAChHT,OAAOW,MAAMuB,qBAAqB,EAAEX,oBAAoB,CAAC,MAAM,OAAO;YAAEgB,KAAK;YAAIC,WAAW;QAAQ;QACpGxC,OAAOW,MAAMwB,WAAW,EAAEZ,oBAAoB,CAAC,MAAM,OAAOvB,OAAOkD,gBAAgB,CAAC;YAChFC,QAAQ;YAAaR,UAAU;YAAGf,eAAe;QACrD;IACJ;AACJ"}
@@ -0,0 +1,187 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { PassThrough, Writable } from 'node:stream';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ function agent() {
5
+ return {
6
+ id: '11111111-1111-4111-8111-111111111111',
7
+ name: 'reviewer',
8
+ provider: 'claude',
9
+ mode: 'print',
10
+ cwd: '/project',
11
+ providerSessionId: '22222222-2222-4222-8222-222222222222',
12
+ state: 'running',
13
+ sessionHealth: 'uninitialized',
14
+ createdAt: '',
15
+ updatedAt: '',
16
+ lastActiveAt: null,
17
+ lastResult: null,
18
+ activeRun: null
19
+ };
20
+ }
21
+ function fakeSpawn(events, exitCode = 0) {
22
+ const calls = [];
23
+ const promptChunks = [];
24
+ const child = new EventEmitter();
25
+ child.pid = 4242;
26
+ child.stdout = new PassThrough();
27
+ child.stderr = new PassThrough();
28
+ child.stdin = new Writable({
29
+ write (chunk, _encoding, callback) {
30
+ promptChunks.push(Buffer.from(chunk));
31
+ callback();
32
+ },
33
+ final (callback) {
34
+ for (const event of events)child.stdout.write(`${JSON.stringify(event)}\n`);
35
+ child.stdout.end();
36
+ queueMicrotask(()=>child.emit('close', exitCode, null));
37
+ callback();
38
+ }
39
+ });
40
+ const spawn = vi.fn((...args)=>{
41
+ calls.push(args);
42
+ return child;
43
+ });
44
+ return {
45
+ spawn,
46
+ calls,
47
+ promptChunks
48
+ };
49
+ }
50
+ describe('ClaudePrintRunner', ()=>{
51
+ it('starts a caller-assigned session and persists provider identity before stdin', async ()=>{
52
+ const api = await import('../../index.js');
53
+ expect(api).toHaveProperty('ClaudePrintRunner');
54
+ const fixture = fakeSpawn([
55
+ {
56
+ type: 'system',
57
+ subtype: 'init',
58
+ session_id: agent().providerSessionId
59
+ },
60
+ {
61
+ type: 'result',
62
+ session_id: agent().providerSessionId,
63
+ result: 'done'
64
+ }
65
+ ]);
66
+ let persisted = false;
67
+ const Runner = api.ClaudePrintRunner;
68
+ const runner = new Runner({
69
+ spawn: fixture.spawn,
70
+ processInspector: {
71
+ getIdentity: ()=>({
72
+ pid: 4242,
73
+ startedAt: 'provider-start'
74
+ })
75
+ }
76
+ });
77
+ const result = await runner.run({
78
+ agent: agent(),
79
+ prompt: 'secret prompt',
80
+ executable: 'claude-test',
81
+ firstRun: true,
82
+ onSpawn: async ()=>{
83
+ expect(fixture.promptChunks).toHaveLength(0);
84
+ persisted = true;
85
+ }
86
+ });
87
+ expect(persisted).toBe(true);
88
+ expect(fixture.calls[0]).toEqual([
89
+ 'claude-test',
90
+ [
91
+ '-p',
92
+ '--session-id',
93
+ agent().providerSessionId,
94
+ '--output-format',
95
+ 'stream-json',
96
+ '--verbose'
97
+ ],
98
+ expect.objectContaining({
99
+ cwd: '/project',
100
+ shell: false,
101
+ stdio: [
102
+ 'pipe',
103
+ 'pipe',
104
+ 'pipe'
105
+ ]
106
+ })
107
+ ]);
108
+ expect(JSON.stringify(fixture.calls)).not.toContain('secret prompt');
109
+ expect(Buffer.concat(fixture.promptChunks).toString()).toBe('secret prompt');
110
+ expect(result).toEqual({
111
+ sessionId: agent().providerSessionId,
112
+ result: 'done',
113
+ exitCode: 0
114
+ });
115
+ });
116
+ it('uses exact resume and rejects a mismatched result session', async ()=>{
117
+ const api = await import('../../index.js');
118
+ const fixture = fakeSpawn([
119
+ {
120
+ type: 'result',
121
+ session_id: 'wrong',
122
+ result: 'nope'
123
+ }
124
+ ]);
125
+ const Runner = api.ClaudePrintRunner;
126
+ const runner = new Runner({
127
+ spawn: fixture.spawn,
128
+ processInspector: {
129
+ getIdentity: ()=>({
130
+ pid: 4242,
131
+ startedAt: 'provider-start'
132
+ })
133
+ }
134
+ });
135
+ await expect(runner.run({
136
+ agent: agent(),
137
+ prompt: 'followup',
138
+ executable: 'claude',
139
+ firstRun: false,
140
+ onSpawn: vi.fn()
141
+ })).rejects.toMatchObject({
142
+ code: 'CLAUDE_SESSION_MISMATCH'
143
+ });
144
+ expect(fixture.calls[0][1]).toEqual([
145
+ '-p',
146
+ '--resume',
147
+ agent().providerSessionId,
148
+ '--output-format',
149
+ 'stream-json',
150
+ '--verbose'
151
+ ]);
152
+ });
153
+ it('does not disclose provider stderr in a failed-run error', async ()=>{
154
+ const api = await import('../../index.js');
155
+ const fixture = fakeSpawn([], 1);
156
+ const Runner = api.ClaudePrintRunner;
157
+ const runner = new Runner({
158
+ spawn: fixture.spawn,
159
+ processInspector: {
160
+ getIdentity: ()=>({
161
+ pid: 4242,
162
+ startedAt: 'provider-start'
163
+ })
164
+ }
165
+ });
166
+ fixture.spawn.mockImplementationOnce((...args)=>{
167
+ const child = fakeSpawn([], 1).spawn(...args);
168
+ child.stdin = new Writable({
169
+ final (callback) {
170
+ child.stderr.write('secret prompt echoed by provider');
171
+ child.stderr.end();
172
+ queueMicrotask(()=>child.emit('close', 1, null));
173
+ callback();
174
+ }
175
+ });
176
+ return child;
177
+ });
178
+ await expect(runner.run({
179
+ agent: agent(),
180
+ prompt: 'secret prompt',
181
+ firstRun: true,
182
+ onSpawn: vi.fn()
183
+ })).rejects.not.toThrow(/secret prompt/);
184
+ });
185
+ });
186
+
187
+ //# sourceMappingURL=ClaudePrintRunner.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/print/ClaudePrintRunner.test.ts"],"sourcesContent":["import { EventEmitter } from 'node:events';\nimport { PassThrough, Writable } from 'node:stream';\nimport { describe, expect, it, vi } from 'vitest';\nimport type { PrintAgent } from '../../index.js';\n\nfunction agent(): PrintAgent {\n return {\n id: '11111111-1111-4111-8111-111111111111', name: 'reviewer', provider: 'claude', mode: 'print',\n cwd: '/project', providerSessionId: '22222222-2222-4222-8222-222222222222', state: 'running',\n sessionHealth: 'uninitialized', createdAt: '', updatedAt: '', lastActiveAt: null, lastResult: null,\n activeRun: null,\n };\n}\n\nfunction fakeSpawn(events: object[], exitCode = 0) {\n const calls: unknown[][] = [];\n const promptChunks: Buffer[] = [];\n const child = new EventEmitter() as any;\n child.pid = 4242;\n child.stdout = new PassThrough();\n child.stderr = new PassThrough();\n child.stdin = new Writable({\n write(chunk, _encoding, callback) { promptChunks.push(Buffer.from(chunk)); callback(); },\n final(callback) {\n for (const event of events) child.stdout.write(`${JSON.stringify(event)}\\n`);\n child.stdout.end();\n queueMicrotask(() => child.emit('close', exitCode, null));\n callback();\n },\n });\n const spawn = vi.fn((...args: unknown[]) => { calls.push(args); return child; });\n return { spawn, calls, promptChunks };\n}\n\ndescribe('ClaudePrintRunner', () => {\n it('starts a caller-assigned session and persists provider identity before stdin', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n expect(api).toHaveProperty('ClaudePrintRunner');\n const fixture = fakeSpawn([\n { type: 'system', subtype: 'init', session_id: agent().providerSessionId },\n { type: 'result', session_id: agent().providerSessionId, result: 'done' },\n ]);\n let persisted = false;\n const Runner = api.ClaudePrintRunner as new (options: unknown) => any;\n const runner = new Runner({ spawn: fixture.spawn, processInspector: {\n getIdentity: () => ({ pid: 4242, startedAt: 'provider-start' }),\n } });\n\n const result = await runner.run({\n agent: agent(), prompt: 'secret prompt', executable: 'claude-test', firstRun: true,\n onSpawn: async () => { expect(fixture.promptChunks).toHaveLength(0); persisted = true; },\n });\n\n expect(persisted).toBe(true);\n expect(fixture.calls[0]).toEqual([\n 'claude-test',\n ['-p', '--session-id', agent().providerSessionId, '--output-format', 'stream-json', '--verbose'],\n expect.objectContaining({ cwd: '/project', shell: false, stdio: ['pipe', 'pipe', 'pipe'] }),\n ]);\n expect(JSON.stringify(fixture.calls)).not.toContain('secret prompt');\n expect(Buffer.concat(fixture.promptChunks).toString()).toBe('secret prompt');\n expect(result).toEqual({ sessionId: agent().providerSessionId, result: 'done', exitCode: 0 });\n });\n\n it('uses exact resume and rejects a mismatched result session', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n const fixture = fakeSpawn([{ type: 'result', session_id: 'wrong', result: 'nope' }]);\n const Runner = api.ClaudePrintRunner as new (options: unknown) => any;\n const runner = new Runner({ spawn: fixture.spawn, processInspector: {\n getIdentity: () => ({ pid: 4242, startedAt: 'provider-start' }),\n } });\n\n await expect(runner.run({\n agent: agent(), prompt: 'followup', executable: 'claude', firstRun: false, onSpawn: vi.fn(),\n })).rejects.toMatchObject({ code: 'CLAUDE_SESSION_MISMATCH' });\n expect(fixture.calls[0]![1]).toEqual([\n '-p', '--resume', agent().providerSessionId, '--output-format', 'stream-json', '--verbose',\n ]);\n });\n\n it('does not disclose provider stderr in a failed-run error', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n const fixture = fakeSpawn([], 1);\n const Runner = api.ClaudePrintRunner as new (options: unknown) => any;\n const runner = new Runner({ spawn: fixture.spawn, processInspector: {\n getIdentity: () => ({ pid: 4242, startedAt: 'provider-start' }),\n } });\n fixture.spawn.mockImplementationOnce((...args: unknown[]) => {\n const child = (fakeSpawn([], 1).spawn as any)(...args);\n child.stdin = new Writable({\n final(callback) {\n child.stderr.write('secret prompt echoed by provider');\n child.stderr.end();\n queueMicrotask(() => child.emit('close', 1, null));\n callback();\n },\n });\n return child;\n });\n\n await expect(runner.run({\n agent: agent(), prompt: 'secret prompt', firstRun: true, onSpawn: vi.fn(),\n })).rejects.not.toThrow(/secret prompt/);\n });\n});\n"],"names":["EventEmitter","PassThrough","Writable","describe","expect","it","vi","agent","id","name","provider","mode","cwd","providerSessionId","state","sessionHealth","createdAt","updatedAt","lastActiveAt","lastResult","activeRun","fakeSpawn","events","exitCode","calls","promptChunks","child","pid","stdout","stderr","stdin","write","chunk","_encoding","callback","push","Buffer","from","final","event","JSON","stringify","end","queueMicrotask","emit","spawn","fn","args","api","toHaveProperty","fixture","type","subtype","session_id","result","persisted","Runner","ClaudePrintRunner","runner","processInspector","getIdentity","startedAt","run","prompt","executable","firstRun","onSpawn","toHaveLength","toBe","toEqual","objectContaining","shell","stdio","not","toContain","concat","toString","sessionId","rejects","toMatchObject","code","mockImplementationOnce","toThrow"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ describe('print-agent public domain', ()=>{
3
+ it('exports a classified busy error without exposing prompt data', async ()=>{
4
+ const api = await import('../../index.js');
5
+ expect(api).toHaveProperty('PrintAgentBusyError');
6
+ const ErrorType = api.PrintAgentBusyError;
7
+ const error = new ErrorType('agent-id', 'reviewer');
8
+ expect(error).toMatchObject({
9
+ name: 'PrintAgentBusyError',
10
+ code: 'PRINT_AGENT_BUSY',
11
+ agentId: 'agent-id',
12
+ message: 'Print agent "reviewer" is busy.'
13
+ });
14
+ });
15
+ });
16
+
17
+ //# sourceMappingURL=PrintAgent.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/print/PrintAgent.test.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest';\n\ndescribe('print-agent public domain', () => {\n it('exports a classified busy error without exposing prompt data', async () => {\n const api = await import('../../index.js') as Record<string, unknown>;\n\n expect(api).toHaveProperty('PrintAgentBusyError');\n const ErrorType = api.PrintAgentBusyError as new (agentId: string, name: string) => Error & {\n code: string;\n agentId: string;\n };\n const error = new ErrorType('agent-id', 'reviewer');\n\n expect(error).toMatchObject({\n name: 'PrintAgentBusyError',\n code: 'PRINT_AGENT_BUSY',\n agentId: 'agent-id',\n message: 'Print agent \"reviewer\" is busy.',\n });\n });\n});\n"],"names":["describe","expect","it","api","toHaveProperty","ErrorType","PrintAgentBusyError","error","toMatchObject","name","code","agentId","message"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAE9CF,SAAS,6BAA6B;IAClCE,GAAG,gEAAgE;QAC/D,MAAMC,MAAM,MAAM,MAAM,CAAC;QAEzBF,OAAOE,KAAKC,cAAc,CAAC;QAC3B,MAAMC,YAAYF,IAAIG,mBAAmB;QAIzC,MAAMC,QAAQ,IAAIF,UAAU,YAAY;QAExCJ,OAAOM,OAAOC,aAAa,CAAC;YACxBC,MAAM;YACNC,MAAM;YACNC,SAAS;YACTC,SAAS;QACb;IACJ;AACJ"}
@@ -0,0 +1,307 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ const tempDirs = [];
6
+ afterEach(()=>{
7
+ for (const dir of tempDirs.splice(0))fs.rmSync(dir, {
8
+ recursive: true,
9
+ force: true
10
+ });
11
+ });
12
+ async function loadStore() {
13
+ const api = await import('../../index.js');
14
+ expect(api).toHaveProperty('PrintAgentStore');
15
+ return api.PrintAgentStore;
16
+ }
17
+ function fixture() {
18
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'print-agent-store-'));
19
+ tempDirs.push(root);
20
+ const cwd = path.join(root, 'project');
21
+ fs.mkdirSync(cwd);
22
+ return {
23
+ root,
24
+ cwd,
25
+ filePath: path.join(root, 'state', 'print-agents.json')
26
+ };
27
+ }
28
+ describe('PrintAgentStore create/list/resolve', ()=>{
29
+ it('creates distinct durable identities with a canonical cwd and lists them', async ()=>{
30
+ const PrintAgentStore = await loadStore();
31
+ const { cwd, filePath } = fixture();
32
+ const store = new PrintAgentStore({
33
+ filePath,
34
+ now: ()=>new Date('2026-08-07T09:00:00Z')
35
+ });
36
+ const agent = await store.create({
37
+ name: 'reviewer',
38
+ cwd
39
+ });
40
+ expect(agent).toMatchObject({
41
+ name: 'reviewer',
42
+ provider: 'claude',
43
+ mode: 'print',
44
+ cwd: fs.realpathSync(cwd),
45
+ state: 'ready',
46
+ sessionHealth: 'uninitialized',
47
+ activeRun: null
48
+ });
49
+ expect(agent.id).toMatch(/^[0-9a-f-]{36}$/);
50
+ expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);
51
+ expect(agent.id).not.toBe(agent.providerSessionId);
52
+ expect(await store.list()).toEqual([
53
+ agent
54
+ ]);
55
+ expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);
56
+ });
57
+ it('resolves exact ids and names and rejects duplicate names', async ()=>{
58
+ const PrintAgentStore = await loadStore();
59
+ const { cwd, filePath } = fixture();
60
+ const store = new PrintAgentStore({
61
+ filePath
62
+ });
63
+ const agent = await store.create({
64
+ name: 'Reviewer',
65
+ cwd
66
+ });
67
+ expect(await store.resolve(agent.id)).toMatchObject({
68
+ id: agent.id
69
+ });
70
+ expect(await store.resolve('reviewer')).toMatchObject({
71
+ id: agent.id
72
+ });
73
+ expect(await store.resolve('view')).toBeNull();
74
+ await expect(store.create({
75
+ name: 'reviewer',
76
+ cwd
77
+ })).rejects.toMatchObject({
78
+ code: 'PRINT_AGENT_NAME_CONFLICT'
79
+ });
80
+ });
81
+ it('rejects missing cwd, malformed storage, and symlinked store targets', async ()=>{
82
+ const PrintAgentStore = await loadStore();
83
+ const { root, cwd, filePath } = fixture();
84
+ const store = new PrintAgentStore({
85
+ filePath
86
+ });
87
+ await expect(store.create({
88
+ name: 'missing',
89
+ cwd: path.join(root, 'missing')
90
+ })).rejects.toMatchObject({
91
+ code: 'PRINT_AGENT_STORE'
92
+ });
93
+ fs.mkdirSync(path.dirname(filePath), {
94
+ recursive: true
95
+ });
96
+ fs.writeFileSync(filePath, '{bad json', {
97
+ mode: 0o600
98
+ });
99
+ await expect(store.list()).rejects.toMatchObject({
100
+ code: 'PRINT_AGENT_STORE'
101
+ });
102
+ fs.rmSync(filePath);
103
+ const target = path.join(root, 'target.json');
104
+ fs.writeFileSync(target, JSON.stringify({
105
+ version: 1,
106
+ agents: []
107
+ }));
108
+ fs.symlinkSync(target, filePath);
109
+ await expect(store.create({
110
+ name: 'unsafe',
111
+ cwd
112
+ })).rejects.toMatchObject({
113
+ code: 'PRINT_AGENT_STORE'
114
+ });
115
+ });
116
+ it('recovers an abandoned old mutation lock after a crash', async ()=>{
117
+ const PrintAgentStore = await loadStore();
118
+ const { cwd, filePath } = fixture();
119
+ fs.mkdirSync(path.dirname(filePath), {
120
+ recursive: true
121
+ });
122
+ const lockPath = `${filePath}.lock`;
123
+ fs.mkdirSync(lockPath);
124
+ const old = new Date(Date.now() - 60_000);
125
+ fs.utimesSync(lockPath, old, old);
126
+ const store = new PrintAgentStore({
127
+ filePath,
128
+ mutationLockStaleMs: 10
129
+ });
130
+ await expect(store.create({
131
+ name: 'recovered',
132
+ cwd
133
+ })).resolves.toMatchObject({
134
+ name: 'recovered'
135
+ });
136
+ });
137
+ });
138
+ describe('PrintAgentStore run ownership', ()=>{
139
+ it('fails fast when another exact owner is live and completes only for its token', async ()=>{
140
+ const PrintAgentStore = await loadStore();
141
+ const { cwd, filePath } = fixture();
142
+ const live = new Map([
143
+ [
144
+ process.pid,
145
+ 'owner-start'
146
+ ]
147
+ ]);
148
+ const processInspector = {
149
+ getIdentity: (pid)=>{
150
+ const startedAt = live.get(pid);
151
+ return startedAt ? {
152
+ pid,
153
+ startedAt
154
+ } : null;
155
+ }
156
+ };
157
+ const store = new PrintAgentStore({
158
+ filePath,
159
+ processInspector
160
+ });
161
+ const agent = await store.create({
162
+ name: 'runner',
163
+ cwd
164
+ });
165
+ const acquired = await store.acquireRun(agent.id);
166
+ await expect(store.acquireRun(agent.id)).rejects.toMatchObject({
167
+ code: 'PRINT_AGENT_BUSY'
168
+ });
169
+ await expect(store.completeRun(agent.id, 'wrong-token', {
170
+ status: 'succeeded',
171
+ exitCode: 0,
172
+ summary: 'done',
173
+ sessionHealth: 'healthy'
174
+ })).rejects.toMatchObject({
175
+ code: 'PRINT_AGENT_STORE'
176
+ });
177
+ const completed = await store.completeRun(agent.id, acquired.token, {
178
+ status: 'succeeded',
179
+ exitCode: 0,
180
+ summary: 'done',
181
+ sessionHealth: 'healthy'
182
+ });
183
+ expect(completed).toMatchObject({
184
+ state: 'ready',
185
+ sessionHealth: 'healthy',
186
+ activeRun: null
187
+ });
188
+ });
189
+ it('retains busy for a live provider then recovers a dead run without signaling it', async ()=>{
190
+ const PrintAgentStore = await loadStore();
191
+ const { cwd, filePath } = fixture();
192
+ const live = new Map([
193
+ [
194
+ process.pid,
195
+ 'owner-start'
196
+ ],
197
+ [
198
+ 4242,
199
+ 'provider-start'
200
+ ]
201
+ ]);
202
+ const processInspector = {
203
+ getIdentity: (pid)=>{
204
+ const startedAt = live.get(pid);
205
+ return startedAt ? {
206
+ pid,
207
+ startedAt
208
+ } : null;
209
+ }
210
+ };
211
+ const first = new PrintAgentStore({
212
+ filePath,
213
+ processInspector
214
+ });
215
+ const agent = await first.create({
216
+ name: 'recoverable',
217
+ cwd
218
+ });
219
+ const run = await first.acquireRun(agent.id);
220
+ await first.recordProviderProcess(agent.id, run.token, {
221
+ pid: 4242,
222
+ startedAt: 'provider-start'
223
+ });
224
+ live.delete(process.pid);
225
+ await expect(first.acquireRun(agent.id)).rejects.toMatchObject({
226
+ code: 'PRINT_AGENT_BUSY'
227
+ });
228
+ live.delete(4242);
229
+ live.set(process.pid, 'replacement-owner-start');
230
+ const recovered = await first.acquireRun(agent.id);
231
+ expect(recovered.agent).toMatchObject({
232
+ state: 'running',
233
+ lastResult: {
234
+ status: 'interrupted'
235
+ }
236
+ });
237
+ await first.completeRun(agent.id, recovered.token, {
238
+ status: 'failed',
239
+ exitCode: 1,
240
+ summary: 'failed',
241
+ sessionHealth: 'unknown'
242
+ });
243
+ });
244
+ it('reconciles an old incomplete lock to degraded during list', async ()=>{
245
+ const PrintAgentStore = await loadStore();
246
+ const { root, cwd, filePath } = fixture();
247
+ const live = new Map([
248
+ [
249
+ process.pid,
250
+ 'owner-start'
251
+ ]
252
+ ]);
253
+ const store = new PrintAgentStore({
254
+ filePath,
255
+ incompleteLockGraceMs: 10,
256
+ processInspector: {
257
+ getIdentity: (pid)=>{
258
+ const startedAt = live.get(pid);
259
+ return startedAt ? {
260
+ pid,
261
+ startedAt
262
+ } : null;
263
+ }
264
+ }
265
+ });
266
+ const agent = await store.create({
267
+ name: 'crashed',
268
+ cwd
269
+ });
270
+ await store.acquireRun(agent.id);
271
+ const lockPath = path.join(root, 'state', 'print-agent-locks', `${agent.id}.lock`);
272
+ fs.unlinkSync(path.join(lockPath, 'owner.json'));
273
+ const old = new Date(Date.now() - 1000);
274
+ fs.utimesSync(lockPath, old, old);
275
+ live.clear();
276
+ const listed = await store.list();
277
+ expect(listed[0]).toMatchObject({
278
+ state: 'degraded',
279
+ sessionHealth: 'unknown',
280
+ activeRun: null,
281
+ lastResult: {
282
+ status: 'interrupted'
283
+ }
284
+ });
285
+ });
286
+ it('rejects send acquisition when the bound cwd is replaced by a symlink', async ()=>{
287
+ const PrintAgentStore = await loadStore();
288
+ const { root, cwd, filePath } = fixture();
289
+ const store = new PrintAgentStore({
290
+ filePath
291
+ });
292
+ const agent = await store.create({
293
+ name: 'bound',
294
+ cwd
295
+ });
296
+ const moved = path.join(root, 'moved-project');
297
+ const other = path.join(root, 'other-project');
298
+ fs.renameSync(cwd, moved);
299
+ fs.mkdirSync(other);
300
+ fs.symlinkSync(other, cwd);
301
+ await expect(store.acquireRun(agent.id)).rejects.toMatchObject({
302
+ code: 'PRINT_AGENT_STORE'
303
+ });
304
+ });
305
+ });
306
+
307
+ //# sourceMappingURL=PrintAgentStore.test.js.map