session-pool-scope-key.test.js
5.18 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
/**
* Unit Tests — SessionPool 的 key 存取一致性(28 号第三轮补检查)
*
* 这条测试兜的是一个「不报错的性能塌方」:
* `getOrCreate` 用**带 scope** 的 key 去池子里查,`create` 只要漏传 scope 就用
* **不带 scope** 的 key 存 —— 两个 key 永远对不上。查不到就再建一个,于是
* ① 池子里堆的全是查不到的死会话;② 每次调用都新起一个 CLI 子进程。
* 两个后果都不抛异常,只表现为莫名其妙的变慢 + 进程泄漏,靠人眼几乎看不出来。
* (这不是新引入的:原始版本的 getKey 就收 scope、getOrCreate 就传 scope,
* 只有 create 一路没接住,属于从第一版起就埋着的裂缝。)
*
* 另外两组断言守 28 §6 的运行态隔离:key 必须按 (companyId, taskId) 分开,
* 否则两家公司调同一个共享本体会复用同一个 CLI 会话、上下文互串且照样成功返回。
*
* 全程不碰文件系统、不起子进程:把 protocol-factory 的 'claude-cli' 换成一个
* 纯内存桩(registerProtocol 是它自带的运行时扩展口子),afterAll 原样还回去。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { EventEmitter } from 'events';
const { SessionPool } = require('../../src/core/session-pool');
const { registerProtocol } = require('../../src/core/protocol-factory');
/** 只满足 create() 用到的那几个方法,不 spawn 任何东西 */
let stubCounter = 0;
class StubProtocol extends EventEmitter {
constructor(agentInfo) {
super();
this.agentInfo = agentInfo;
this.isConnected = false;
this.sessionId = null;
}
async connect() { this.isConnected = true; }
async initialize() { }
async newSession() { this.sessionId = `stub-session-${++stubCounter}`; }
disconnect() { this.isConnected = false; }
}
// create() 只有 protocolName === 'claude-cli' 这一支不去建 launcher/transport,
// 所以桩必须挂在这个名字上。
beforeAll(() => registerProtocol('claude-cli', () => StubProtocol));
afterAll(() => registerProtocol('claude-cli', () => require('@mas/acp-core').ClaudeCliProtocol));
const CFG = { client: 'claude-local', deploy: 'local', companyId: 'zz-tenant', taskId: 'task-1' };
const WORK_DIR = '/tmp/session-pool-scope-key';
describe('SessionPool — key 存取一致性', () => {
it('用 scope 存进去的会话,用同一个 scope 取得回来(透传断链就红)', async () => {
const pool = new SessionPool();
const scope = 'conv-42';
const created = await pool.create('zz-agent', CFG, null, 'claude-cli', WORK_DIR, scope);
// ① 存的位置 = getKey(…, scope) 算出来的位置
expect(created.key).toBe(pool.getKey('zz-agent', CFG, WORK_DIR, scope));
expect(pool.sessions.get(pool.getKey('zz-agent', CFG, WORK_DIR, scope))).toBe(created);
// ② 同 scope 再取,必须命中同一个会话对象,而不是又建一个
const reused = await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR, scope);
expect(reused).toBe(created);
expect(pool.sessions.size).toBe(1);
});
it('不同 scope 各占一个槽位,互不覆盖', async () => {
const pool = new SessionPool();
const a = await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR, 'conv-a');
const b = await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR, 'conv-b');
expect(a.key).not.toBe(b.key);
expect(pool.sessions.size).toBe(2);
// 各自还能按自己的 scope 取回来(互相没把对方挤掉)
expect(await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR, 'conv-a')).toBe(a);
expect(await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR, 'conv-b')).toBe(b);
});
it('不传 scope 也不崩,且照样存取一致(生产现有三个调用方走的就是这条路)', async () => {
const pool = new SessionPool();
const created = await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR);
expect(created.key).toBe(pool.getKey('zz-agent', CFG, WORK_DIR));
expect(created.key).not.toMatch(/undefined/);
expect(await pool.getOrCreate('zz-agent', CFG, null, 'claude-cli', WORK_DIR)).toBe(created);
expect(pool.sessions.size).toBe(1);
// 不传 scope 的 key 不能和任何带 scope 的 key 撞上
expect(pool.getKey('zz-agent', CFG, WORK_DIR, 'conv-a')).not.toBe(created.key);
});
it('key 按 (companyId, taskId) 分开:换公司 / 换任务都换 key,同一对则同 key', () => {
const pool = new SessionPool();
const key = (companyId, taskId) =>
pool.getKey('zz-agent', { ...CFG, companyId, taskId }, WORK_DIR);
expect(key('co-a', 't1')).toBe(key('co-a', 't1')); // 同一对 → 同 key(会话可复用)
expect(key('co-a', 't1')).not.toBe(key('co-b', 't1')); // 换公司 → 换 key
expect(key('co-a', 't1')).not.toBe(key('co-a', 't2')); // 换任务 → 换 key
// 两段都缺省时用保留值兜底,不能塌成 undefined 拼出来的同一个 key
const bare = pool.getKey('zz-agent', { client: 'c', deploy: 'd' }, WORK_DIR);
expect(bare).toContain('_global/_no-task');
expect(bare).not.toBe(key('co-a', 't1'));
});
});