agent-manager-concurrent-scope.test.js
7.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
* Unit Tests — 运行态坐标的**并发**安全 + registry 分支的隔离(28 号第二轮修复)
*
* 两个失败模式都不报错,所以断言全部落在「两次重叠调用算出来的东西相不相同」上:
*
* 1. `_applyRuntimeScope` 就地改写共享缓存对象:同一寻址名的两次调用只要在时间上重叠,
* 后一次会把前一次的 taskId / workDir 覆盖掉 —— 前一次此刻还卡在 callAgent 的 await 上。
* 结果是两次调用塌进同一个 cwd + 同一个 sessionKey:复用同一个长驻 CLI 子进程、
* 上下文互串,两边照样成功返回。跨公司因缓存键含 cid 躲过,同公司跨任务必中。
* 2. registry 分支在 `_applyRuntimeScope` 之前 return:companyId / taskId / companyDir /
* workDir 一个都不挂 → session key 恒为 `_global/_no-task`、cwd 退到仓库根、
* 公司语境与 memory 一行不进提示词。生产里眼下没人传 options.registry,属于埋雷。
*
* 用真实的 system/agents/ 目录(agent-locator 的根是常量,没有 env 覆盖),所以本体是
* 真的建在注册表里的:id 固定为 `zz-conc-agent`,**本体不删**(28 号裁决:测试建出来的
* agent 也是正式注册表条目)。afterAll 只清公司态目录 —— 那才是这次跑留下的运行痕迹。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
const AgentManager = require('../../src/core/agent-manager');
const { SessionPool } = require('../../src/core/session-pool');
const locator = require('../../services/agent-locator');
describe('AgentManager 运行态坐标 — 并发不串台 / registry 不绕过隔离', () => {
const AGENT_ID = 'zz-conc-agent'; // zz- 前缀 = 测试建的注册表条目,一眼可辨
const BODY_DIR = locator.bodyDir(AGENT_ID);
const CO = 'zz-conc-co';
const CO_OTHER = 'zz-conc-co-2';
const ADDR = `${CO}:${AGENT_ID}`;
const newManager = (options) => {
const m = new AgentManager({}, locator.REPO_ROOT, options);
// 日志 / hook 是 callAgent 里与本用例无关的两段 await,桩掉即可
m._resolveLogSession = async () => null;
m.hookRunner = { run: async () => {} };
return m;
};
beforeAll(() => {
// 内容每次重写,保证跨次跑不漂;三份定义写齐 = 注册表里是一份完整本体
fs.mkdirSync(BODY_DIR, { recursive: true });
fs.writeFileSync(path.join(BODY_DIR, 'agent.md'), '# 共享本体\n', 'utf-8');
fs.writeFileSync(path.join(BODY_DIR, 'soul.md'), '# zz-conc-agent 的人格\n\n单测用的常驻本体。\n', 'utf-8');
fs.writeFileSync(path.join(BODY_DIR, 'config.json'),
JSON.stringify({ id: AGENT_ID, name: AGENT_ID, type: 'skill', status: 'active' }, null, 2), 'utf-8');
});
afterAll(() => {
// 只清公司态,本体留在注册表里
for (const cid of [CO, CO_OTHER]) {
fs.rmSync(locator.companyDir(AGENT_ID, cid), { recursive: true, force: true });
}
});
it('第二次解析不能改掉第一次已经拿到的 agent 对象', () => {
const manager = newManager();
const first = manager.getAgentConfig(ADDR, { metadata: { taskId: 'task-1' } }).agent;
const firstWorkDir = first.workDir;
const second = manager.getAgentConfig(ADDR, { metadata: { taskId: 'task-2' } }).agent;
expect(first).not.toBe(second); // 两次调用不能拿到同一个对象
expect(first.workDir).toBe(firstWorkDir); // 第一次的 cwd 不被第二次冲掉
expect(first.taskId).toBe('task-1');
expect(second.taskId).toBe('task-2');
expect(second.workDir).toBe(locator.taskDir(AGENT_ID, CO, 'task-2'));
});
it('两次交错调用的 sessionKey 不同(key 撞上 = 复用同一个 CLI 子进程)', () => {
const manager = newManager();
const pool = new SessionPool();
const a = manager.getAgentConfig(ADDR, { metadata: { taskId: 'task-1' } }).agent;
const b = manager.getAgentConfig(ADDR, { metadata: { taskId: 'task-2' } }).agent;
expect(pool.getKey(ADDR, a, a.workDir)).not.toBe(pool.getKey(ADDR, b, b.workDir));
});
it('两次 callAgent 真并发:各自的 cwd / sessionKey / 提示词坐标互不干扰', async () => {
const manager = newManager();
const pool = new SessionPool();
const seen = [];
const promptAgents = [];
// 提示词也必须按**本次调用**的 agent 拼:按名字回查共享 config 拿到的是
// 「最后一次解析」的坐标,并发时属于另一个任务。
manager.buildFullPrompt = (name, task, agent) => {
promptAgents.push(agent);
return task;
};
manager._runViaProtocol = async (name, agent) => {
// 多让一个 tick:模拟 pre-task hook / logAgentStart 期间另一次调用挤进来
await new Promise((r) => setTimeout(r, 0));
seen.push({
taskId: agent.taskId,
workDir: agent.workDir,
key: pool.getKey(name, agent, agent.workDir),
});
return { agent: name, output: '', status: 'success' };
};
await Promise.all([
manager.callAgent(ADDR, 'T1', { metadata: { taskId: 'conc-1' } }),
manager.callAgent(ADDR, 'T2', { metadata: { taskId: 'conc-2' } }),
]);
const byTask = Object.fromEntries(seen.map((s) => [s.taskId, s]));
expect(Object.keys(byTask).sort()).toEqual(['conc-1', 'conc-2']);
expect(byTask['conc-1'].workDir).toBe(locator.taskDir(AGENT_ID, CO, 'conc-1'));
expect(byTask['conc-2'].workDir).toBe(locator.taskDir(AGENT_ID, CO, 'conc-2'));
expect(byTask['conc-1'].key).not.toBe(byTask['conc-2'].key);
// 每次调用拿到的坐标要和它自己的 taskId 对得上
expect(promptAgents.map((a) => a?.workDir).sort()).toEqual(
[locator.taskDir(AGENT_ID, CO, 'conc-1'), locator.taskDir(AGENT_ID, CO, 'conc-2')].sort()
);
});
it('registry 命中:companyDir / companyContextPath / workDir / taskId 照样挂上', () => {
const registryAgent = {
type: 'orchestrator',
protocol: 'claude-cli',
client: 'claude-local',
prompt: path.join(BODY_DIR, 'agent.md'),
};
const manager = newManager({
registry: { on() {}, getAgent: () => ({ agent: registryAgent, client: { url: 'local' } }) },
});
const { agent } = manager.getAgentConfig(ADDR, { metadata: { taskId: 'reg-task' } });
expect(agent.agentId).toBe(AGENT_ID);
expect(agent.companyId).toBe(CO);
expect(agent.taskId).toBe('reg-task');
expect(agent.companyDir).toBe(locator.companyDir(AGENT_ID, CO));
expect(agent.companyContextPath).toBe(path.join(locator.companyDir(AGENT_ID, CO), 'company-context.md'));
expect(agent.workDir).toBe(locator.taskDir(AGENT_ID, CO, 'reg-task'));
expect(fs.existsSync(agent.workDir)).toBe(true);
// registry 的 _agents[name] 是进程级共享缓存,只读不写
expect(registryAgent.workDir).toBeUndefined();
expect(registryAgent.taskId).toBeUndefined();
// 换一家公司必须换一个 session key(原来 registry 分支的 runtime 段恒是 `_global/_no-task`,
// 两家公司复用同一个 CLI 会话、上下文互串且不报错)
const otherAddr = `${CO_OTHER}:${AGENT_ID}`;
const other = manager.getAgentConfig(otherAddr, { metadata: { taskId: 'reg-task' } }).agent;
const pool = new SessionPool();
expect(pool.getKey(ADDR, agent, '/same')).not.toBe(pool.getKey(ADDR, other, '/same'));
});
it('registry 里自带 workDir 的配置保留自己的 cwd,但仍挂 (companyId, taskId)', () => {
const pinned = { type: 'executor', client: 'claude-local', workDir: '/tmp/pinned-agent' };
const manager = newManager({
registry: { on() {}, getAgent: () => ({ agent: pinned, client: { url: 'local' } }) },
});
const { agent } = manager.getAgentConfig(ADDR, { metadata: { taskId: 'reg-task' } });
expect(agent.workDir).toBe('/tmp/pinned-agent');
expect(agent.companyId).toBe(CO);
expect(agent.taskId).toBe('reg-task');
});
});