@doingdev/opencode-claude-manager-plugin 0.1.64 → 0.1.66

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 (125) hide show
  1. package/README.md +106 -120
  2. package/dist/claude/claude-agent-sdk-adapter.js +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/manager/team-orchestrator.js +1 -1
  5. package/dist/plugin/agents/common.d.ts +2 -2
  6. package/dist/plugin/agents/common.js +5 -0
  7. package/dist/plugin/claude-manager.plugin.js +104 -0
  8. package/dist/plugin/inbox-ops.d.ts +50 -0
  9. package/dist/plugin/inbox-ops.js +166 -0
  10. package/dist/types/contracts.d.ts +18 -0
  11. package/package.json +13 -13
  12. package/dist/claude/session-live-tailer.d.ts +0 -51
  13. package/dist/claude/session-live-tailer.js +0 -269
  14. package/dist/manager/session-controller.d.ts +0 -41
  15. package/dist/manager/session-controller.js +0 -97
  16. package/dist/metadata/claude-metadata.service.d.ts +0 -12
  17. package/dist/metadata/claude-metadata.service.js +0 -38
  18. package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
  19. package/dist/metadata/repo-claude-config-reader.js +0 -154
  20. package/dist/plugin/orchestrator.plugin.d.ts +0 -2
  21. package/dist/plugin/orchestrator.plugin.js +0 -116
  22. package/dist/providers/claude-code-wrapper.d.ts +0 -13
  23. package/dist/providers/claude-code-wrapper.js +0 -13
  24. package/dist/safety/bash-safety.d.ts +0 -21
  25. package/dist/safety/bash-safety.js +0 -62
  26. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
  27. package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
  28. package/dist/src/claude/claude-session.service.d.ts +0 -9
  29. package/dist/src/claude/claude-session.service.js +0 -15
  30. package/dist/src/claude/session-live-tailer.d.ts +0 -51
  31. package/dist/src/claude/session-live-tailer.js +0 -269
  32. package/dist/src/claude/tool-approval-manager.d.ts +0 -30
  33. package/dist/src/claude/tool-approval-manager.js +0 -279
  34. package/dist/src/index.d.ts +0 -5
  35. package/dist/src/index.js +0 -3
  36. package/dist/src/manager/context-tracker.d.ts +0 -32
  37. package/dist/src/manager/context-tracker.js +0 -103
  38. package/dist/src/manager/git-operations.d.ts +0 -18
  39. package/dist/src/manager/git-operations.js +0 -86
  40. package/dist/src/manager/persistent-manager.d.ts +0 -39
  41. package/dist/src/manager/persistent-manager.js +0 -44
  42. package/dist/src/manager/session-controller.d.ts +0 -41
  43. package/dist/src/manager/session-controller.js +0 -97
  44. package/dist/src/manager/team-orchestrator.d.ts +0 -81
  45. package/dist/src/manager/team-orchestrator.js +0 -612
  46. package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
  47. package/dist/src/plugin/agent-hierarchy.js +0 -2
  48. package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
  49. package/dist/src/plugin/agents/browser-qa.js +0 -31
  50. package/dist/src/plugin/agents/common.d.ts +0 -36
  51. package/dist/src/plugin/agents/common.js +0 -59
  52. package/dist/src/plugin/agents/cto.d.ts +0 -9
  53. package/dist/src/plugin/agents/cto.js +0 -39
  54. package/dist/src/plugin/agents/engineers.d.ts +0 -9
  55. package/dist/src/plugin/agents/engineers.js +0 -11
  56. package/dist/src/plugin/agents/index.d.ts +0 -5
  57. package/dist/src/plugin/agents/index.js +0 -5
  58. package/dist/src/plugin/agents/team-planner.d.ts +0 -10
  59. package/dist/src/plugin/agents/team-planner.js +0 -23
  60. package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
  61. package/dist/src/plugin/claude-manager.plugin.js +0 -950
  62. package/dist/src/plugin/service-factory.d.ts +0 -38
  63. package/dist/src/plugin/service-factory.js +0 -101
  64. package/dist/src/prompts/registry.d.ts +0 -2
  65. package/dist/src/prompts/registry.js +0 -210
  66. package/dist/src/state/file-run-state-store.d.ts +0 -14
  67. package/dist/src/state/file-run-state-store.js +0 -85
  68. package/dist/src/state/team-state-store.d.ts +0 -14
  69. package/dist/src/state/team-state-store.js +0 -88
  70. package/dist/src/state/transcript-store.d.ts +0 -15
  71. package/dist/src/state/transcript-store.js +0 -44
  72. package/dist/src/team/roster.d.ts +0 -5
  73. package/dist/src/team/roster.js +0 -40
  74. package/dist/src/types/contracts.d.ts +0 -261
  75. package/dist/src/types/contracts.js +0 -2
  76. package/dist/src/util/fs-helpers.d.ts +0 -8
  77. package/dist/src/util/fs-helpers.js +0 -21
  78. package/dist/src/util/project-context.d.ts +0 -10
  79. package/dist/src/util/project-context.js +0 -105
  80. package/dist/src/util/transcript-append.d.ts +0 -7
  81. package/dist/src/util/transcript-append.js +0 -29
  82. package/dist/state/file-run-state-store.d.ts +0 -14
  83. package/dist/state/file-run-state-store.js +0 -85
  84. package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
  85. package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
  86. package/dist/test/claude-manager.plugin.test.d.ts +0 -1
  87. package/dist/test/claude-manager.plugin.test.js +0 -316
  88. package/dist/test/context-tracker.test.d.ts +0 -1
  89. package/dist/test/context-tracker.test.js +0 -130
  90. package/dist/test/cto-active-team.test.d.ts +0 -1
  91. package/dist/test/cto-active-team.test.js +0 -199
  92. package/dist/test/file-run-state-store.test.d.ts +0 -1
  93. package/dist/test/file-run-state-store.test.js +0 -82
  94. package/dist/test/fs-helpers.test.d.ts +0 -1
  95. package/dist/test/fs-helpers.test.js +0 -56
  96. package/dist/test/git-operations.test.d.ts +0 -1
  97. package/dist/test/git-operations.test.js +0 -133
  98. package/dist/test/persistent-manager.test.d.ts +0 -1
  99. package/dist/test/persistent-manager.test.js +0 -48
  100. package/dist/test/project-context.test.d.ts +0 -1
  101. package/dist/test/project-context.test.js +0 -92
  102. package/dist/test/prompt-registry.test.d.ts +0 -1
  103. package/dist/test/prompt-registry.test.js +0 -117
  104. package/dist/test/report-claude-event.test.d.ts +0 -1
  105. package/dist/test/report-claude-event.test.js +0 -304
  106. package/dist/test/session-controller.test.d.ts +0 -1
  107. package/dist/test/session-controller.test.js +0 -149
  108. package/dist/test/session-live-tailer.test.d.ts +0 -1
  109. package/dist/test/session-live-tailer.test.js +0 -313
  110. package/dist/test/team-orchestrator.test.d.ts +0 -1
  111. package/dist/test/team-orchestrator.test.js +0 -583
  112. package/dist/test/team-state-store.test.d.ts +0 -1
  113. package/dist/test/team-state-store.test.js +0 -54
  114. package/dist/test/tool-approval-manager.test.d.ts +0 -1
  115. package/dist/test/tool-approval-manager.test.js +0 -260
  116. package/dist/test/transcript-append.test.d.ts +0 -1
  117. package/dist/test/transcript-append.test.js +0 -37
  118. package/dist/test/transcript-store.test.d.ts +0 -1
  119. package/dist/test/transcript-store.test.js +0 -50
  120. package/dist/test/undo-propagation.test.d.ts +0 -1
  121. package/dist/test/undo-propagation.test.js +0 -837
  122. package/dist/util/project-context.d.ts +0 -10
  123. package/dist/util/project-context.js +0 -105
  124. package/dist/vitest.config.d.ts +0 -2
  125. package/dist/vitest.config.js +0 -11
@@ -1,304 +0,0 @@
1
- /**
2
- * Tests for reportClaudeEvent via the real plugin onEvent chain,
3
- * plus integration tests for second-invocation continuity across
4
- * clearPluginServices() / new plugin instance.
5
- */
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { mkdtemp, rm } from 'node:fs/promises';
8
- import { join } from 'node:path';
9
- import { tmpdir } from 'node:os';
10
- import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
11
- import { clearPluginServices, getOrCreatePluginServices, getSessionTeam, registerParentSession, } from '../src/plugin/service-factory.js';
12
- import { AGENT_CTO, ENGINEER_AGENT_IDS } from '../src/plugin/agent-hierarchy.js';
13
- import { TeamStateStore } from '../src/state/team-state-store.js';
14
- import { TeamOrchestrator } from '../src/manager/team-orchestrator.js';
15
- const BROWSER_QA_TEST_CAPS = {
16
- sessionPrompt: 'Browser QA prompt',
17
- restrictWriteTools: true,
18
- skipModeInstructions: true,
19
- plannerEligible: false,
20
- isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
21
- runtimeUnavailableTitle: '❌ Playwright unavailable',
22
- };
23
- function makeContext(worktree, agentId, sessionID) {
24
- const metadata = vi.fn();
25
- const ctx = {
26
- metadata,
27
- worktree,
28
- sessionID,
29
- agent: agentId,
30
- abort: new AbortController().signal,
31
- };
32
- return { metadata, ctx };
33
- }
34
- function makeDispatchResult(override = {}) {
35
- const context = {
36
- sessionId: 'ses-tom-1',
37
- totalTurns: 1,
38
- totalCostUsd: 0.01,
39
- latestInputTokens: 500,
40
- latestOutputTokens: 100,
41
- contextWindowSize: 200_000,
42
- estimatedContextPercent: 0.5,
43
- warningLevel: 'ok',
44
- compactionCount: 0,
45
- };
46
- return {
47
- teamId: 'team-1',
48
- engineer: 'Tom',
49
- mode: 'explore',
50
- sessionId: 'ses-tom-1',
51
- finalText: 'done',
52
- turns: 1,
53
- totalCostUsd: 0.01,
54
- inputTokens: 500,
55
- outputTokens: 100,
56
- contextWindowSize: 200_000,
57
- context,
58
- ...override,
59
- };
60
- }
61
- async function executeClaude(plugin, ctx, args = { mode: 'explore', message: 'do work' }) {
62
- const claudeTool = plugin.tool['claude'];
63
- return claudeTool.execute(args, ctx);
64
- }
65
- // ── reportClaudeEvent via the plugin's onEvent chain ────────────────────────
66
- describe('reportClaudeEvent — via plugin onEvent chain', () => {
67
- let tempRoot;
68
- beforeEach(async () => {
69
- tempRoot = await mkdtemp(join(tmpdir(), 'report-event-'));
70
- clearPluginServices();
71
- });
72
- afterEach(async () => {
73
- clearPluginServices();
74
- if (tempRoot)
75
- await rm(tempRoot, { recursive: true, force: true });
76
- });
77
- /**
78
- * Helper: creates a plugin, sets the active CTO team, then stubs
79
- * dispatchEngineer so it fires the given events before returning.
80
- */
81
- async function setupPlugin(events) {
82
- const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
83
- const chatMessage = plugin['chat.message'];
84
- await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-1' });
85
- const services = getOrCreatePluginServices(tempRoot);
86
- vi.spyOn(services.orchestrator, 'dispatchEngineer').mockImplementation(async (input) => {
87
- for (const event of events) {
88
- await input.onEvent?.(event);
89
- }
90
- return makeDispatchResult();
91
- });
92
- vi.spyOn(services.orchestrator, 'recordWrapperExchange').mockResolvedValue(undefined);
93
- return { plugin, services };
94
- }
95
- it('surfaces tool name, args, and toolId from a tool_call event', async () => {
96
- const event = {
97
- type: 'tool_call',
98
- sessionId: 'ses-1',
99
- text: JSON.stringify({ name: 'read', id: 'call-abc', input: { file_path: '/foo.ts' } }),
100
- };
101
- const { plugin } = await setupPlugin([event]);
102
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Tom, 'wrapper-1');
103
- await executeClaude(plugin, ctx);
104
- const call = metadata.mock.calls.find(([c]) => c?.title?.includes('→'))?.[0];
105
- expect(call).toBeDefined();
106
- expect(call.title).toBe('⚡ Tom → Reading: /foo.ts');
107
- expect(call.metadata.toolName).toBe('read');
108
- expect(call.metadata.toolId).toBe('call-abc');
109
- expect(call.metadata.toolArgs).toEqual({ file_path: '/foo.ts' });
110
- expect(call.metadata.sessionId).toBe('ses-1');
111
- expect(call.metadata.workerName).toBe('Tom');
112
- });
113
- it('double-decodes a JSON-string input (tool input serialised twice)', async () => {
114
- // The SDK adapter may serialize `input` as a JSON string inside the outer JSON on
115
- // some tool calls. The handler should parse the inner string into an object.
116
- const event = {
117
- type: 'tool_call',
118
- sessionId: 'ses-2',
119
- text: JSON.stringify({
120
- name: 'bash',
121
- id: 'call-def',
122
- input: JSON.stringify({ command: 'ls -la' }),
123
- }),
124
- };
125
- const { plugin } = await setupPlugin([event]);
126
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Tom, 'wrapper-2');
127
- await executeClaude(plugin, ctx);
128
- const call = metadata.mock.calls.find(([c]) => c?.title?.includes('→'))?.[0];
129
- expect(call.title).toBe('⚡ Tom → Running: ls -la');
130
- expect(call.metadata.toolArgs).toEqual({ command: 'ls -la' });
131
- });
132
- it('falls back to generic title and omits tool fields when event.text is not JSON', async () => {
133
- const event = {
134
- type: 'tool_call',
135
- sessionId: 'ses-3',
136
- text: 'not-json-at-all',
137
- };
138
- const { plugin } = await setupPlugin([event]);
139
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Tom, 'wrapper-3');
140
- await executeClaude(plugin, ctx);
141
- const call = metadata.mock.calls.find(([c]) => c?.title?.includes('is using Claude Code tools'))?.[0];
142
- expect(call).toBeDefined();
143
- expect(call.title).toBe('⚡ Tom is using Claude Code tools');
144
- expect(call.metadata).not.toHaveProperty('toolName');
145
- expect(call.metadata).not.toHaveProperty('toolId');
146
- expect(call.metadata).not.toHaveProperty('toolArgs');
147
- });
148
- it('falls back to generic title when parsed JSON has no name field', async () => {
149
- const event = {
150
- type: 'tool_call',
151
- sessionId: 'ses-4',
152
- text: JSON.stringify({ id: 'call-xyz', input: {} }),
153
- };
154
- const { plugin } = await setupPlugin([event]);
155
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.John, 'wrapper-4');
156
- await executeClaude(plugin, ctx);
157
- const call = metadata.mock.calls.find(([c]) => c?.title?.includes('is using Claude Code tools'))?.[0];
158
- expect(call).toBeDefined();
159
- expect(call.metadata).not.toHaveProperty('toolName');
160
- });
161
- it('includes toolArgs when input is an empty object', async () => {
162
- const event = {
163
- type: 'tool_call',
164
- sessionId: 'ses-5',
165
- text: JSON.stringify({ name: 'git_status', id: 'call-ghi', input: {} }),
166
- };
167
- const { plugin } = await setupPlugin([event]);
168
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Maya, 'wrapper-5');
169
- await executeClaude(plugin, ctx);
170
- const call = metadata.mock.calls.find(([c]) => c?.title?.includes('→'))?.[0];
171
- expect(call.title).toBe('⚡ Maya → git_status');
172
- expect(call.metadata.toolArgs).toEqual({});
173
- });
174
- it('surfaces status event as visible metadata', async () => {
175
- const event = {
176
- type: 'status',
177
- text: 'Context exhausted; resetting session and retrying once with a fresh session.',
178
- };
179
- const { plugin } = await setupPlugin([event]);
180
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Sara, 'wrapper-6');
181
- await executeClaude(plugin, ctx);
182
- const statusCall = metadata.mock.calls.find(([c]) => c?.title?.includes('ℹ️'))?.[0];
183
- expect(statusCall).toBeDefined();
184
- expect(statusCall.title).toBe('ℹ️ Sara: Context exhausted; resetting session and retrying once with a fresh session.');
185
- expect(statusCall.metadata.status).toBe('Context exhausted; resetting session and retrying once with a fresh session.');
186
- });
187
- });
188
- // ── Second-invocation continuity ─────────────────────────────────────────────
189
- describe('second invocation continuity', () => {
190
- let tempRoot;
191
- beforeEach(async () => {
192
- tempRoot = await mkdtemp(join(tmpdir(), 'continuity-'));
193
- clearPluginServices();
194
- });
195
- afterEach(async () => {
196
- clearPluginServices();
197
- if (tempRoot)
198
- await rm(tempRoot, { recursive: true, force: true });
199
- });
200
- it('wrapper memory is injected after clearPluginServices and same CTO session resumes', async () => {
201
- // ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
202
- // Team ID = 'cto-1' (the CTO session ID that originally ran this work).
203
- const store = new TeamStateStore();
204
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
205
- await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
206
- await orchestrator.recordWrapperExchange(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1', 'explore', 'Investigate the auth flow', 'Found two race conditions in the token refresh path.');
207
- // ── Phase 2: process restart ───────────────────────────────────────────
208
- clearPluginServices();
209
- // ── Phase 3: same CTO session resumes, engineers run a new wrapper ──────
210
- const plugin2 = await ClaudeManagerPlugin({ worktree: tempRoot });
211
- const chatMessage2 = plugin2['chat.message'];
212
- const systemTransform2 = plugin2['experimental.chat.system.transform'];
213
- // Same CTO session ID resumes — re-registers itself as the team.
214
- await chatMessage2({ agent: AGENT_CTO, sessionID: 'cto-1' });
215
- expect(getSessionTeam('cto-1')).toBe('cto-1');
216
- // Simulate session.created: OpenCode fires this when cto-1 spawns the new wrapper.
217
- registerParentSession('wrapper-tom-2', 'cto-1');
218
- // Tom's new wrapper session is registered under the same team.
219
- await chatMessage2({ agent: ENGINEER_AGENT_IDS.Tom, sessionID: 'wrapper-tom-2' });
220
- // Transform fires (after chat.message has registered the session mapping).
221
- const output = { system: [] };
222
- await systemTransform2({ sessionID: 'wrapper-tom-2', model: 'claude-sonnet-4-6' }, output);
223
- expect(output.system).toHaveLength(1);
224
- expect(output.system[0]).toContain('Persistent wrapper memory for Tom');
225
- expect(output.system[0]).toContain('Investigate the auth flow');
226
- expect(output.system[0]).toContain('Found two race conditions');
227
- });
228
- it('existing engineer Claude session is resumed on second invocation', async () => {
229
- // ── Phase 1: pre-seed Tom with a claudeSessionId under team 'cto-1' ────
230
- const store = new TeamStateStore();
231
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
232
- await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
233
- await store.updateTeam(tempRoot, 'cto-1', (team) => ({
234
- ...team,
235
- engineers: team.engineers.map((e) => e.name === 'Tom' ? { ...e, claudeSessionId: 'ses-tom-persisted' } : e),
236
- }));
237
- // ── Phase 2: process restart ───────────────────────────────────────────
238
- clearPluginServices();
239
- // ── Phase 3: same CTO session resumes, engineer runs second task ───────
240
- const plugin2 = await ClaudeManagerPlugin({ worktree: tempRoot });
241
- const chatMessage2 = plugin2['chat.message'];
242
- // Same CTO session ID — re-registers as the team so Tom can find his session.
243
- await chatMessage2({ agent: AGENT_CTO, sessionID: 'cto-1' });
244
- // Simulate session.created: OpenCode fires this when cto-1 spawns the new wrapper.
245
- registerParentSession('wrapper-tom-2', 'cto-1');
246
- await chatMessage2({ agent: ENGINEER_AGENT_IDS.Tom, sessionID: 'wrapper-tom-2' });
247
- const services2 = getOrCreatePluginServices(tempRoot);
248
- // Mock at the session level so dispatchEngineer runs its real logic
249
- // (reads claudeSessionId, passes resumeSessionId to runTask).
250
- const runTask = vi.spyOn(services2.sessions, 'runTask').mockResolvedValue({
251
- sessionId: 'ses-tom-persisted',
252
- events: [],
253
- finalText: 'resumed result',
254
- turns: 2,
255
- totalCostUsd: 0.02,
256
- inputTokens: 1000,
257
- outputTokens: 200,
258
- contextWindowSize: 200_000,
259
- });
260
- vi.spyOn(services2.orchestrator, 'recordWrapperExchange').mockResolvedValue(undefined);
261
- const { ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Tom, 'wrapper-tom-2');
262
- await executeClaude(plugin2, ctx);
263
- // dispatchEngineer should have read claudeSessionId='ses-tom-persisted' from
264
- // the team store and forwarded it as resumeSessionId.
265
- expect(runTask).toHaveBeenCalledOnce();
266
- expect(runTask.mock.calls[0]?.[0]).toMatchObject({
267
- resumeSessionId: 'ses-tom-persisted',
268
- });
269
- expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined(); // no system prompt when resuming
270
- });
271
- it('BrowserQA PLAYWRIGHT_UNAVAILABLE returns plainly and emits warning metadata', async () => {
272
- const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
273
- const chatMessage = plugin['chat.message'];
274
- // Register BrowserQA session
275
- await chatMessage({ agent: ENGINEER_AGENT_IDS.BrowserQA, sessionID: 'wrapper-browserqa-1' });
276
- const services = getOrCreatePluginServices(tempRoot);
277
- // Mock dispatchEngineer to return PLAYWRIGHT_UNAVAILABLE sentinel
278
- const unavailableMessage = 'PLAYWRIGHT_UNAVAILABLE: Playwright is not available in this environment. Consider installing it via npm.';
279
- vi.spyOn(services.orchestrator, 'dispatchEngineer').mockResolvedValueOnce(makeDispatchResult({
280
- engineer: 'BrowserQA',
281
- finalText: unavailableMessage,
282
- }));
283
- vi.spyOn(services.orchestrator, 'recordWrapperExchange').mockResolvedValue(undefined);
284
- const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.BrowserQA, 'wrapper-browserqa-1');
285
- const result = await executeClaude(plugin, ctx, {
286
- mode: 'verify',
287
- message: 'Check if browser is available',
288
- });
289
- // Result should contain the PLAYWRIGHT_UNAVAILABLE text plainly (not wrapped in error)
290
- expect(result).toContain('PLAYWRIGHT_UNAVAILABLE');
291
- expect(result).toContain('Playwright is not available');
292
- // Verify warning metadata was emitted
293
- expect(metadata).toHaveBeenCalled();
294
- const calls = metadata.mock.calls;
295
- const warningCall = calls.find((call) => {
296
- const arg = call[0];
297
- const title = arg?.title ?? '';
298
- return (title.includes('Playwright') ||
299
- title.includes('unavailable') ||
300
- title.toLowerCase().includes('playwright unavailable'));
301
- });
302
- expect(warningCall).toBeDefined();
303
- });
304
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,149 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { SessionController } from '../src/manager/session-controller.js';
3
- import { ContextTracker } from '../src/manager/context-tracker.js';
4
- function createMockAdapter(results = [{}]) {
5
- let callIndex = 0;
6
- return {
7
- runSession: vi.fn(async () => {
8
- const base = {
9
- sessionId: 'ses_test',
10
- events: [],
11
- finalText: 'Done.',
12
- turns: 1,
13
- totalCostUsd: 0.01,
14
- inputTokens: 10_000,
15
- outputTokens: 500,
16
- contextWindowSize: 200_000,
17
- };
18
- const overrides = results[callIndex] ?? {};
19
- callIndex++;
20
- return { ...base, ...overrides };
21
- }),
22
- };
23
- }
24
- describe('SessionController', () => {
25
- it('creates a new session on first sendMessage', async () => {
26
- const adapter = createMockAdapter();
27
- const tracker = new ContextTracker();
28
- const controller = new SessionController(adapter, tracker, 'test prompt', 'test', '/tmp');
29
- expect(controller.isActive).toBe(false);
30
- const result = await controller.sendMessage('hello');
31
- expect(controller.isActive).toBe(true);
32
- expect(controller.sessionId).toBe('ses_test');
33
- expect(result.finalText).toBe('Done.');
34
- // Verify system prompt was used (no resume)
35
- const call = adapter.runSession.mock.calls[0][0];
36
- expect(call.systemPrompt).toBe('test prompt');
37
- expect(call.resumeSessionId).toBeUndefined();
38
- });
39
- it('sends settingSources as user-only', async () => {
40
- const adapter = createMockAdapter();
41
- const tracker = new ContextTracker();
42
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
43
- await controller.sendMessage('hello');
44
- const call = adapter.runSession.mock.calls[0][0];
45
- expect(call.settingSources).toEqual(['user']);
46
- });
47
- it('resumes session on subsequent sends', async () => {
48
- const adapter = createMockAdapter([{}, {}]);
49
- const tracker = new ContextTracker();
50
- const controller = new SessionController(adapter, tracker, 'test prompt', 'test', '/tmp');
51
- await controller.sendMessage('first');
52
- await controller.sendMessage('second');
53
- const secondCall = adapter.runSession.mock.calls[1][0];
54
- expect(secondCall.resumeSessionId).toBe('ses_test');
55
- expect(secondCall.systemPrompt).toBeUndefined();
56
- });
57
- it('updates context tracker on each message', async () => {
58
- const adapter = createMockAdapter([{ turns: 3, totalCostUsd: 0.05, inputTokens: 50_000 }]);
59
- const tracker = new ContextTracker();
60
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
61
- await controller.sendMessage('task');
62
- const snap = controller.getContextSnapshot();
63
- expect(snap.totalTurns).toBe(3);
64
- expect(snap.totalCostUsd).toBe(0.05);
65
- expect(snap.latestInputTokens).toBe(50_000);
66
- expect(snap.estimatedContextPercent).toBe(25);
67
- });
68
- it('clears session and resets context', async () => {
69
- const adapter = createMockAdapter();
70
- const tracker = new ContextTracker();
71
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
72
- await controller.sendMessage('task');
73
- expect(controller.isActive).toBe(true);
74
- const clearedId = await controller.clearSession();
75
- expect(clearedId).toBe('ses_test');
76
- expect(controller.isActive).toBe(false);
77
- expect(controller.sessionId).toBeNull();
78
- const snap = controller.getContextSnapshot();
79
- expect(snap.totalTurns).toBe(0);
80
- expect(snap.sessionId).toBeNull();
81
- });
82
- it('sends /compact to current session', async () => {
83
- const adapter = createMockAdapter([{}, {}]);
84
- const tracker = new ContextTracker();
85
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
86
- await controller.sendMessage('task');
87
- await controller.compactSession();
88
- const compactCall = adapter.runSession.mock.calls[1][0];
89
- expect(compactCall.prompt).toBe('/compact');
90
- expect(compactCall.resumeSessionId).toBe('ses_test');
91
- });
92
- it('throws when compacting without active session', async () => {
93
- const adapter = createMockAdapter();
94
- const tracker = new ContextTracker();
95
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
96
- await expect(controller.compactSession()).rejects.toThrow('No active session to compact');
97
- });
98
- it('threads effort through to the SDK input', async () => {
99
- const adapter = createMockAdapter();
100
- const tracker = new ContextTracker();
101
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
102
- await controller.sendMessage('hard task', { effort: 'max' });
103
- const call = adapter.runSession.mock.calls[0][0];
104
- expect(call.effort).toBe('max');
105
- });
106
- describe('plan/free mode', () => {
107
- const modePrefixes = {
108
- plan: '[PLAN MODE] Read-only planning.',
109
- free: '',
110
- };
111
- it('defaults to free mode with acceptEdits permissionMode', async () => {
112
- const adapter = createMockAdapter();
113
- const tracker = new ContextTracker();
114
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
115
- await controller.sendMessage('do something');
116
- const call = adapter.runSession.mock.calls[0][0];
117
- expect(call.permissionMode).toBe('acceptEdits');
118
- expect(call.prompt).toBe('do something');
119
- });
120
- it('sets permissionMode to plan and prepends prefix in plan mode', async () => {
121
- const adapter = createMockAdapter();
122
- const tracker = new ContextTracker();
123
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
124
- await controller.sendMessage('analyze this', { mode: 'plan' });
125
- const call = adapter.runSession.mock.calls[0][0];
126
- expect(call.permissionMode).toBe('plan');
127
- expect(call.prompt).toBe('[PLAN MODE] Read-only planning.\n\nanalyze this');
128
- });
129
- it('explicit free mode uses acceptEdits and no prefix', async () => {
130
- const adapter = createMockAdapter();
131
- const tracker = new ContextTracker();
132
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp', modePrefixes);
133
- await controller.sendMessage('build it', { mode: 'free' });
134
- const call = adapter.runSession.mock.calls[0][0];
135
- expect(call.permissionMode).toBe('acceptEdits');
136
- expect(call.prompt).toBe('build it');
137
- });
138
- it('works without modePrefixes constructor arg (backward compat)', async () => {
139
- const adapter = createMockAdapter();
140
- const tracker = new ContextTracker();
141
- const controller = new SessionController(adapter, tracker, 'test', 'test', '/tmp');
142
- await controller.sendMessage('hello', { mode: 'plan' });
143
- const call = adapter.runSession.mock.calls[0][0];
144
- expect(call.permissionMode).toBe('plan');
145
- // Empty prefix defaults — prompt should be unchanged
146
- expect(call.prompt).toBe('hello');
147
- });
148
- });
149
- });
@@ -1 +0,0 @@
1
- export {};