@ai-devkit/agent-manager 0.15.0 → 0.17.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.
- package/.swcrc +27 -0
- package/dist/AgentManager.d.ts +4 -0
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +97 -64
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +564 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -0
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js +1354 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
- package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
- package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
- package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
- package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
- package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
- package/dist/__tests__/utils/matching.test.js +324 -0
- package/dist/__tests__/utils/matching.test.js.map +1 -0
- package/dist/__tests__/utils/process.test.js +177 -0
- package/dist/__tests__/utils/process.test.js.map +1 -0
- package/dist/__tests__/utils/session.test.js +158 -0
- package/dist/__tests__/utils/session.test.js.map +1 -0
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +8 -7
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +66 -77
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +13 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +208 -132
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +44 -0
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
- package/dist/adapters/CopilotAdapter.js +314 -0
- package/dist/adapters/CopilotAdapter.js.map +1 -0
- package/dist/adapters/GeminiCliAdapter.d.ts +6 -1
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +160 -149
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +60 -69
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +62 -49
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TmuxManager.d.ts +28 -0
- package/dist/terminal/TmuxManager.d.ts.map +1 -0
- package/dist/terminal/TmuxManager.js +137 -0
- package/dist/terminal/TmuxManager.js.map +1 -0
- package/dist/terminal/TtyWriter.js +37 -16
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/index.js +1 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +35 -0
- package/dist/utils/AgentRegistry.d.ts.map +1 -0
- package/dist/utils/AgentRegistry.js +123 -0
- package/dist/utils/AgentRegistry.js.map +1 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
- package/dist/utils/ClaudeSessionParser.js +62 -93
- package/dist/utils/ClaudeSessionParser.js.map +1 -1
- package/dist/utils/agents.d.ts +15 -0
- package/dist/utils/agents.d.ts.map +1 -0
- package/dist/utils/agents.js +41 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/applescript.js +3 -6
- package/dist/utils/applescript.js.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.js +21 -29
- package/dist/utils/matching.js.map +1 -1
- package/dist/utils/process.js +77 -56
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.js +20 -34
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/sortAgents.js +6 -8
- package/dist/utils/sortAgents.js.map +1 -1
- package/package.json +11 -7
- package/src/AgentManager.ts +40 -0
- package/src/__tests__/AgentManager.test.ts +135 -0
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +3 -2
- package/src/__tests__/adapters/CodexAdapter.test.ts +199 -0
- package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +185 -1
- package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
- package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +71 -0
- package/src/adapters/AgentAdapter.ts +1 -1
- package/src/adapters/CodexAdapter.ts +155 -8
- package/src/adapters/CopilotAdapter.ts +453 -0
- package/src/adapters/GeminiCliAdapter.ts +84 -6
- package/src/adapters/index.ts +1 -0
- package/src/index.ts +7 -0
- package/src/terminal/TmuxManager.ts +118 -0
- package/src/utils/AgentRegistry.ts +139 -0
- package/src/utils/ClaudeSessionParser.ts +21 -10
- package/src/utils/agents.ts +41 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OpenCodeAdapter
|
|
3
|
+
*/ import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';
|
|
6
|
+
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
7
|
+
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
8
|
+
import { generateAgentName } from '../../utils/matching.js';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
vi.mock('../../utils/process.js', ()=>({
|
|
11
|
+
listAgentProcesses: vi.fn(),
|
|
12
|
+
enrichProcesses: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('../../utils/matching.js', ()=>({
|
|
15
|
+
generateAgentName: vi.fn(),
|
|
16
|
+
matchProcessesToSessions: vi.fn()
|
|
17
|
+
}));
|
|
18
|
+
const mockedListAgentProcesses = listAgentProcesses;
|
|
19
|
+
const mockedEnrichProcesses = enrichProcesses;
|
|
20
|
+
const mockedGenerateAgentName = generateAgentName;
|
|
21
|
+
function makeDb(queries) {
|
|
22
|
+
const prepareImpl = (sql)=>{
|
|
23
|
+
const normalized = sql.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
24
|
+
if (normalized.includes('from session')) {
|
|
25
|
+
return {
|
|
26
|
+
all: ()=>(queries.session ?? []).map((r)=>({
|
|
27
|
+
id: r.id,
|
|
28
|
+
directory: r.directory,
|
|
29
|
+
timeCreated: r.time_created
|
|
30
|
+
})),
|
|
31
|
+
get: (dir)=>{
|
|
32
|
+
const match = (queries.session ?? []).find((r)=>r.directory === dir);
|
|
33
|
+
if (!match) return undefined;
|
|
34
|
+
return {
|
|
35
|
+
id: match.id,
|
|
36
|
+
directory: match.directory,
|
|
37
|
+
time_created: match.time_created
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (normalized.includes('max(time_updated)')) {
|
|
43
|
+
return {
|
|
44
|
+
get: ()=>({
|
|
45
|
+
maxUpdated: queries.lastMessage?.timeUpdated ?? 0
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (normalized.includes('from message') && !normalized.includes("'$.time.completed'") && !normalized.includes('order by p.time_created')) {
|
|
50
|
+
return {
|
|
51
|
+
get: ()=>queries.lastMessage ?? undefined
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (normalized.includes("'$.time.completed'")) {
|
|
55
|
+
return {
|
|
56
|
+
get: ()=>queries.lastAssistant === undefined ? undefined : queries.lastAssistant ?? undefined
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (normalized.includes("json_extract(m.data, '$.role') = 'user'")) {
|
|
60
|
+
return {
|
|
61
|
+
get: ()=>queries.firstUserText === undefined ? undefined : queries.firstUserText ?? undefined
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (normalized.includes('order by p.time_created asc')) {
|
|
65
|
+
return {
|
|
66
|
+
all: ()=>queries.parts ?? []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
all: ()=>[],
|
|
71
|
+
get: ()=>undefined
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
prepare: prepareImpl,
|
|
76
|
+
close: vi.fn()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function makeDbConstructor(db) {
|
|
80
|
+
return vi.fn().mockReturnValue(db);
|
|
81
|
+
}
|
|
82
|
+
describe('OpenCodeAdapter', ()=>{
|
|
83
|
+
let adapter;
|
|
84
|
+
let tmpDir;
|
|
85
|
+
let dbPath;
|
|
86
|
+
beforeEach(()=>{
|
|
87
|
+
adapter = new OpenCodeAdapter();
|
|
88
|
+
mockedListAgentProcesses.mockReset();
|
|
89
|
+
mockedEnrichProcesses.mockReset();
|
|
90
|
+
mockedGenerateAgentName.mockReset();
|
|
91
|
+
mockedEnrichProcesses.mockImplementation((procs)=>procs);
|
|
92
|
+
mockedGenerateAgentName.mockImplementation((cwd, pid)=>{
|
|
93
|
+
const folder = path.basename(cwd) || 'unknown';
|
|
94
|
+
return `${folder}-${pid}`;
|
|
95
|
+
});
|
|
96
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-test-'));
|
|
97
|
+
dbPath = path.join(tmpDir, 'opencode.db');
|
|
98
|
+
adapter.dbPath = dbPath;
|
|
99
|
+
adapter.db = null;
|
|
100
|
+
});
|
|
101
|
+
afterEach(()=>{
|
|
102
|
+
fs.rmSync(tmpDir, {
|
|
103
|
+
recursive: true,
|
|
104
|
+
force: true
|
|
105
|
+
});
|
|
106
|
+
vi.restoreAllMocks();
|
|
107
|
+
});
|
|
108
|
+
describe('type', ()=>{
|
|
109
|
+
it('exposes opencode type', ()=>{
|
|
110
|
+
expect(adapter.type).toBe('opencode');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('canHandle', ()=>{
|
|
114
|
+
it('returns true for opencode command', ()=>{
|
|
115
|
+
expect(adapter.canHandle({
|
|
116
|
+
pid: 1,
|
|
117
|
+
command: 'opencode',
|
|
118
|
+
cwd: '/repo',
|
|
119
|
+
tty: 'ttys001'
|
|
120
|
+
})).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
it('returns true for full path opencode', ()=>{
|
|
123
|
+
expect(adapter.canHandle({
|
|
124
|
+
pid: 2,
|
|
125
|
+
command: '/usr/local/bin/opencode serve',
|
|
126
|
+
cwd: '/repo',
|
|
127
|
+
tty: 'ttys002'
|
|
128
|
+
})).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('returns true for opencode.exe with unix-style path', ()=>{
|
|
131
|
+
expect(adapter.canHandle({
|
|
132
|
+
pid: 3,
|
|
133
|
+
command: '/usr/bin/opencode.exe',
|
|
134
|
+
cwd: '/repo',
|
|
135
|
+
tty: 'ttys003'
|
|
136
|
+
})).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('returns false for non-opencode processes', ()=>{
|
|
139
|
+
expect(adapter.canHandle({
|
|
140
|
+
pid: 4,
|
|
141
|
+
command: 'node server.js',
|
|
142
|
+
cwd: '/repo',
|
|
143
|
+
tty: 'ttys004'
|
|
144
|
+
})).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it('returns false when opencode appears only in path args', ()=>{
|
|
147
|
+
expect(adapter.canHandle({
|
|
148
|
+
pid: 5,
|
|
149
|
+
command: 'node /projects/opencode-plugin/index.js',
|
|
150
|
+
cwd: '/repo',
|
|
151
|
+
tty: 'ttys005'
|
|
152
|
+
})).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('detectAgents', ()=>{
|
|
156
|
+
it('returns empty list when no opencode processes running', async ()=>{
|
|
157
|
+
mockedListAgentProcesses.mockReturnValue([]);
|
|
158
|
+
const agents = await adapter.detectAgents();
|
|
159
|
+
expect(agents).toEqual([]);
|
|
160
|
+
expect(mockedListAgentProcesses).toHaveBeenCalledWith('opencode');
|
|
161
|
+
});
|
|
162
|
+
it('returns process-only agent when DB does not exist', async ()=>{
|
|
163
|
+
const procs = [
|
|
164
|
+
{
|
|
165
|
+
pid: 100,
|
|
166
|
+
command: 'opencode',
|
|
167
|
+
cwd: '/repo',
|
|
168
|
+
tty: 'ttys001'
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
172
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
173
|
+
const agents = await adapter.detectAgents();
|
|
174
|
+
expect(agents).toHaveLength(1);
|
|
175
|
+
expect(agents[0]).toMatchObject({
|
|
176
|
+
type: 'opencode',
|
|
177
|
+
status: AgentStatus.RUNNING,
|
|
178
|
+
pid: 100,
|
|
179
|
+
projectPath: '/repo',
|
|
180
|
+
sessionId: 'pid-100',
|
|
181
|
+
summary: 'OpenCode process running'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
it('returns process-only agent when no session matches CWD', async ()=>{
|
|
185
|
+
const procs = [
|
|
186
|
+
{
|
|
187
|
+
pid: 100,
|
|
188
|
+
command: 'opencode',
|
|
189
|
+
cwd: '/repo',
|
|
190
|
+
tty: 'ttys001'
|
|
191
|
+
}
|
|
192
|
+
];
|
|
193
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
194
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
195
|
+
fs.writeFileSync(dbPath, ''); // file exists but empty → sqlite throws
|
|
196
|
+
const db = makeDb({
|
|
197
|
+
session: []
|
|
198
|
+
}); // no matching session
|
|
199
|
+
vi.doMock('better-sqlite3', ()=>makeDbConstructor(db));
|
|
200
|
+
adapter.db = db;
|
|
201
|
+
const agents = await adapter.detectAgents();
|
|
202
|
+
expect(agents).toHaveLength(1);
|
|
203
|
+
expect(agents[0]).toMatchObject({
|
|
204
|
+
type: 'opencode',
|
|
205
|
+
status: AgentStatus.RUNNING,
|
|
206
|
+
sessionId: 'pid-100'
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
it('returns waiting when assistant turn has time.completed set', async ()=>{
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const procs = [
|
|
212
|
+
{
|
|
213
|
+
pid: 200,
|
|
214
|
+
command: 'opencode',
|
|
215
|
+
cwd: '/my-project',
|
|
216
|
+
tty: 'ttys002'
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
220
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
221
|
+
const db = makeDb({
|
|
222
|
+
session: [
|
|
223
|
+
{
|
|
224
|
+
id: 'sess-001',
|
|
225
|
+
directory: '/my-project',
|
|
226
|
+
time_created: now - 60000
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
lastMessage: {
|
|
230
|
+
role: 'assistant',
|
|
231
|
+
timeUpdated: now - 60_000
|
|
232
|
+
},
|
|
233
|
+
lastAssistant: {
|
|
234
|
+
completed: now - 30_000,
|
|
235
|
+
errored: null
|
|
236
|
+
},
|
|
237
|
+
firstUserText: {
|
|
238
|
+
text: 'Refactor the auth module'
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
adapter.db = db;
|
|
242
|
+
const agents = await adapter.detectAgents();
|
|
243
|
+
expect(agents).toHaveLength(1);
|
|
244
|
+
expect(agents[0]).toMatchObject({
|
|
245
|
+
type: 'opencode',
|
|
246
|
+
status: AgentStatus.WAITING,
|
|
247
|
+
sessionId: 'sess-001',
|
|
248
|
+
summary: 'Refactor the auth module'
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
it('returns running when assistant turn has no time.completed (in-progress, any age)', async ()=>{
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const procs = [
|
|
254
|
+
{
|
|
255
|
+
pid: 250,
|
|
256
|
+
command: 'opencode',
|
|
257
|
+
cwd: '/proj',
|
|
258
|
+
tty: 'ttys004'
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
262
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
263
|
+
const db = makeDb({
|
|
264
|
+
session: [
|
|
265
|
+
{
|
|
266
|
+
id: 'sess-tool',
|
|
267
|
+
directory: '/proj',
|
|
268
|
+
time_created: now - 180_000
|
|
269
|
+
}
|
|
270
|
+
],
|
|
271
|
+
lastMessage: {
|
|
272
|
+
role: 'assistant',
|
|
273
|
+
timeUpdated: now - 120_000
|
|
274
|
+
},
|
|
275
|
+
lastAssistant: {
|
|
276
|
+
completed: null,
|
|
277
|
+
errored: null
|
|
278
|
+
},
|
|
279
|
+
firstUserText: {
|
|
280
|
+
text: 'Run the build'
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
adapter.db = db;
|
|
284
|
+
const agents = await adapter.detectAgents();
|
|
285
|
+
expect(agents[0]).toMatchObject({
|
|
286
|
+
status: AgentStatus.RUNNING,
|
|
287
|
+
sessionId: 'sess-tool'
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
it('returns running between steps (the bug fix) — no time.completed even during quiet moment', async ()=>{
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const procs = [
|
|
293
|
+
{
|
|
294
|
+
pid: 260,
|
|
295
|
+
command: 'opencode',
|
|
296
|
+
cwd: '/proj-b',
|
|
297
|
+
tty: 'ttys005'
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
301
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
302
|
+
const db = makeDb({
|
|
303
|
+
session: [
|
|
304
|
+
{
|
|
305
|
+
id: 'sess-mid',
|
|
306
|
+
directory: '/proj-b',
|
|
307
|
+
time_created: now - 60_000
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
lastMessage: {
|
|
311
|
+
role: 'assistant',
|
|
312
|
+
timeUpdated: now - 45_000
|
|
313
|
+
},
|
|
314
|
+
lastAssistant: {
|
|
315
|
+
completed: null,
|
|
316
|
+
errored: null
|
|
317
|
+
},
|
|
318
|
+
firstUserText: null
|
|
319
|
+
});
|
|
320
|
+
adapter.db = db;
|
|
321
|
+
const agents = await adapter.detectAgents();
|
|
322
|
+
expect(agents[0].status).toBe(AgentStatus.RUNNING);
|
|
323
|
+
});
|
|
324
|
+
it('returns waiting even when latest user message was metadata-updated after assistant completion', async ()=>{
|
|
325
|
+
// Regression: OpenCode updates user.message.time_updated when appending
|
|
326
|
+
// summary diffs after a turn finishes. Ordering by time_created (not
|
|
327
|
+
// time_updated) keeps the assistant message correctly identified as latest.
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
const procs = [
|
|
330
|
+
{
|
|
331
|
+
pid: 270,
|
|
332
|
+
command: 'opencode',
|
|
333
|
+
cwd: '/proj-c',
|
|
334
|
+
tty: 'ttys006'
|
|
335
|
+
}
|
|
336
|
+
];
|
|
337
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
338
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
339
|
+
// makeDb returns lastMessage from the time_created-ordered query — supply the assistant.
|
|
340
|
+
const db = makeDb({
|
|
341
|
+
session: [
|
|
342
|
+
{
|
|
343
|
+
id: 'sess-meta',
|
|
344
|
+
directory: '/proj-c',
|
|
345
|
+
time_created: now - 120_000
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
lastMessage: {
|
|
349
|
+
role: 'assistant',
|
|
350
|
+
timeUpdated: now - 30_000
|
|
351
|
+
},
|
|
352
|
+
lastAssistant: {
|
|
353
|
+
completed: now - 30_000,
|
|
354
|
+
errored: null
|
|
355
|
+
},
|
|
356
|
+
firstUserText: null
|
|
357
|
+
});
|
|
358
|
+
adapter.db = db;
|
|
359
|
+
const agents = await adapter.detectAgents();
|
|
360
|
+
expect(agents[0].status).toBe(AgentStatus.WAITING);
|
|
361
|
+
});
|
|
362
|
+
it('returns running agent when last role is user (no assistant message yet)', async ()=>{
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
const procs = [
|
|
365
|
+
{
|
|
366
|
+
pid: 300,
|
|
367
|
+
command: 'opencode',
|
|
368
|
+
cwd: '/work',
|
|
369
|
+
tty: 'ttys003'
|
|
370
|
+
}
|
|
371
|
+
];
|
|
372
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
373
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
374
|
+
const db = makeDb({
|
|
375
|
+
session: [
|
|
376
|
+
{
|
|
377
|
+
id: 'sess-002',
|
|
378
|
+
directory: '/work',
|
|
379
|
+
time_created: now - 30000
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
lastMessage: {
|
|
383
|
+
role: 'user',
|
|
384
|
+
timeUpdated: now - 30_000
|
|
385
|
+
},
|
|
386
|
+
lastAssistant: null,
|
|
387
|
+
firstUserText: {
|
|
388
|
+
text: 'Add unit tests'
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
adapter.db = db;
|
|
392
|
+
const agents = await adapter.detectAgents();
|
|
393
|
+
expect(agents[0]).toMatchObject({
|
|
394
|
+
status: AgentStatus.RUNNING,
|
|
395
|
+
sessionId: 'sess-002'
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
it('returns idle agent when last activity exceeds threshold', async ()=>{
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const staleTime = now - 10 * 60 * 1000; // 10 minutes ago
|
|
401
|
+
const procs = [
|
|
402
|
+
{
|
|
403
|
+
pid: 400,
|
|
404
|
+
command: 'opencode',
|
|
405
|
+
cwd: '/old-work',
|
|
406
|
+
tty: 'ttys004'
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
mockedListAgentProcesses.mockReturnValue(procs);
|
|
410
|
+
mockedEnrichProcesses.mockReturnValue(procs);
|
|
411
|
+
const db = makeDb({
|
|
412
|
+
session: [
|
|
413
|
+
{
|
|
414
|
+
id: 'sess-003',
|
|
415
|
+
directory: '/old-work',
|
|
416
|
+
time_created: staleTime
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
lastMessage: {
|
|
420
|
+
role: 'assistant',
|
|
421
|
+
timeUpdated: staleTime
|
|
422
|
+
},
|
|
423
|
+
lastAssistant: {
|
|
424
|
+
completed: staleTime,
|
|
425
|
+
errored: null
|
|
426
|
+
},
|
|
427
|
+
firstUserText: null
|
|
428
|
+
});
|
|
429
|
+
adapter.db = db;
|
|
430
|
+
const agents = await adapter.detectAgents();
|
|
431
|
+
expect(agents[0]).toMatchObject({
|
|
432
|
+
status: AgentStatus.IDLE,
|
|
433
|
+
sessionId: 'sess-003'
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
describe('getConversation', ()=>{
|
|
438
|
+
it('returns empty array for invalid session ref', ()=>{
|
|
439
|
+
const messages = adapter.getConversation('/no-separator-here');
|
|
440
|
+
expect(messages).toEqual([]);
|
|
441
|
+
});
|
|
442
|
+
it('returns text messages from session parts', ()=>{
|
|
443
|
+
const db = makeDb({
|
|
444
|
+
parts: [
|
|
445
|
+
{
|
|
446
|
+
role: 'user',
|
|
447
|
+
partData: JSON.stringify({
|
|
448
|
+
type: 'text',
|
|
449
|
+
text: 'Hello agent'
|
|
450
|
+
}),
|
|
451
|
+
timeCreated: 1000
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
role: 'assistant',
|
|
455
|
+
partData: JSON.stringify({
|
|
456
|
+
type: 'text',
|
|
457
|
+
text: 'Hi, how can I help?'
|
|
458
|
+
}),
|
|
459
|
+
timeCreated: 2000
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
});
|
|
463
|
+
adapter.db = db;
|
|
464
|
+
const ref = `${dbPath}::sess-abc`;
|
|
465
|
+
const messages = adapter.getConversation(ref);
|
|
466
|
+
expect(messages).toHaveLength(2);
|
|
467
|
+
expect(messages[0]).toEqual({
|
|
468
|
+
role: 'user',
|
|
469
|
+
content: 'Hello agent'
|
|
470
|
+
});
|
|
471
|
+
expect(messages[1]).toEqual({
|
|
472
|
+
role: 'assistant',
|
|
473
|
+
content: 'Hi, how can I help?'
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
it('skips reasoning parts when verbose is false', ()=>{
|
|
477
|
+
const db = makeDb({
|
|
478
|
+
parts: [
|
|
479
|
+
{
|
|
480
|
+
role: 'assistant',
|
|
481
|
+
partData: JSON.stringify({
|
|
482
|
+
type: 'reasoning',
|
|
483
|
+
reasoning: 'internal thought'
|
|
484
|
+
}),
|
|
485
|
+
timeCreated: 1000
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
role: 'assistant',
|
|
489
|
+
partData: JSON.stringify({
|
|
490
|
+
type: 'text',
|
|
491
|
+
text: 'Answer'
|
|
492
|
+
}),
|
|
493
|
+
timeCreated: 2000
|
|
494
|
+
}
|
|
495
|
+
]
|
|
496
|
+
});
|
|
497
|
+
adapter.db = db;
|
|
498
|
+
const messages = adapter.getConversation(`${dbPath}::sess-x`);
|
|
499
|
+
expect(messages).toHaveLength(1);
|
|
500
|
+
expect(messages[0].content).toBe('Answer');
|
|
501
|
+
});
|
|
502
|
+
it('includes reasoning and tool parts when verbose is true', ()=>{
|
|
503
|
+
const db = makeDb({
|
|
504
|
+
parts: [
|
|
505
|
+
{
|
|
506
|
+
role: 'assistant',
|
|
507
|
+
partData: JSON.stringify({
|
|
508
|
+
type: 'reasoning',
|
|
509
|
+
reasoning: 'my thinking'
|
|
510
|
+
}),
|
|
511
|
+
timeCreated: 1000
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
partData: JSON.stringify({
|
|
516
|
+
type: 'tool',
|
|
517
|
+
tool: 'read_file'
|
|
518
|
+
}),
|
|
519
|
+
timeCreated: 2000
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
adapter.db = db;
|
|
524
|
+
const messages = adapter.getConversation(`${dbPath}::sess-y`, {
|
|
525
|
+
verbose: true
|
|
526
|
+
});
|
|
527
|
+
expect(messages).toHaveLength(2);
|
|
528
|
+
expect(messages[0].content).toContain('my thinking');
|
|
529
|
+
expect(messages[1].content).toContain('read_file');
|
|
530
|
+
});
|
|
531
|
+
it('returns empty array when DB cannot be opened', ()=>{
|
|
532
|
+
adapter.db = null;
|
|
533
|
+
const messages = adapter.getConversation(`${dbPath}::sess-z`);
|
|
534
|
+
expect(messages).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
describe('listSessions', ()=>{
|
|
538
|
+
it('returns empty array when DB does not exist', async ()=>{
|
|
539
|
+
const sessions = await adapter.listSessions();
|
|
540
|
+
expect(sessions).toEqual([]);
|
|
541
|
+
});
|
|
542
|
+
it('returns all sessions from DB', async ()=>{
|
|
543
|
+
const now = Date.now();
|
|
544
|
+
const db = makeDb({
|
|
545
|
+
session: [
|
|
546
|
+
{
|
|
547
|
+
id: 'sess-a',
|
|
548
|
+
directory: '/proj-a',
|
|
549
|
+
time_created: now - 3000
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: 'sess-b',
|
|
553
|
+
directory: '/proj-b',
|
|
554
|
+
time_created: now - 6000
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
lastMessage: null,
|
|
558
|
+
lastAssistant: null,
|
|
559
|
+
firstUserText: {
|
|
560
|
+
text: 'Build the feature'
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
adapter.db = db;
|
|
564
|
+
const sessions = await adapter.listSessions();
|
|
565
|
+
expect(sessions).toHaveLength(2);
|
|
566
|
+
expect(sessions[0]).toMatchObject({
|
|
567
|
+
type: 'opencode',
|
|
568
|
+
sessionId: 'sess-a',
|
|
569
|
+
cwd: '/proj-a'
|
|
570
|
+
});
|
|
571
|
+
expect(sessions[1]).toMatchObject({
|
|
572
|
+
type: 'opencode',
|
|
573
|
+
sessionId: 'sess-b',
|
|
574
|
+
cwd: '/proj-b'
|
|
575
|
+
});
|
|
576
|
+
expect(sessions[0].sessionFilePath).toContain('sess-a');
|
|
577
|
+
});
|
|
578
|
+
it('filters sessions by cwd when opts.cwd is set', async ()=>{
|
|
579
|
+
const now = Date.now();
|
|
580
|
+
const db = makeDb({
|
|
581
|
+
session: [
|
|
582
|
+
{
|
|
583
|
+
id: 'sess-a',
|
|
584
|
+
directory: '/proj-a',
|
|
585
|
+
time_created: now - 3000
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 'sess-b',
|
|
589
|
+
directory: '/proj-b',
|
|
590
|
+
time_created: now - 6000
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
lastMessage: null,
|
|
594
|
+
lastAssistant: null,
|
|
595
|
+
firstUserText: null
|
|
596
|
+
});
|
|
597
|
+
adapter.db = db;
|
|
598
|
+
const sessions = await adapter.listSessions({
|
|
599
|
+
cwd: '/proj-a'
|
|
600
|
+
});
|
|
601
|
+
expect(sessions).toHaveLength(1);
|
|
602
|
+
expect(sessions[0].sessionId).toBe('sess-a');
|
|
603
|
+
});
|
|
604
|
+
it('uses session time_created as startedAt and lastActive when no parts exist', async ()=>{
|
|
605
|
+
const timeCreated = Date.now() - 120000;
|
|
606
|
+
const db = makeDb({
|
|
607
|
+
session: [
|
|
608
|
+
{
|
|
609
|
+
id: 'sess-c',
|
|
610
|
+
directory: '/repo',
|
|
611
|
+
time_created: timeCreated
|
|
612
|
+
}
|
|
613
|
+
],
|
|
614
|
+
lastMessage: null,
|
|
615
|
+
lastAssistant: null,
|
|
616
|
+
firstUserText: null
|
|
617
|
+
});
|
|
618
|
+
adapter.db = db;
|
|
619
|
+
const [session] = await adapter.listSessions();
|
|
620
|
+
expect(session.startedAt.getTime()).toBeCloseTo(timeCreated, -2);
|
|
621
|
+
expect(session.lastActive.getTime()).toBeCloseTo(timeCreated, -2);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
//# sourceMappingURL=OpenCodeAdapter.test.js.map
|