@doingdev/opencode-claude-manager-plugin 0.1.65 → 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.
- package/dist/index.d.ts +1 -1
- package/dist/manager/team-orchestrator.js +1 -1
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +5 -0
- package/dist/plugin/claude-manager.plugin.js +104 -0
- package/dist/plugin/inbox-ops.d.ts +50 -0
- package/dist/plugin/inbox-ops.js +166 -0
- package/dist/types/contracts.d.ts +18 -0
- package/package.json +1 -1
- package/dist/claude/session-live-tailer.d.ts +0 -51
- package/dist/claude/session-live-tailer.js +0 -269
- package/dist/manager/session-controller.d.ts +0 -41
- package/dist/manager/session-controller.js +0 -97
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/orchestrator.plugin.d.ts +0 -2
- package/dist/plugin/orchestrator.plugin.js +0 -116
- package/dist/providers/claude-code-wrapper.d.ts +0 -13
- package/dist/providers/claude-code-wrapper.js +0 -13
- package/dist/safety/bash-safety.d.ts +0 -21
- package/dist/safety/bash-safety.js +0 -62
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
- package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
- package/dist/src/claude/claude-session.service.d.ts +0 -9
- package/dist/src/claude/claude-session.service.js +0 -15
- package/dist/src/claude/session-live-tailer.d.ts +0 -51
- package/dist/src/claude/session-live-tailer.js +0 -269
- package/dist/src/claude/tool-approval-manager.d.ts +0 -30
- package/dist/src/claude/tool-approval-manager.js +0 -279
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.js +0 -3
- package/dist/src/manager/context-tracker.d.ts +0 -32
- package/dist/src/manager/context-tracker.js +0 -103
- package/dist/src/manager/git-operations.d.ts +0 -18
- package/dist/src/manager/git-operations.js +0 -86
- package/dist/src/manager/persistent-manager.d.ts +0 -39
- package/dist/src/manager/persistent-manager.js +0 -44
- package/dist/src/manager/session-controller.d.ts +0 -41
- package/dist/src/manager/session-controller.js +0 -97
- package/dist/src/manager/team-orchestrator.d.ts +0 -81
- package/dist/src/manager/team-orchestrator.js +0 -612
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +0 -2
- package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
- package/dist/src/plugin/agents/browser-qa.js +0 -31
- package/dist/src/plugin/agents/common.d.ts +0 -36
- package/dist/src/plugin/agents/common.js +0 -59
- package/dist/src/plugin/agents/cto.d.ts +0 -9
- package/dist/src/plugin/agents/cto.js +0 -39
- package/dist/src/plugin/agents/engineers.d.ts +0 -9
- package/dist/src/plugin/agents/engineers.js +0 -11
- package/dist/src/plugin/agents/index.d.ts +0 -5
- package/dist/src/plugin/agents/index.js +0 -5
- package/dist/src/plugin/agents/team-planner.d.ts +0 -10
- package/dist/src/plugin/agents/team-planner.js +0 -23
- package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
- package/dist/src/plugin/claude-manager.plugin.js +0 -950
- package/dist/src/plugin/service-factory.d.ts +0 -38
- package/dist/src/plugin/service-factory.js +0 -101
- package/dist/src/prompts/registry.d.ts +0 -2
- package/dist/src/prompts/registry.js +0 -210
- package/dist/src/state/file-run-state-store.d.ts +0 -14
- package/dist/src/state/file-run-state-store.js +0 -85
- package/dist/src/state/team-state-store.d.ts +0 -14
- package/dist/src/state/team-state-store.js +0 -88
- package/dist/src/state/transcript-store.d.ts +0 -15
- package/dist/src/state/transcript-store.js +0 -44
- package/dist/src/team/roster.d.ts +0 -5
- package/dist/src/team/roster.js +0 -40
- package/dist/src/types/contracts.d.ts +0 -261
- package/dist/src/types/contracts.js +0 -2
- package/dist/src/util/fs-helpers.d.ts +0 -8
- package/dist/src/util/fs-helpers.js +0 -21
- package/dist/src/util/project-context.d.ts +0 -10
- package/dist/src/util/project-context.js +0 -105
- package/dist/src/util/transcript-append.d.ts +0 -7
- package/dist/src/util/transcript-append.js +0 -29
- package/dist/state/file-run-state-store.d.ts +0 -14
- package/dist/state/file-run-state-store.js +0 -85
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
- package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
- package/dist/test/claude-manager.plugin.test.d.ts +0 -1
- package/dist/test/claude-manager.plugin.test.js +0 -316
- package/dist/test/context-tracker.test.d.ts +0 -1
- package/dist/test/context-tracker.test.js +0 -130
- package/dist/test/cto-active-team.test.d.ts +0 -1
- package/dist/test/cto-active-team.test.js +0 -199
- package/dist/test/file-run-state-store.test.d.ts +0 -1
- package/dist/test/file-run-state-store.test.js +0 -82
- package/dist/test/fs-helpers.test.d.ts +0 -1
- package/dist/test/fs-helpers.test.js +0 -56
- package/dist/test/git-operations.test.d.ts +0 -1
- package/dist/test/git-operations.test.js +0 -133
- package/dist/test/persistent-manager.test.d.ts +0 -1
- package/dist/test/persistent-manager.test.js +0 -48
- package/dist/test/project-context.test.d.ts +0 -1
- package/dist/test/project-context.test.js +0 -92
- package/dist/test/prompt-registry.test.d.ts +0 -1
- package/dist/test/prompt-registry.test.js +0 -117
- package/dist/test/report-claude-event.test.d.ts +0 -1
- package/dist/test/report-claude-event.test.js +0 -304
- package/dist/test/session-controller.test.d.ts +0 -1
- package/dist/test/session-controller.test.js +0 -149
- package/dist/test/session-live-tailer.test.d.ts +0 -1
- package/dist/test/session-live-tailer.test.js +0 -313
- package/dist/test/team-orchestrator.test.d.ts +0 -1
- package/dist/test/team-orchestrator.test.js +0 -583
- package/dist/test/team-state-store.test.d.ts +0 -1
- package/dist/test/team-state-store.test.js +0 -54
- package/dist/test/tool-approval-manager.test.d.ts +0 -1
- package/dist/test/tool-approval-manager.test.js +0 -260
- package/dist/test/transcript-append.test.d.ts +0 -1
- package/dist/test/transcript-append.test.js +0 -37
- package/dist/test/transcript-store.test.d.ts +0 -1
- package/dist/test/transcript-store.test.js +0 -50
- package/dist/test/undo-propagation.test.d.ts +0 -1
- package/dist/test/undo-propagation.test.js +0 -837
- package/dist/util/project-context.d.ts +0 -10
- package/dist/util/project-context.js +0 -105
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -11
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
|
|
6
|
-
import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
|
|
7
|
-
import { clearPluginServices } from '../src/plugin/service-factory.js';
|
|
8
|
-
describe('ClaudeManagerPlugin', () => {
|
|
9
|
-
it('configures CTO with orchestration tools and question access', async () => {
|
|
10
|
-
const plugin = await ClaudeManagerPlugin({
|
|
11
|
-
worktree: '/tmp/project',
|
|
12
|
-
});
|
|
13
|
-
const config = {};
|
|
14
|
-
await plugin.config?.(config);
|
|
15
|
-
const agents = (config.agent ?? {});
|
|
16
|
-
const cto = agents[AGENT_CTO];
|
|
17
|
-
expect(cto).toBeDefined();
|
|
18
|
-
expect(cto.mode).toBe('primary');
|
|
19
|
-
expect(cto.permission).toMatchObject({
|
|
20
|
-
'*': 'deny',
|
|
21
|
-
read: 'allow',
|
|
22
|
-
grep: 'allow',
|
|
23
|
-
glob: 'allow',
|
|
24
|
-
list: 'allow',
|
|
25
|
-
codesearch: 'allow',
|
|
26
|
-
webfetch: 'allow',
|
|
27
|
-
websearch: 'allow',
|
|
28
|
-
lsp: 'allow',
|
|
29
|
-
todowrite: 'allow',
|
|
30
|
-
todoread: 'allow',
|
|
31
|
-
question: 'allow',
|
|
32
|
-
team_status: 'allow',
|
|
33
|
-
reset_engineer: 'allow',
|
|
34
|
-
git_diff: 'allow',
|
|
35
|
-
git_commit: 'allow',
|
|
36
|
-
git_reset: 'allow',
|
|
37
|
-
git_status: 'allow',
|
|
38
|
-
git_log: 'allow',
|
|
39
|
-
claude: 'deny',
|
|
40
|
-
});
|
|
41
|
-
// Task permissions should include both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
42
|
-
expect(cto.permission.task).toEqual({
|
|
43
|
-
'*': 'deny',
|
|
44
|
-
Tom: 'allow',
|
|
45
|
-
tom: 'allow',
|
|
46
|
-
John: 'allow',
|
|
47
|
-
john: 'allow',
|
|
48
|
-
Maya: 'allow',
|
|
49
|
-
maya: 'allow',
|
|
50
|
-
Sara: 'allow',
|
|
51
|
-
sara: 'allow',
|
|
52
|
-
Alex: 'allow',
|
|
53
|
-
alex: 'allow',
|
|
54
|
-
BrowserQA: 'allow',
|
|
55
|
-
'browser-qa': 'allow',
|
|
56
|
-
'team-planner': 'allow',
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
it('allows CTO to delegate to engineers using both uppercase and lowercase agent IDs', async () => {
|
|
60
|
-
const plugin = await ClaudeManagerPlugin({
|
|
61
|
-
worktree: '/tmp/project',
|
|
62
|
-
});
|
|
63
|
-
const config = {};
|
|
64
|
-
await plugin.config?.(config);
|
|
65
|
-
const agents = (config.agent ?? {});
|
|
66
|
-
const cto = agents[AGENT_CTO];
|
|
67
|
-
const taskPerms = cto.permission.task;
|
|
68
|
-
// Verify both uppercase and lowercase can be used for delegation.
|
|
69
|
-
// This prevents delegation failures when users write task(subagent_type: 'Maya') vs task(subagent_type: 'maya').
|
|
70
|
-
expect(taskPerms.Tom).toBe('allow');
|
|
71
|
-
expect(taskPerms.tom).toBe('allow');
|
|
72
|
-
expect(taskPerms.Maya).toBe('allow');
|
|
73
|
-
expect(taskPerms.maya).toBe('allow');
|
|
74
|
-
});
|
|
75
|
-
it('configures every named engineer with only the claude bridge tool', async () => {
|
|
76
|
-
const plugin = await ClaudeManagerPlugin({
|
|
77
|
-
worktree: '/tmp/project',
|
|
78
|
-
});
|
|
79
|
-
const config = {};
|
|
80
|
-
await plugin.config?.(config);
|
|
81
|
-
const agents = (config.agent ?? {});
|
|
82
|
-
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
83
|
-
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
84
|
-
const agent = agents[agentId];
|
|
85
|
-
expect(agent).toBeDefined();
|
|
86
|
-
expect(agent.mode).toBe('subagent');
|
|
87
|
-
expect(agent.description).toContain(engineer);
|
|
88
|
-
expect(agent.description).toContain('persistent engineer');
|
|
89
|
-
expect(agent.permission).toMatchObject({
|
|
90
|
-
'*': 'deny',
|
|
91
|
-
claude: 'allow',
|
|
92
|
-
git_diff: 'deny',
|
|
93
|
-
git_commit: 'deny',
|
|
94
|
-
reset_engineer: 'deny',
|
|
95
|
-
});
|
|
96
|
-
expect(agent.permission).not.toHaveProperty('read');
|
|
97
|
-
expect(agent.permission).not.toHaveProperty('grep');
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
it('configures team-planner as a thin planning wrapper subagent', async () => {
|
|
101
|
-
const plugin = await ClaudeManagerPlugin({
|
|
102
|
-
worktree: '/tmp/project',
|
|
103
|
-
});
|
|
104
|
-
const config = {};
|
|
105
|
-
await plugin.config?.(config);
|
|
106
|
-
const agents = (config.agent ?? {});
|
|
107
|
-
const teamPlanner = agents[AGENT_TEAM_PLANNER];
|
|
108
|
-
expect(teamPlanner).toBeDefined();
|
|
109
|
-
expect(teamPlanner.mode).toBe('subagent');
|
|
110
|
-
expect(teamPlanner.description.toLowerCase()).toContain('plan');
|
|
111
|
-
expect(teamPlanner.permission).toMatchObject({
|
|
112
|
-
'*': 'deny',
|
|
113
|
-
plan_with_team: 'allow',
|
|
114
|
-
question: 'allow',
|
|
115
|
-
claude: 'deny',
|
|
116
|
-
});
|
|
117
|
-
expect(teamPlanner.permission).not.toHaveProperty('read');
|
|
118
|
-
expect(teamPlanner.permission).not.toHaveProperty('grep');
|
|
119
|
-
expect(teamPlanner.permission).not.toHaveProperty('glob');
|
|
120
|
-
});
|
|
121
|
-
it('registers the named engineer bridge and team status tools', async () => {
|
|
122
|
-
const plugin = await ClaudeManagerPlugin({
|
|
123
|
-
worktree: '/tmp/project',
|
|
124
|
-
});
|
|
125
|
-
const tools = plugin.tool;
|
|
126
|
-
expect(tools['claude']).toBeDefined();
|
|
127
|
-
expect(tools['team_status']).toBeDefined();
|
|
128
|
-
expect(tools['plan_with_team']).toBeDefined();
|
|
129
|
-
expect(tools['reset_engineer']).toBeDefined();
|
|
130
|
-
expect(tools['assign_engineer']).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
it('claude tool requires mode and message and supports optional model', async () => {
|
|
133
|
-
const plugin = await ClaudeManagerPlugin({
|
|
134
|
-
worktree: '/tmp/project',
|
|
135
|
-
});
|
|
136
|
-
const tools = plugin.tool;
|
|
137
|
-
const claudeTool = tools['claude'];
|
|
138
|
-
const modeSchema = claudeTool.args.mode;
|
|
139
|
-
const messageSchema = claudeTool.args.message;
|
|
140
|
-
const modelSchema = claudeTool.args.model;
|
|
141
|
-
expect(modeSchema.safeParse('explore').success).toBe(true);
|
|
142
|
-
expect(modeSchema.safeParse('implement').success).toBe(true);
|
|
143
|
-
expect(modeSchema.safeParse('verify').success).toBe(true);
|
|
144
|
-
expect(modeSchema.safeParse('plan').success).toBe(false);
|
|
145
|
-
expect(messageSchema.safeParse('do the work').success).toBe(true);
|
|
146
|
-
expect(messageSchema.safeParse('').success).toBe(false);
|
|
147
|
-
expect(modelSchema.safeParse('claude-opus-4-6').success).toBe(true);
|
|
148
|
-
expect(modelSchema.safeParse('claude-sonnet-4-6').success).toBe(true);
|
|
149
|
-
expect(modelSchema.safeParse(undefined).success).toBe(true);
|
|
150
|
-
expect(modelSchema.safeParse('claude-haiku-4-5').success).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
it('does not expose explicit plan tracking tools', async () => {
|
|
153
|
-
const plugin = await ClaudeManagerPlugin({
|
|
154
|
-
worktree: '/tmp/project',
|
|
155
|
-
});
|
|
156
|
-
const tools = plugin.tool;
|
|
157
|
-
expect(tools['confirm_plan']).toBeUndefined();
|
|
158
|
-
expect(tools['advance_slice']).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
it('exposes hooks for CTO team tracking and wrapper memory injection', async () => {
|
|
161
|
-
const plugin = await ClaudeManagerPlugin({
|
|
162
|
-
worktree: '/tmp/project',
|
|
163
|
-
});
|
|
164
|
-
expect(plugin['chat.message']).toBeTypeOf('function');
|
|
165
|
-
expect(plugin['experimental.chat.system.transform']).toBeTypeOf('function');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
describe('Agent ID normalization and lookup helpers', () => {
|
|
169
|
-
it('normalizeAgentId converts mixed-case agent IDs to lowercase', async () => {
|
|
170
|
-
const { normalizeAgentId } = await import('../src/plugin/claude-manager.plugin.js');
|
|
171
|
-
expect(normalizeAgentId('Tom')).toBe('tom');
|
|
172
|
-
expect(normalizeAgentId('MAYA')).toBe('maya');
|
|
173
|
-
expect(normalizeAgentId('john')).toBe('john');
|
|
174
|
-
expect(normalizeAgentId('JoHn')).toBe('john');
|
|
175
|
-
});
|
|
176
|
-
it('engineerFromAgent resolves both uppercase and lowercase agent IDs', async () => {
|
|
177
|
-
const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
178
|
-
// Lowercase (canonical)
|
|
179
|
-
expect(engineerFromAgent('tom')).toBe('Tom');
|
|
180
|
-
expect(engineerFromAgent('maya')).toBe('Maya');
|
|
181
|
-
expect(engineerFromAgent('john')).toBe('John');
|
|
182
|
-
// Uppercase (normalized)
|
|
183
|
-
expect(engineerFromAgent('Tom')).toBe('Tom');
|
|
184
|
-
expect(engineerFromAgent('Maya')).toBe('Maya');
|
|
185
|
-
expect(engineerFromAgent('John')).toBe('John');
|
|
186
|
-
// Mixed case
|
|
187
|
-
expect(engineerFromAgent('JoHn')).toBe('John');
|
|
188
|
-
expect(engineerFromAgent('mAyA')).toBe('Maya');
|
|
189
|
-
});
|
|
190
|
-
it('engineerFromAgent throws on invalid agent IDs', async () => {
|
|
191
|
-
const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
192
|
-
expect(() => engineerFromAgent('invalid')).toThrow('The claude tool can only be used from a named engineer agent');
|
|
193
|
-
expect(() => engineerFromAgent('TomInvalid')).toThrow('The claude tool can only be used from a named engineer agent');
|
|
194
|
-
});
|
|
195
|
-
it('isEngineerAgent identifies both uppercase and lowercase agent IDs', async () => {
|
|
196
|
-
const { isEngineerAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
197
|
-
// Lowercase (canonical)
|
|
198
|
-
expect(isEngineerAgent('tom')).toBe(true);
|
|
199
|
-
expect(isEngineerAgent('maya')).toBe(true);
|
|
200
|
-
expect(isEngineerAgent('john')).toBe(true);
|
|
201
|
-
expect(isEngineerAgent('sara')).toBe(true);
|
|
202
|
-
expect(isEngineerAgent('alex')).toBe(true);
|
|
203
|
-
// Uppercase (normalized)
|
|
204
|
-
expect(isEngineerAgent('Tom')).toBe(true);
|
|
205
|
-
expect(isEngineerAgent('Maya')).toBe(true);
|
|
206
|
-
expect(isEngineerAgent('John')).toBe(true);
|
|
207
|
-
expect(isEngineerAgent('Sara')).toBe(true);
|
|
208
|
-
expect(isEngineerAgent('Alex')).toBe(true);
|
|
209
|
-
// Mixed case
|
|
210
|
-
expect(isEngineerAgent('JoHn')).toBe(true);
|
|
211
|
-
expect(isEngineerAgent('mAyA')).toBe(true);
|
|
212
|
-
// Invalid
|
|
213
|
-
expect(isEngineerAgent('invalid')).toBe(false);
|
|
214
|
-
expect(isEngineerAgent('cto')).toBe(false);
|
|
215
|
-
expect(isEngineerAgent('team-planner')).toBe(false);
|
|
216
|
-
});
|
|
217
|
-
it('CTO agent config does not have direct assign_engineer access (delegates to named engineers)', async () => {
|
|
218
|
-
const { buildCtoAgentConfig } = await import('../src/plugin/agent-hierarchy.js');
|
|
219
|
-
const { managerPromptRegistry } = await import('../src/prompts/registry.js');
|
|
220
|
-
const ctoConfig = buildCtoAgentConfig(managerPromptRegistry);
|
|
221
|
-
const ctoPermissions = ctoConfig.permission;
|
|
222
|
-
// CTO should NOT have direct access to assign_engineer (uses task() to named engineers instead)
|
|
223
|
-
expect(ctoPermissions['assign_engineer']).not.toBe('allow');
|
|
224
|
-
// CTO should NOT have direct access to plan_with_team (must delegate to team-planner)
|
|
225
|
-
expect(ctoPermissions['plan_with_team']).not.toBe('allow');
|
|
226
|
-
});
|
|
227
|
-
it('browser-qa agent is registered with correct permissions', async () => {
|
|
228
|
-
const plugin = await ClaudeManagerPlugin({
|
|
229
|
-
worktree: '/tmp/project',
|
|
230
|
-
});
|
|
231
|
-
const config = {};
|
|
232
|
-
await plugin.config?.(config);
|
|
233
|
-
const agents = (config.agent ?? {});
|
|
234
|
-
const { AGENT_BROWSER_QA } = await import('../src/plugin/agent-hierarchy.js');
|
|
235
|
-
const browserQa = agents[AGENT_BROWSER_QA];
|
|
236
|
-
expect(browserQa).toBeDefined();
|
|
237
|
-
expect(browserQa.permission).toBeDefined();
|
|
238
|
-
// BrowserQA should have access to claude tool
|
|
239
|
-
expect(browserQa.permission?.['claude']).toBe('allow');
|
|
240
|
-
});
|
|
241
|
-
it('browser-qa session prompt allows Playwright skill/command', async () => {
|
|
242
|
-
const { managerPromptRegistry } = await import('../src/prompts/registry.js');
|
|
243
|
-
expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Playwright skill');
|
|
244
|
-
expect(managerPromptRegistry.browserQaSessionPrompt).toContain('real browser');
|
|
245
|
-
expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Allowed tools');
|
|
246
|
-
});
|
|
247
|
-
it('browser-qa agent prompt mentions PLAYWRIGHT_UNAVAILABLE sentinel', async () => {
|
|
248
|
-
const { managerPromptRegistry } = await import('../src/prompts/registry.js');
|
|
249
|
-
expect(managerPromptRegistry.browserQaAgentPrompt).toContain('PLAYWRIGHT_UNAVAILABLE');
|
|
250
|
-
expect(managerPromptRegistry.browserQaAgentPrompt).toContain('Playwright');
|
|
251
|
-
});
|
|
252
|
-
it('browser-qa agent is registered in ENGINEER_AGENT_IDS', async () => {
|
|
253
|
-
const { AGENT_BROWSER_QA, ENGINEER_AGENT_IDS } = await import('../src/plugin/agent-hierarchy.js');
|
|
254
|
-
// BrowserQA should be in the engineer IDs
|
|
255
|
-
const engineerIds = Object.values(ENGINEER_AGENT_IDS);
|
|
256
|
-
expect(engineerIds).toContain('browser-qa');
|
|
257
|
-
expect(ENGINEER_AGENT_IDS['BrowserQA']).toBe(AGENT_BROWSER_QA);
|
|
258
|
-
});
|
|
259
|
-
it('browser-qa agent config has restricted write access', async () => {
|
|
260
|
-
const plugin = await ClaudeManagerPlugin({
|
|
261
|
-
worktree: '/tmp/project',
|
|
262
|
-
});
|
|
263
|
-
const config = {};
|
|
264
|
-
await plugin.config?.(config);
|
|
265
|
-
const agents = (config.agent ?? {});
|
|
266
|
-
const { AGENT_BROWSER_QA } = await import('../src/plugin/agent-hierarchy.js');
|
|
267
|
-
const browserQa = agents[AGENT_BROWSER_QA];
|
|
268
|
-
expect(browserQa).toBeDefined();
|
|
269
|
-
// Should allow claude tool (engineers can call claude)
|
|
270
|
-
expect(browserQa.permission?.['claude']).toBe('allow');
|
|
271
|
-
// Should deny most other tools
|
|
272
|
-
expect(browserQa.permission?.['read']).toBeUndefined(); // Falls back to deny
|
|
273
|
-
expect(browserQa.permission?.['write']).toBeUndefined(); // Falls back to deny
|
|
274
|
-
});
|
|
275
|
-
it('browserqa agent uses same engineer dispatch path', async () => {
|
|
276
|
-
const plugin = await ClaudeManagerPlugin({
|
|
277
|
-
worktree: '/tmp/project',
|
|
278
|
-
});
|
|
279
|
-
const config = {};
|
|
280
|
-
await plugin.config?.(config);
|
|
281
|
-
const agents = (config.agent ?? {});
|
|
282
|
-
// BrowserQA should be configured as an agent
|
|
283
|
-
expect(agents['browser-qa']).toBeDefined();
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
describe('claude tool — engineer failure debug log', () => {
|
|
287
|
-
let tempRoot;
|
|
288
|
-
afterEach(async () => {
|
|
289
|
-
clearPluginServices();
|
|
290
|
-
if (tempRoot) {
|
|
291
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
it('appends an engineer_failure entry to debug.log when dispatchEngineer throws', async () => {
|
|
295
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'plugin-failure-log-'));
|
|
296
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
297
|
-
const tools = plugin.tool;
|
|
298
|
-
const context = {
|
|
299
|
-
sessionID: 'wrapper-browserqa-fail',
|
|
300
|
-
worktree: tempRoot,
|
|
301
|
-
agent: AGENT_BROWSER_QA,
|
|
302
|
-
metadata: vi.fn(),
|
|
303
|
-
};
|
|
304
|
-
// BrowserQA in implement mode throws synchronously before running a session
|
|
305
|
-
await expect(tools['claude'].execute({ mode: 'implement', message: 'Write a feature' }, context)).rejects.toThrow('modeNotSupported');
|
|
306
|
-
const logPath = join(tempRoot, '.claude-manager', 'debug.log');
|
|
307
|
-
const content = await readFile(logPath, 'utf8');
|
|
308
|
-
const entry = JSON.parse(content.trim().split('\n')[0]);
|
|
309
|
-
expect(entry.type).toBe('engineer_failure');
|
|
310
|
-
expect(entry.engineer).toBe('BrowserQA');
|
|
311
|
-
expect(entry.mode).toBe('implement');
|
|
312
|
-
expect(entry.failureKind).toBe('modeNotSupported');
|
|
313
|
-
expect(typeof entry.message).toBe('string');
|
|
314
|
-
expect(typeof entry.ts).toBe('string');
|
|
315
|
-
});
|
|
316
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { ContextTracker } from '../src/manager/context-tracker.js';
|
|
3
|
-
describe('ContextTracker', () => {
|
|
4
|
-
it('starts with null estimates', () => {
|
|
5
|
-
const tracker = new ContextTracker();
|
|
6
|
-
const snap = tracker.snapshot();
|
|
7
|
-
expect(snap.estimatedContextPercent).toBeNull();
|
|
8
|
-
expect(snap.warningLevel).toBe('ok');
|
|
9
|
-
expect(snap.totalTurns).toBe(0);
|
|
10
|
-
expect(snap.totalCostUsd).toBe(0);
|
|
11
|
-
expect(snap.compactionCount).toBe(0);
|
|
12
|
-
});
|
|
13
|
-
it('estimates context % from input tokens (tier 1)', () => {
|
|
14
|
-
const tracker = new ContextTracker();
|
|
15
|
-
tracker.recordResult({
|
|
16
|
-
inputTokens: 100_000,
|
|
17
|
-
contextWindowSize: 200_000,
|
|
18
|
-
turns: 10,
|
|
19
|
-
totalCostUsd: 0.5,
|
|
20
|
-
});
|
|
21
|
-
const snap = tracker.snapshot();
|
|
22
|
-
expect(snap.estimatedContextPercent).toBe(50);
|
|
23
|
-
expect(snap.warningLevel).toBe('moderate');
|
|
24
|
-
});
|
|
25
|
-
it('estimates context % from cost (tier 2) when no token data', () => {
|
|
26
|
-
const tracker = new ContextTracker();
|
|
27
|
-
tracker.recordResult({
|
|
28
|
-
totalCostUsd: 0.5,
|
|
29
|
-
turns: 10,
|
|
30
|
-
});
|
|
31
|
-
const snap = tracker.snapshot();
|
|
32
|
-
// 0.5 * 130_000 = 65_000 / 200_000 = 32.5%
|
|
33
|
-
expect(snap.estimatedContextPercent).toBe(33);
|
|
34
|
-
expect(snap.warningLevel).toBe('ok');
|
|
35
|
-
});
|
|
36
|
-
it('estimates context % from turns (tier 3) when no cost or tokens', () => {
|
|
37
|
-
const tracker = new ContextTracker();
|
|
38
|
-
tracker.recordResult({ turns: 20 });
|
|
39
|
-
const snap = tracker.snapshot();
|
|
40
|
-
// 20 * 6_000 = 120_000 / 200_000 = 60%
|
|
41
|
-
expect(snap.estimatedContextPercent).toBe(60);
|
|
42
|
-
expect(snap.warningLevel).toBe('moderate');
|
|
43
|
-
});
|
|
44
|
-
it('caps at 100%', () => {
|
|
45
|
-
const tracker = new ContextTracker();
|
|
46
|
-
tracker.recordResult({
|
|
47
|
-
inputTokens: 300_000,
|
|
48
|
-
contextWindowSize: 200_000,
|
|
49
|
-
});
|
|
50
|
-
expect(tracker.estimateContextPercent()).toBe(100);
|
|
51
|
-
});
|
|
52
|
-
it('returns critical warning at 85%+', () => {
|
|
53
|
-
const tracker = new ContextTracker();
|
|
54
|
-
tracker.recordResult({
|
|
55
|
-
inputTokens: 180_000,
|
|
56
|
-
contextWindowSize: 200_000,
|
|
57
|
-
});
|
|
58
|
-
expect(tracker.warningLevel()).toBe('critical');
|
|
59
|
-
});
|
|
60
|
-
it('returns high warning at 70-85%', () => {
|
|
61
|
-
const tracker = new ContextTracker();
|
|
62
|
-
tracker.recordResult({
|
|
63
|
-
inputTokens: 150_000,
|
|
64
|
-
contextWindowSize: 200_000,
|
|
65
|
-
});
|
|
66
|
-
expect(tracker.warningLevel()).toBe('high');
|
|
67
|
-
});
|
|
68
|
-
it('detects compaction from input token drop', () => {
|
|
69
|
-
const tracker = new ContextTracker();
|
|
70
|
-
tracker.recordResult({
|
|
71
|
-
inputTokens: 150_000,
|
|
72
|
-
contextWindowSize: 200_000,
|
|
73
|
-
});
|
|
74
|
-
expect(tracker.snapshot().compactionCount).toBe(0);
|
|
75
|
-
// After compaction, input tokens drop significantly
|
|
76
|
-
tracker.recordResult({
|
|
77
|
-
inputTokens: 40_000,
|
|
78
|
-
contextWindowSize: 200_000,
|
|
79
|
-
});
|
|
80
|
-
expect(tracker.snapshot().compactionCount).toBe(1);
|
|
81
|
-
expect(tracker.estimateContextPercent()).toBe(20);
|
|
82
|
-
});
|
|
83
|
-
it('tracks explicit compaction events', () => {
|
|
84
|
-
const tracker = new ContextTracker();
|
|
85
|
-
tracker.recordCompaction();
|
|
86
|
-
tracker.recordCompaction();
|
|
87
|
-
expect(tracker.snapshot().compactionCount).toBe(2);
|
|
88
|
-
});
|
|
89
|
-
it('resets all state', () => {
|
|
90
|
-
const tracker = new ContextTracker();
|
|
91
|
-
tracker.recordResult({
|
|
92
|
-
sessionId: 'ses_123',
|
|
93
|
-
inputTokens: 100_000,
|
|
94
|
-
outputTokens: 5_000,
|
|
95
|
-
contextWindowSize: 200_000,
|
|
96
|
-
turns: 10,
|
|
97
|
-
totalCostUsd: 0.5,
|
|
98
|
-
});
|
|
99
|
-
tracker.recordCompaction();
|
|
100
|
-
tracker.reset();
|
|
101
|
-
const snap = tracker.snapshot();
|
|
102
|
-
expect(snap.sessionId).toBeNull();
|
|
103
|
-
expect(snap.totalTurns).toBe(0);
|
|
104
|
-
expect(snap.totalCostUsd).toBe(0);
|
|
105
|
-
expect(snap.latestInputTokens).toBeNull();
|
|
106
|
-
expect(snap.compactionCount).toBe(0);
|
|
107
|
-
expect(snap.estimatedContextPercent).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
it('restores from persisted state', () => {
|
|
110
|
-
const tracker = new ContextTracker();
|
|
111
|
-
tracker.restore({
|
|
112
|
-
sessionId: 'ses_456',
|
|
113
|
-
totalTurns: 15,
|
|
114
|
-
totalCostUsd: 0.8,
|
|
115
|
-
estimatedContextPercent: 60,
|
|
116
|
-
contextWindowSize: 200_000,
|
|
117
|
-
latestInputTokens: 120_000,
|
|
118
|
-
});
|
|
119
|
-
const snap = tracker.snapshot();
|
|
120
|
-
expect(snap.sessionId).toBe('ses_456');
|
|
121
|
-
expect(snap.totalTurns).toBe(15);
|
|
122
|
-
expect(snap.totalCostUsd).toBe(0.8);
|
|
123
|
-
expect(snap.estimatedContextPercent).toBe(60);
|
|
124
|
-
});
|
|
125
|
-
it('accumulates session ID from results', () => {
|
|
126
|
-
const tracker = new ContextTracker();
|
|
127
|
-
tracker.recordResult({ sessionId: 'ses_abc' });
|
|
128
|
-
expect(tracker.snapshot().sessionId).toBe('ses_abc');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the session-per-team CTO model:
|
|
3
|
-
* - Each CTO session ID is its own team ID (no shared repo-global state).
|
|
4
|
-
* - Same CTO session ID recovers the same team context across restarts.
|
|
5
|
-
* - A different CTO session ID in the same worktree creates an independent team.
|
|
6
|
-
* - Engineers spawned within a CTO session resolve to that CTO's team.
|
|
7
|
-
* - CTO task permissions do not allow self-delegation.
|
|
8
|
-
*/
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
|
|
14
|
-
import { clearPluginServices, getOrCreatePluginServices, getSessionTeam, getWrapperSessionMapping, registerParentSession, } from '../src/plugin/service-factory.js';
|
|
15
|
-
import { AGENT_CTO, ENGINEER_AGENT_IDS } from '../src/plugin/agents/index.js';
|
|
16
|
-
describe('CTO chat.message — session-per-team model', () => {
|
|
17
|
-
let tempRoot;
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'cto-team-'));
|
|
20
|
-
clearPluginServices();
|
|
21
|
-
});
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
clearPluginServices();
|
|
24
|
-
if (tempRoot) {
|
|
25
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
it('CTO session ID is used directly as the team ID', async () => {
|
|
29
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
30
|
-
const chatMessage = plugin['chat.message'];
|
|
31
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-session-1' });
|
|
32
|
-
expect(getSessionTeam('cto-session-1')).toBe('cto-session-1');
|
|
33
|
-
});
|
|
34
|
-
it('same CTO session ID recovers its team context after a process restart', async () => {
|
|
35
|
-
// Phase 1: CTO session 'cto-1' runs and a team record is created on disk.
|
|
36
|
-
const plugin1 = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
37
|
-
const chatMessage1 = plugin1['chat.message'];
|
|
38
|
-
await chatMessage1({ agent: AGENT_CTO, sessionID: 'cto-1' });
|
|
39
|
-
const services1 = getOrCreatePluginServices(tempRoot);
|
|
40
|
-
await services1.orchestrator.getOrCreateTeam(tempRoot, 'cto-1'); // persist team to disk
|
|
41
|
-
expect(getSessionTeam('cto-1')).toBe('cto-1');
|
|
42
|
-
// Phase 2: Simulate a process restart (all in-memory state is lost).
|
|
43
|
-
clearPluginServices();
|
|
44
|
-
// Phase 3: The same CTO session ID resumes — it should re-register itself.
|
|
45
|
-
const plugin2 = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
46
|
-
const chatMessage2 = plugin2['chat.message'];
|
|
47
|
-
await chatMessage2({ agent: AGENT_CTO, sessionID: 'cto-1' });
|
|
48
|
-
expect(getSessionTeam('cto-1')).toBe('cto-1');
|
|
49
|
-
// The persisted team record from Phase 1 should still be accessible.
|
|
50
|
-
const services2 = getOrCreatePluginServices(tempRoot);
|
|
51
|
-
const team = await services2.orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
52
|
-
expect(team.id).toBe('cto-1');
|
|
53
|
-
});
|
|
54
|
-
it('a different CTO session ID creates an independent team, not adopting prior state', async () => {
|
|
55
|
-
// Phase 1: CTO session 'cto-1' runs.
|
|
56
|
-
const plugin1 = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
57
|
-
const chatMessage1 = plugin1['chat.message'];
|
|
58
|
-
await chatMessage1({ agent: AGENT_CTO, sessionID: 'cto-1' });
|
|
59
|
-
const services1 = getOrCreatePluginServices(tempRoot);
|
|
60
|
-
await services1.orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
61
|
-
// Phase 2: Process restart.
|
|
62
|
-
clearPluginServices();
|
|
63
|
-
// Phase 3: A brand-new CTO session 'cto-2' starts.
|
|
64
|
-
const plugin2 = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
65
|
-
const chatMessage2 = plugin2['chat.message'];
|
|
66
|
-
await chatMessage2({ agent: AGENT_CTO, sessionID: 'cto-2' });
|
|
67
|
-
// Must use its OWN session ID as team — must NOT adopt 'cto-1'.
|
|
68
|
-
expect(getSessionTeam('cto-2')).toBe('cto-2');
|
|
69
|
-
expect(getSessionTeam('cto-1')).toBeUndefined(); // cleared by restart
|
|
70
|
-
// 'cto-1' team data remains on disk, untouched.
|
|
71
|
-
const services2 = getOrCreatePluginServices(tempRoot);
|
|
72
|
-
const team1 = await services2.teamStore.getTeam(tempRoot, 'cto-1');
|
|
73
|
-
expect(team1).not.toBeNull(); // still present
|
|
74
|
-
expect(team1.id).toBe('cto-1');
|
|
75
|
-
});
|
|
76
|
-
it('multiple chat.message calls from the same CTO session do not change the active team', async () => {
|
|
77
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
78
|
-
const chatMessage = plugin['chat.message'];
|
|
79
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-session-1' });
|
|
80
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-session-1' });
|
|
81
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-session-1' });
|
|
82
|
-
expect(getSessionTeam('cto-session-1')).toBe('cto-session-1');
|
|
83
|
-
});
|
|
84
|
-
it("engineers spawned during a CTO session resolve to that CTO session's team", async () => {
|
|
85
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
86
|
-
const chatMessage = plugin['chat.message'];
|
|
87
|
-
// CTO session fires first, establishing the active team.
|
|
88
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-A' });
|
|
89
|
-
// Simulate session.created event: OpenCode fires this before chat.message for the engineer.
|
|
90
|
-
registerParentSession('wrapper-tom-1', 'cto-A');
|
|
91
|
-
// Engineer wrapper session fires (spawned by the CTO).
|
|
92
|
-
await chatMessage({ agent: ENGINEER_AGENT_IDS.Tom, sessionID: 'wrapper-tom-1' });
|
|
93
|
-
// The wrapper session must be mapped to the CTO's team, not a new orphan team.
|
|
94
|
-
const mapping = getWrapperSessionMapping(tempRoot, 'wrapper-tom-1');
|
|
95
|
-
expect(mapping).toBeDefined();
|
|
96
|
-
expect(mapping.teamId).toBe('cto-A');
|
|
97
|
-
expect(mapping.workerName).toBe('Tom');
|
|
98
|
-
});
|
|
99
|
-
it('two concurrent CTO sessions each bind their own engineers independently', async () => {
|
|
100
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
101
|
-
const chatMessage = plugin['chat.message'];
|
|
102
|
-
// Two CTO sessions start concurrently in the same worktree.
|
|
103
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-alpha' });
|
|
104
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-beta' });
|
|
105
|
-
// Simulate session.created events: each CTO spawns one engineer sub-session.
|
|
106
|
-
// The event hook normally calls registerParentSession; call it directly here.
|
|
107
|
-
registerParentSession('wrapper-tom-alpha', 'cto-alpha');
|
|
108
|
-
registerParentSession('wrapper-sara-beta', 'cto-beta');
|
|
109
|
-
// Engineer wrapper sessions check in.
|
|
110
|
-
await chatMessage({ agent: ENGINEER_AGENT_IDS.Tom, sessionID: 'wrapper-tom-alpha' });
|
|
111
|
-
await chatMessage({ agent: ENGINEER_AGENT_IDS.Sara, sessionID: 'wrapper-sara-beta' });
|
|
112
|
-
// Tom must bind to cto-alpha's team, not cto-beta's.
|
|
113
|
-
const tomMapping = getWrapperSessionMapping(tempRoot, 'wrapper-tom-alpha');
|
|
114
|
-
expect(tomMapping).toBeDefined();
|
|
115
|
-
expect(tomMapping.teamId).toBe('cto-alpha');
|
|
116
|
-
// Sara must bind to cto-beta's team, not cto-alpha's.
|
|
117
|
-
const saraMapping = getWrapperSessionMapping(tempRoot, 'wrapper-sara-beta');
|
|
118
|
-
expect(saraMapping).toBeDefined();
|
|
119
|
-
expect(saraMapping.teamId).toBe('cto-beta');
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
describe('CTO task permissions — self-delegation', () => {
|
|
123
|
-
let tempRoot;
|
|
124
|
-
beforeEach(async () => {
|
|
125
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'cto-perms-'));
|
|
126
|
-
clearPluginServices();
|
|
127
|
-
});
|
|
128
|
-
afterEach(async () => {
|
|
129
|
-
clearPluginServices();
|
|
130
|
-
if (tempRoot)
|
|
131
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
132
|
-
});
|
|
133
|
-
it('CTO task permissions deny delegation to cto', async () => {
|
|
134
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
135
|
-
const config = {};
|
|
136
|
-
await plugin.config?.(config);
|
|
137
|
-
const agents = (config.agent ?? {});
|
|
138
|
-
const cto = agents[AGENT_CTO];
|
|
139
|
-
const taskPerms = cto.permission.task;
|
|
140
|
-
// Default deny must apply, and cto must not be explicitly allowed.
|
|
141
|
-
expect(taskPerms['*']).toBe('deny');
|
|
142
|
-
expect(taskPerms['cto']).toBeUndefined();
|
|
143
|
-
expect(taskPerms['CTO']).toBeUndefined();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
describe('CTO tool isolation — concurrent sessions', () => {
|
|
147
|
-
let tempRoot;
|
|
148
|
-
beforeEach(async () => {
|
|
149
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'cto-isolation-'));
|
|
150
|
-
clearPluginServices();
|
|
151
|
-
});
|
|
152
|
-
afterEach(async () => {
|
|
153
|
-
clearPluginServices();
|
|
154
|
-
if (tempRoot)
|
|
155
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
156
|
-
});
|
|
157
|
-
it('team_status called from CTO-A uses CTO-A team even after CTO-B has chatted', async () => {
|
|
158
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
|
|
159
|
-
const chatMessage = plugin['chat.message'];
|
|
160
|
-
// CTO-A registers first, then CTO-B registers (simulating two concurrent sessions).
|
|
161
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-A' });
|
|
162
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-B' });
|
|
163
|
-
// Execute team_status as CTO-A (sessionID = 'cto-A').
|
|
164
|
-
const teamStatusTool = plugin.tool['team_status'];
|
|
165
|
-
const ctx = {
|
|
166
|
-
metadata: vi.fn(),
|
|
167
|
-
worktree: tempRoot,
|
|
168
|
-
sessionID: 'cto-A',
|
|
169
|
-
agent: AGENT_CTO,
|
|
170
|
-
abort: new AbortController().signal,
|
|
171
|
-
};
|
|
172
|
-
const result = JSON.parse(await teamStatusTool.execute({}, ctx));
|
|
173
|
-
// Must load cto-A's team, NOT cto-B's (last-write-wins global would give 'cto-B').
|
|
174
|
-
expect(result.id).toBe('cto-A');
|
|
175
|
-
});
|
|
176
|
-
it('resolves engineer team via live SDK lookup when session.created was not received', async () => {
|
|
177
|
-
// Simulate a client whose session.get returns parentID for the engineer session.
|
|
178
|
-
const mockClient = {
|
|
179
|
-
session: {
|
|
180
|
-
get: vi.fn().mockImplementation(async ({ path }) => {
|
|
181
|
-
if (path.id === 'wrapper-tom-live') {
|
|
182
|
-
return { data: { id: 'wrapper-tom-live', parentID: 'cto-live' } };
|
|
183
|
-
}
|
|
184
|
-
return { data: undefined };
|
|
185
|
-
}),
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
const plugin = await ClaudeManagerPlugin({ worktree: tempRoot, client: mockClient });
|
|
189
|
-
const chatMessage = plugin['chat.message'];
|
|
190
|
-
// CTO registers its team via chat.message.
|
|
191
|
-
await chatMessage({ agent: AGENT_CTO, sessionID: 'cto-live' });
|
|
192
|
-
// No registerParentSession call — simulates session.created arriving late or being missed.
|
|
193
|
-
// Engineer wrapper fires; resolveTeamId must fall through to live SDK lookup.
|
|
194
|
-
await chatMessage({ agent: ENGINEER_AGENT_IDS.Tom, sessionID: 'wrapper-tom-live' });
|
|
195
|
-
const mapping = getWrapperSessionMapping(tempRoot, 'wrapper-tom-live');
|
|
196
|
-
expect(mapping?.teamId).toBe('cto-live');
|
|
197
|
-
expect(mockClient.session.get).toHaveBeenCalledWith({ path: { id: 'wrapper-tom-live' } });
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|