@ai-devkit/agent-manager 0.38.0 → 0.39.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/dist/__tests__/harnesses/devin/DevinAdapter.test.js +597 -0
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js +91 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js +192 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js +407 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/fixtures.js +95 -0
- package/dist/__tests__/harnesses/devin/fixtures.js.map +1 -0
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +64 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/terminal/GhosttyBackend.test.js +334 -0
- package/dist/__tests__/terminal/GhosttyBackend.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 +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.d.ts +39 -0
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAdapter.js +177 -0
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts +11 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.js +35 -0
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts +46 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.js +176 -0
- package/dist/harnesses/devin/DevinSessionLocator.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts +29 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.js +166 -0
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -0
- package/dist/harnesses/devin/readiness.d.ts +3 -0
- package/dist/harnesses/devin/readiness.d.ts.map +1 -0
- package/dist/harnesses/devin/readiness.js +39 -0
- package/dist/harnesses/devin/readiness.js.map +1 -0
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +4 -3
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/backends/ghostty.d.ts +3 -0
- package/dist/terminal/backends/ghostty.d.ts.map +1 -0
- package/dist/terminal/backends/ghostty.js +161 -0
- package/dist/terminal/backends/ghostty.js.map +1 -0
- package/dist/terminal/backends/index.d.ts.map +1 -1
- package/dist/terminal/backends/index.js +2 -0
- package/dist/terminal/backends/index.js.map +1 -1
- package/dist/terminal/backends/shared.d.ts +2 -0
- package/dist/terminal/backends/shared.d.ts.map +1 -1
- package/dist/terminal/backends/shared.js +4 -1
- package/dist/terminal/backends/shared.js.map +1 -1
- package/dist/terminal/types.d.ts +1 -0
- package/dist/terminal/types.d.ts.map +1 -1
- package/dist/terminal/types.js +1 -0
- package/dist/terminal/types.js.map +1 -1
- package/dist/utils/process.d.ts +2 -0
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +3 -0
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +292 -0
- package/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +88 -0
- package/src/__tests__/harnesses/devin/DevinSessionLocator.test.ts +139 -0
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +218 -0
- package/src/__tests__/harnesses/devin/fixtures.ts +144 -0
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +41 -0
- package/src/__tests__/terminal/GhosttyBackend.test.ts +271 -0
- package/src/adapters/AgentAdapter.ts +1 -0
- package/src/harnesses/devin/DevinAdapter.ts +224 -0
- package/src/harnesses/devin/DevinAgentMapper.ts +43 -0
- package/src/harnesses/devin/DevinSessionLocator.ts +230 -0
- package/src/harnesses/devin/DevinSessionParser.ts +236 -0
- package/src/harnesses/devin/readiness.ts +36 -0
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/terminal/TtyWriter.ts +5 -5
- package/src/terminal/backends/ghostty.ts +180 -0
- package/src/terminal/backends/index.ts +2 -0
- package/src/terminal/backends/shared.ts +6 -1
- package/src/terminal/types.ts +1 -0
- package/src/utils/process.ts +5 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { DevinSessionParser } from "../../../harnesses/devin/DevinSessionParser.js";
|
|
7
|
+
import { writeDatabase } from "./fixtures.js";
|
|
8
|
+
describe("DevinSessionParser", ()=>{
|
|
9
|
+
let tmpDir;
|
|
10
|
+
let dbPath;
|
|
11
|
+
let parser;
|
|
12
|
+
beforeEach(()=>{
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devin-parser-test-"));
|
|
14
|
+
dbPath = path.join(tmpDir, "sessions.db");
|
|
15
|
+
parser = new DevinSessionParser();
|
|
16
|
+
});
|
|
17
|
+
afterEach(()=>{
|
|
18
|
+
fs.rmSync(tmpDir, {
|
|
19
|
+
recursive: true,
|
|
20
|
+
force: true
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
function openDb() {
|
|
24
|
+
return new Database(dbPath, {
|
|
25
|
+
readonly: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
it("returns user and assistant messages in node order, skipping system", ()=>{
|
|
29
|
+
writeDatabase(dbPath, {
|
|
30
|
+
nodes: [
|
|
31
|
+
{
|
|
32
|
+
sessionId: "s",
|
|
33
|
+
nodeId: 1,
|
|
34
|
+
role: "system",
|
|
35
|
+
content: "preamble"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
sessionId: "s",
|
|
39
|
+
nodeId: 2,
|
|
40
|
+
role: "user",
|
|
41
|
+
content: "hello",
|
|
42
|
+
isUserInput: true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
sessionId: "s",
|
|
46
|
+
nodeId: 3,
|
|
47
|
+
role: "assistant",
|
|
48
|
+
content: "hi"
|
|
49
|
+
},
|
|
50
|
+
// Later turn roots a fresh preamble chain:
|
|
51
|
+
{
|
|
52
|
+
sessionId: "s",
|
|
53
|
+
nodeId: 4,
|
|
54
|
+
role: "system",
|
|
55
|
+
content: "more preamble"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
sessionId: "s",
|
|
59
|
+
nodeId: 5,
|
|
60
|
+
role: "user",
|
|
61
|
+
content: "again",
|
|
62
|
+
isUserInput: true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
sessionId: "s",
|
|
66
|
+
nodeId: 6,
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: "done"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
const db = openDb();
|
|
73
|
+
try {
|
|
74
|
+
expect(parser.getConversation(db, "s")).toEqual([
|
|
75
|
+
{
|
|
76
|
+
role: "user",
|
|
77
|
+
content: "hello"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: "hi"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
role: "user",
|
|
85
|
+
content: "again"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
role: "assistant",
|
|
89
|
+
content: "done"
|
|
90
|
+
}
|
|
91
|
+
]);
|
|
92
|
+
} finally{
|
|
93
|
+
db.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it("dedupes sibling revisions by message_id keeping the highest node_id", ()=>{
|
|
97
|
+
writeDatabase(dbPath, {
|
|
98
|
+
nodes: [
|
|
99
|
+
{
|
|
100
|
+
sessionId: "s",
|
|
101
|
+
nodeId: 1,
|
|
102
|
+
role: "assistant",
|
|
103
|
+
content: "draft",
|
|
104
|
+
messageId: "m1"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
sessionId: "s",
|
|
108
|
+
nodeId: 2,
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: "final",
|
|
111
|
+
messageId: "m1"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
const db = openDb();
|
|
116
|
+
try {
|
|
117
|
+
expect(parser.getConversation(db, "s")).toEqual([
|
|
118
|
+
{
|
|
119
|
+
role: "assistant",
|
|
120
|
+
content: "final"
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
} finally{
|
|
124
|
+
db.close();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
it("skips non-input user nodes and empty content", ()=>{
|
|
128
|
+
writeDatabase(dbPath, {
|
|
129
|
+
nodes: [
|
|
130
|
+
{
|
|
131
|
+
sessionId: "s",
|
|
132
|
+
nodeId: 1,
|
|
133
|
+
role: "user",
|
|
134
|
+
content: "wrapper",
|
|
135
|
+
isUserInput: false
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
sessionId: "s",
|
|
139
|
+
nodeId: 2,
|
|
140
|
+
role: "user",
|
|
141
|
+
content: " ",
|
|
142
|
+
isUserInput: true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
sessionId: "s",
|
|
146
|
+
nodeId: 3,
|
|
147
|
+
role: "user",
|
|
148
|
+
content: "real",
|
|
149
|
+
isUserInput: true
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
const db = openDb();
|
|
154
|
+
try {
|
|
155
|
+
expect(parser.getConversation(db, "s")).toEqual([
|
|
156
|
+
{
|
|
157
|
+
role: "user",
|
|
158
|
+
content: "real"
|
|
159
|
+
}
|
|
160
|
+
]);
|
|
161
|
+
} finally{
|
|
162
|
+
db.close();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
it("includes tools and thinking only in verbose mode", ()=>{
|
|
166
|
+
writeDatabase(dbPath, {
|
|
167
|
+
nodes: [
|
|
168
|
+
{
|
|
169
|
+
sessionId: "s",
|
|
170
|
+
nodeId: 1,
|
|
171
|
+
role: "assistant",
|
|
172
|
+
content: "working",
|
|
173
|
+
thinking: "hmm",
|
|
174
|
+
toolCalls: [
|
|
175
|
+
{
|
|
176
|
+
function: {
|
|
177
|
+
name: "exec"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
sessionId: "s",
|
|
184
|
+
nodeId: 2,
|
|
185
|
+
role: "tool",
|
|
186
|
+
content: "tool output",
|
|
187
|
+
name: "exec"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
const db = openDb();
|
|
192
|
+
try {
|
|
193
|
+
expect(parser.getConversation(db, "s")).toEqual([
|
|
194
|
+
{
|
|
195
|
+
role: "assistant",
|
|
196
|
+
content: "working"
|
|
197
|
+
}
|
|
198
|
+
]);
|
|
199
|
+
expect(parser.getConversation(db, "s", {
|
|
200
|
+
verbose: true
|
|
201
|
+
})).toEqual([
|
|
202
|
+
{
|
|
203
|
+
role: "assistant",
|
|
204
|
+
content: "[thinking] hmm"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
role: "assistant",
|
|
208
|
+
content: "working"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
role: "assistant",
|
|
212
|
+
content: "[tool: exec]"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
role: "assistant",
|
|
216
|
+
content: "[tool: exec] tool output"
|
|
217
|
+
}
|
|
218
|
+
]);
|
|
219
|
+
} finally{
|
|
220
|
+
db.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
it("honors tail and bounds the query window", ()=>{
|
|
224
|
+
writeDatabase(dbPath, {
|
|
225
|
+
nodes: Array.from({
|
|
226
|
+
length: 30
|
|
227
|
+
}, (_, i)=>({
|
|
228
|
+
sessionId: "s",
|
|
229
|
+
nodeId: i + 1,
|
|
230
|
+
role: "user",
|
|
231
|
+
content: `msg-${i + 1}`,
|
|
232
|
+
isUserInput: true
|
|
233
|
+
}))
|
|
234
|
+
});
|
|
235
|
+
const db = openDb();
|
|
236
|
+
try {
|
|
237
|
+
const convo = parser.getConversation(db, "s", {
|
|
238
|
+
tail: 3
|
|
239
|
+
});
|
|
240
|
+
expect(convo).toEqual([
|
|
241
|
+
{
|
|
242
|
+
role: "user",
|
|
243
|
+
content: "msg-28"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: "msg-29"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
role: "user",
|
|
251
|
+
content: "msg-30"
|
|
252
|
+
}
|
|
253
|
+
]);
|
|
254
|
+
} finally{
|
|
255
|
+
db.close();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
it("skips malformed chat_message rows and missing sessions", ()=>{
|
|
259
|
+
writeDatabase(dbPath, {
|
|
260
|
+
nodes: [
|
|
261
|
+
{
|
|
262
|
+
sessionId: "s",
|
|
263
|
+
nodeId: 1,
|
|
264
|
+
raw: "{not json"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
sessionId: "s",
|
|
268
|
+
nodeId: 2,
|
|
269
|
+
role: "assistant",
|
|
270
|
+
content: "ok"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
});
|
|
274
|
+
const db = openDb();
|
|
275
|
+
try {
|
|
276
|
+
expect(parser.getConversation(db, "s")).toEqual([
|
|
277
|
+
{
|
|
278
|
+
role: "assistant",
|
|
279
|
+
content: "ok"
|
|
280
|
+
}
|
|
281
|
+
]);
|
|
282
|
+
expect(parser.getConversation(db, "missing")).toEqual([]);
|
|
283
|
+
} finally{
|
|
284
|
+
db.close();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
it("reports session stats from the frontier node and latest prompt", ()=>{
|
|
288
|
+
writeDatabase(dbPath, {
|
|
289
|
+
nodes: [
|
|
290
|
+
{
|
|
291
|
+
sessionId: "s",
|
|
292
|
+
nodeId: 1,
|
|
293
|
+
role: "user",
|
|
294
|
+
content: "go",
|
|
295
|
+
createdAt: 100
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
sessionId: "s",
|
|
299
|
+
nodeId: 2,
|
|
300
|
+
role: "assistant",
|
|
301
|
+
content: "done",
|
|
302
|
+
createdAt: 200
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
prompts: [
|
|
306
|
+
{
|
|
307
|
+
sessionId: "s",
|
|
308
|
+
content: "first task",
|
|
309
|
+
timestamp: 100
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
sessionId: "s",
|
|
313
|
+
content: "/mcp",
|
|
314
|
+
timestamp: 300
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
sessionId: "s",
|
|
318
|
+
content: "latest task",
|
|
319
|
+
timestamp: 200
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
sessionId: "s",
|
|
323
|
+
content: "!shell",
|
|
324
|
+
timestamp: 400,
|
|
325
|
+
isShell: 1
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
const db = openDb();
|
|
330
|
+
try {
|
|
331
|
+
expect(parser.getSessionStats(db, "s")).toEqual({
|
|
332
|
+
lastRole: "assistant",
|
|
333
|
+
lastTimeUpdated: 200_000,
|
|
334
|
+
summary: "latest task"
|
|
335
|
+
});
|
|
336
|
+
expect(parser.getSessionStats(db, "missing")).toEqual({
|
|
337
|
+
lastRole: null,
|
|
338
|
+
lastTimeUpdated: 0,
|
|
339
|
+
summary: ""
|
|
340
|
+
});
|
|
341
|
+
} finally{
|
|
342
|
+
db.close();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
it("falls back to the first is_user_input node for the summary", ()=>{
|
|
346
|
+
writeDatabase(dbPath, {
|
|
347
|
+
nodes: [
|
|
348
|
+
{
|
|
349
|
+
sessionId: "s",
|
|
350
|
+
nodeId: 1,
|
|
351
|
+
role: "user",
|
|
352
|
+
content: "wrapper",
|
|
353
|
+
isUserInput: false
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
sessionId: "s",
|
|
357
|
+
nodeId: 2,
|
|
358
|
+
role: "user",
|
|
359
|
+
content: "real first",
|
|
360
|
+
isUserInput: true
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
const db = openDb();
|
|
365
|
+
try {
|
|
366
|
+
expect(parser.getFirstUserMessage(db, "s")).toBe("real first");
|
|
367
|
+
} finally{
|
|
368
|
+
db.close();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
it("falls back to the last is_user_input node for the working-on summary", ()=>{
|
|
372
|
+
writeDatabase(dbPath, {
|
|
373
|
+
nodes: [
|
|
374
|
+
{
|
|
375
|
+
sessionId: "s",
|
|
376
|
+
nodeId: 1,
|
|
377
|
+
role: "user",
|
|
378
|
+
content: "first",
|
|
379
|
+
isUserInput: true
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
sessionId: "s",
|
|
383
|
+
nodeId: 2,
|
|
384
|
+
role: "user",
|
|
385
|
+
content: "wrapper",
|
|
386
|
+
isUserInput: false
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
sessionId: "s",
|
|
390
|
+
nodeId: 3,
|
|
391
|
+
role: "user",
|
|
392
|
+
content: "last real",
|
|
393
|
+
isUserInput: true
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
});
|
|
397
|
+
const db = openDb();
|
|
398
|
+
try {
|
|
399
|
+
expect(parser.getLastUserPrompt(db, "s")).toBe("last real");
|
|
400
|
+
expect(parser.getLastUserPrompt(db, "missing")).toBe("");
|
|
401
|
+
} finally{
|
|
402
|
+
db.close();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
//# sourceMappingURL=DevinSessionParser.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/harnesses/devin/DevinSessionParser.test.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { DevinSessionParser } from \"../../../harnesses/devin/DevinSessionParser.js\";\nimport { writeDatabase } from \"./fixtures.js\";\n\ndescribe(\"DevinSessionParser\", () => {\n let tmpDir: string;\n let dbPath: string;\n let parser: DevinSessionParser;\n\n beforeEach(() => {\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"devin-parser-test-\"));\n dbPath = path.join(tmpDir, \"sessions.db\");\n parser = new DevinSessionParser();\n });\n\n afterEach(() => {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n });\n\n function openDb(): Database.Database {\n return new Database(dbPath, { readonly: true });\n }\n\n it(\"returns user and assistant messages in node order, skipping system\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"system\", content: \"preamble\" },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"hello\", isUserInput: true },\n { sessionId: \"s\", nodeId: 3, role: \"assistant\", content: \"hi\" },\n // Later turn roots a fresh preamble chain:\n { sessionId: \"s\", nodeId: 4, role: \"system\", content: \"more preamble\" },\n { sessionId: \"s\", nodeId: 5, role: \"user\", content: \"again\", isUserInput: true },\n { sessionId: \"s\", nodeId: 6, role: \"assistant\", content: \"done\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"user\", content: \"hello\" },\n { role: \"assistant\", content: \"hi\" },\n { role: \"user\", content: \"again\" },\n { role: \"assistant\", content: \"done\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"dedupes sibling revisions by message_id keeping the highest node_id\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"assistant\", content: \"draft\", messageId: \"m1\" },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"final\", messageId: \"m1\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"final\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"skips non-input user nodes and empty content\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \" \", isUserInput: true },\n { sessionId: \"s\", nodeId: 3, role: \"user\", content: \"real\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([{ role: \"user\", content: \"real\" }]);\n } finally {\n db.close();\n }\n });\n\n it(\"includes tools and thinking only in verbose mode\", () => {\n writeDatabase(dbPath, {\n nodes: [\n {\n sessionId: \"s\",\n nodeId: 1,\n role: \"assistant\",\n content: \"working\",\n thinking: \"hmm\",\n toolCalls: [{ function: { name: \"exec\" } }],\n },\n { sessionId: \"s\", nodeId: 2, role: \"tool\", content: \"tool output\", name: \"exec\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"working\" },\n ]);\n expect(parser.getConversation(db, \"s\", { verbose: true })).toEqual([\n { role: \"assistant\", content: \"[thinking] hmm\" },\n { role: \"assistant\", content: \"working\" },\n { role: \"assistant\", content: \"[tool: exec]\" },\n { role: \"assistant\", content: \"[tool: exec] tool output\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"honors tail and bounds the query window\", () => {\n writeDatabase(dbPath, {\n nodes: Array.from({ length: 30 }, (_, i) => ({\n sessionId: \"s\",\n nodeId: i + 1,\n role: \"user\" as const,\n content: `msg-${i + 1}`,\n isUserInput: true,\n })),\n });\n const db = openDb();\n try {\n const convo = parser.getConversation(db, \"s\", { tail: 3 });\n expect(convo).toEqual([\n { role: \"user\", content: \"msg-28\" },\n { role: \"user\", content: \"msg-29\" },\n { role: \"user\", content: \"msg-30\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"skips malformed chat_message rows and missing sessions\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, raw: \"{not json\" },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"ok\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"ok\" },\n ]);\n expect(parser.getConversation(db, \"missing\")).toEqual([]);\n } finally {\n db.close();\n }\n });\n\n it(\"reports session stats from the frontier node and latest prompt\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"go\", createdAt: 100 },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"done\", createdAt: 200 },\n ],\n prompts: [\n { sessionId: \"s\", content: \"first task\", timestamp: 100 },\n { sessionId: \"s\", content: \"/mcp\", timestamp: 300 },\n { sessionId: \"s\", content: \"latest task\", timestamp: 200 },\n { sessionId: \"s\", content: \"!shell\", timestamp: 400, isShell: 1 },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getSessionStats(db, \"s\")).toEqual({\n lastRole: \"assistant\",\n lastTimeUpdated: 200_000,\n summary: \"latest task\",\n });\n expect(parser.getSessionStats(db, \"missing\")).toEqual({\n lastRole: null,\n lastTimeUpdated: 0,\n summary: \"\",\n });\n } finally {\n db.close();\n }\n });\n\n it(\"falls back to the first is_user_input node for the summary\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"real first\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getFirstUserMessage(db, \"s\")).toBe(\"real first\");\n } finally {\n db.close();\n }\n });\n\n it(\"falls back to the last is_user_input node for the working-on summary\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"first\", isUserInput: true },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 3, role: \"user\", content: \"last real\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getLastUserPrompt(db, \"s\")).toBe(\"last real\");\n expect(parser.getLastUserPrompt(db, \"missing\")).toBe(\"\");\n } finally {\n db.close();\n }\n });\n});\n"],"names":["Database","fs","os","path","afterEach","beforeEach","describe","expect","it","DevinSessionParser","writeDatabase","tmpDir","dbPath","parser","mkdtempSync","join","tmpdir","rmSync","recursive","force","openDb","readonly","nodes","sessionId","nodeId","role","content","isUserInput","db","getConversation","toEqual","close","messageId","thinking","toolCalls","function","name","verbose","Array","from","length","_","i","convo","tail","raw","createdAt","prompts","timestamp","isShell","getSessionStats","lastRole","lastTimeUpdated","summary","getFirstUserMessage","toBe","getLastUserPrompt"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
export function writeDatabase(dbPath, data) {
|
|
5
|
+
fs.mkdirSync(path.dirname(dbPath), {
|
|
6
|
+
recursive: true
|
|
7
|
+
});
|
|
8
|
+
const db = new Database(dbPath);
|
|
9
|
+
try {
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
working_directory TEXT NOT NULL,
|
|
14
|
+
backend_type TEXT NOT NULL DEFAULT 'local',
|
|
15
|
+
model TEXT NOT NULL DEFAULT 'test-model',
|
|
16
|
+
agent_mode TEXT NOT NULL DEFAULT 'normal',
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
last_activity_at INTEGER NOT NULL,
|
|
19
|
+
title TEXT,
|
|
20
|
+
main_chain_id INTEGER,
|
|
21
|
+
hidden INTEGER NOT NULL DEFAULT 0
|
|
22
|
+
);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS message_nodes (
|
|
24
|
+
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
session_id TEXT NOT NULL,
|
|
26
|
+
node_id INTEGER NOT NULL,
|
|
27
|
+
parent_node_id INTEGER,
|
|
28
|
+
chat_message TEXT NOT NULL,
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
UNIQUE(session_id, node_id)
|
|
31
|
+
);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS prompt_history (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
content TEXT NOT NULL,
|
|
35
|
+
timestamp INTEGER NOT NULL,
|
|
36
|
+
session_id TEXT NOT NULL,
|
|
37
|
+
is_shell INTEGER NOT NULL DEFAULT 0
|
|
38
|
+
);
|
|
39
|
+
`);
|
|
40
|
+
const insertSession = db.prepare(`
|
|
41
|
+
INSERT INTO sessions (id, working_directory, created_at, last_activity_at, title, main_chain_id, hidden)
|
|
42
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
43
|
+
`);
|
|
44
|
+
for (const session of data.sessions ?? []){
|
|
45
|
+
insertSession.run(session.id, session.directory, session.timeCreated, session.lastActivityAt ?? session.timeCreated, session.title ?? null, session.mainChainId ?? null, session.hidden ?? 0);
|
|
46
|
+
}
|
|
47
|
+
const insertNode = db.prepare(`
|
|
48
|
+
INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?)
|
|
50
|
+
`);
|
|
51
|
+
for (const node of data.nodes ?? []){
|
|
52
|
+
const chatMessage = node.raw ?? JSON.stringify({
|
|
53
|
+
message_id: node.messageId ?? `msg-${node.nodeId}`,
|
|
54
|
+
role: node.role,
|
|
55
|
+
content: node.content ?? "",
|
|
56
|
+
...node.toolCalls ? {
|
|
57
|
+
tool_calls: node.toolCalls
|
|
58
|
+
} : {},
|
|
59
|
+
...node.thinking ? {
|
|
60
|
+
thinking: {
|
|
61
|
+
thinking: node.thinking
|
|
62
|
+
}
|
|
63
|
+
} : {},
|
|
64
|
+
...node.name ? {
|
|
65
|
+
name: node.name
|
|
66
|
+
} : {},
|
|
67
|
+
...node.isUserInput !== undefined ? {
|
|
68
|
+
metadata: {
|
|
69
|
+
is_user_input: node.isUserInput
|
|
70
|
+
}
|
|
71
|
+
} : {}
|
|
72
|
+
});
|
|
73
|
+
insertNode.run(node.sessionId, node.nodeId, node.parentNodeId ?? null, chatMessage, node.createdAt ?? 1000 + node.nodeId);
|
|
74
|
+
}
|
|
75
|
+
const insertPrompt = db.prepare(`
|
|
76
|
+
INSERT INTO prompt_history (session_id, content, timestamp, is_shell)
|
|
77
|
+
VALUES (?, ?, ?, ?)
|
|
78
|
+
`);
|
|
79
|
+
for (const prompt of data.prompts ?? []){
|
|
80
|
+
insertPrompt.run(prompt.sessionId, prompt.content, prompt.timestamp ?? 1000, prompt.isShell ?? 0);
|
|
81
|
+
}
|
|
82
|
+
} finally{
|
|
83
|
+
db.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function writeLocks(locksDir, locks) {
|
|
87
|
+
fs.mkdirSync(locksDir, {
|
|
88
|
+
recursive: true
|
|
89
|
+
});
|
|
90
|
+
for (const [sessionId, pid] of Object.entries(locks)){
|
|
91
|
+
fs.writeFileSync(path.join(locksDir, `${sessionId}.lock`), String(pid));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/harnesses/devin/fixtures.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nexport interface DevinSessionInput {\n id: string;\n directory: string;\n title?: string | null;\n timeCreated: number;\n lastActivityAt?: number;\n mainChainId?: number | null;\n hidden?: number;\n}\n\nexport interface DevinNodeInput {\n nodeId: number;\n parentNodeId?: number | null;\n role: string;\n content?: string;\n messageId?: string;\n createdAt?: number;\n isUserInput?: boolean;\n toolCalls?: { name?: string; function?: { name?: string } }[];\n thinking?: string;\n name?: string;\n /** Raw chat_message text; when set, other fields are ignored. */\n raw?: string;\n}\n\nexport interface DevinPromptInput {\n sessionId: string;\n content: string;\n timestamp?: number;\n isShell?: number;\n}\n\nexport function writeDatabase(\n dbPath: string,\n data: {\n sessions?: DevinSessionInput[];\n nodes?: (DevinNodeInput & { sessionId: string })[];\n prompts?: DevinPromptInput[];\n },\n): void {\n fs.mkdirSync(path.dirname(dbPath), { recursive: true });\n const db = new Database(dbPath);\n try {\n db.exec(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n working_directory TEXT NOT NULL,\n backend_type TEXT NOT NULL DEFAULT 'local',\n model TEXT NOT NULL DEFAULT 'test-model',\n agent_mode TEXT NOT NULL DEFAULT 'normal',\n created_at INTEGER NOT NULL,\n last_activity_at INTEGER NOT NULL,\n title TEXT,\n main_chain_id INTEGER,\n hidden INTEGER NOT NULL DEFAULT 0\n );\n CREATE TABLE IF NOT EXISTS message_nodes (\n row_id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n node_id INTEGER NOT NULL,\n parent_node_id INTEGER,\n chat_message TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE(session_id, node_id)\n );\n CREATE TABLE IF NOT EXISTS prompt_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n content TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n is_shell INTEGER NOT NULL DEFAULT 0\n );\n `);\n\n const insertSession = db.prepare(`\n INSERT INTO sessions (id, working_directory, created_at, last_activity_at, title, main_chain_id, hidden)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n for (const session of data.sessions ?? []) {\n insertSession.run(\n session.id,\n session.directory,\n session.timeCreated,\n session.lastActivityAt ?? session.timeCreated,\n session.title ?? null,\n session.mainChainId ?? null,\n session.hidden ?? 0,\n );\n }\n\n const insertNode = db.prepare(`\n INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const node of data.nodes ?? []) {\n const chatMessage =\n node.raw ??\n JSON.stringify({\n message_id: node.messageId ?? `msg-${node.nodeId}`,\n role: node.role,\n content: node.content ?? \"\",\n ...(node.toolCalls ? { tool_calls: node.toolCalls } : {}),\n ...(node.thinking ? { thinking: { thinking: node.thinking } } : {}),\n ...(node.name ? { name: node.name } : {}),\n ...(node.isUserInput !== undefined\n ? { metadata: { is_user_input: node.isUserInput } }\n : {}),\n });\n insertNode.run(\n node.sessionId,\n node.nodeId,\n node.parentNodeId ?? null,\n chatMessage,\n node.createdAt ?? 1000 + node.nodeId,\n );\n }\n\n const insertPrompt = db.prepare(`\n INSERT INTO prompt_history (session_id, content, timestamp, is_shell)\n VALUES (?, ?, ?, ?)\n `);\n for (const prompt of data.prompts ?? []) {\n insertPrompt.run(\n prompt.sessionId,\n prompt.content,\n prompt.timestamp ?? 1000,\n prompt.isShell ?? 0,\n );\n }\n } finally {\n db.close();\n }\n}\n\nexport function writeLocks(locksDir: string, locks: Record<string, number | string>): void {\n fs.mkdirSync(locksDir, { recursive: true });\n for (const [sessionId, pid] of Object.entries(locks)) {\n fs.writeFileSync(path.join(locksDir, `${sessionId}.lock`), String(pid));\n }\n}\n"],"names":["Database","fs","path","writeDatabase","dbPath","data","mkdirSync","dirname","recursive","db","exec","insertSession","prepare","session","sessions","run","id","directory","timeCreated","lastActivityAt","title","mainChainId","hidden","insertNode","node","nodes","chatMessage","raw","JSON","stringify","message_id","messageId","nodeId","role","content","toolCalls","tool_calls","thinking","name","isUserInput","undefined","metadata","is_user_input","sessionId","parentNodeId","createdAt","insertPrompt","prompt","prompts","timestamp","isShell","close","writeLocks","locksDir","locks","pid","Object","entries","writeFileSync","join","String"],"mappings":"AAAA,OAAOA,cAAc,iBAAiB;AACtC,YAAYC,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAkC7B,OAAO,SAASC,cACdC,MAAc,EACdC,IAIC;IAEDJ,GAAGK,SAAS,CAACJ,KAAKK,OAAO,CAACH,SAAS;QAAEI,WAAW;IAAK;IACrD,MAAMC,KAAK,IAAIT,SAASI;IACxB,IAAI;QACFK,GAAGC,IAAI,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6BT,CAAC;QAED,MAAMC,gBAAgBF,GAAGG,OAAO,CAAC,CAAC;;;IAGlC,CAAC;QACD,KAAK,MAAMC,WAAWR,KAAKS,QAAQ,IAAI,EAAE,CAAE;YACzCH,cAAcI,GAAG,CACfF,QAAQG,EAAE,EACVH,QAAQI,SAAS,EACjBJ,QAAQK,WAAW,EACnBL,QAAQM,cAAc,IAAIN,QAAQK,WAAW,EAC7CL,QAAQO,KAAK,IAAI,MACjBP,QAAQQ,WAAW,IAAI,MACvBR,QAAQS,MAAM,IAAI;QAEtB;QAEA,MAAMC,aAAad,GAAGG,OAAO,CAAC,CAAC;;;IAG/B,CAAC;QACD,KAAK,MAAMY,QAAQnB,KAAKoB,KAAK,IAAI,EAAE,CAAE;YACnC,MAAMC,cACJF,KAAKG,GAAG,IACRC,KAAKC,SAAS,CAAC;gBACbC,YAAYN,KAAKO,SAAS,IAAI,CAAC,IAAI,EAAEP,KAAKQ,MAAM,EAAE;gBAClDC,MAAMT,KAAKS,IAAI;gBACfC,SAASV,KAAKU,OAAO,IAAI;gBACzB,GAAIV,KAAKW,SAAS,GAAG;oBAAEC,YAAYZ,KAAKW,SAAS;gBAAC,IAAI,CAAC,CAAC;gBACxD,GAAIX,KAAKa,QAAQ,GAAG;oBAAEA,UAAU;wBAAEA,UAAUb,KAAKa,QAAQ;oBAAC;gBAAE,IAAI,CAAC,CAAC;gBAClE,GAAIb,KAAKc,IAAI,GAAG;oBAAEA,MAAMd,KAAKc,IAAI;gBAAC,IAAI,CAAC,CAAC;gBACxC,GAAId,KAAKe,WAAW,KAAKC,YACrB;oBAAEC,UAAU;wBAAEC,eAAelB,KAAKe,WAAW;oBAAC;gBAAE,IAChD,CAAC,CAAC;YACR;YACFhB,WAAWR,GAAG,CACZS,KAAKmB,SAAS,EACdnB,KAAKQ,MAAM,EACXR,KAAKoB,YAAY,IAAI,MACrBlB,aACAF,KAAKqB,SAAS,IAAI,OAAOrB,KAAKQ,MAAM;QAExC;QAEA,MAAMc,eAAerC,GAAGG,OAAO,CAAC,CAAC;;;IAGjC,CAAC;QACD,KAAK,MAAMmC,UAAU1C,KAAK2C,OAAO,IAAI,EAAE,CAAE;YACvCF,aAAa/B,GAAG,CACdgC,OAAOJ,SAAS,EAChBI,OAAOb,OAAO,EACda,OAAOE,SAAS,IAAI,MACpBF,OAAOG,OAAO,IAAI;QAEtB;IACF,SAAU;QACRzC,GAAG0C,KAAK;IACV;AACF;AAEA,OAAO,SAASC,WAAWC,QAAgB,EAAEC,KAAsC;IACjFrD,GAAGK,SAAS,CAAC+C,UAAU;QAAE7C,WAAW;IAAK;IACzC,KAAK,MAAM,CAACmC,WAAWY,IAAI,IAAIC,OAAOC,OAAO,CAACH,OAAQ;QACpDrD,GAAGyD,aAAa,CAACxD,KAAKyD,IAAI,CAACN,UAAU,GAAGV,UAAU,KAAK,CAAC,GAAGiB,OAAOL;IACpE;AACF"}
|
|
@@ -82,6 +82,7 @@ function fixture(overrides = {}) {
|
|
|
82
82
|
".kiro",
|
|
83
83
|
".gemini/antigravity-cli",
|
|
84
84
|
".config/opencode",
|
|
85
|
+
".config/devin",
|
|
85
86
|
".pi"
|
|
86
87
|
]){
|
|
87
88
|
files[path.join(homeDir, directory)] = "<dir>";
|
|
@@ -125,6 +126,10 @@ function fixture(overrides = {}) {
|
|
|
125
126
|
stdout: "● litellm api\n● OpenAI oauth\n",
|
|
126
127
|
stderr: ""
|
|
127
128
|
};
|
|
129
|
+
if (command === "devin") return {
|
|
130
|
+
stdout: "Logged in (via Devin).\n\nUser:\n Name: test-user\n",
|
|
131
|
+
stderr: ""
|
|
132
|
+
};
|
|
128
133
|
throw new Error(`unexpected command ${command}`);
|
|
129
134
|
}),
|
|
130
135
|
codexAuth: async ()=>true,
|
|
@@ -148,7 +153,8 @@ describe("agent readiness", ()=>{
|
|
|
148
153
|
"antigravity_cli",
|
|
149
154
|
"opencode",
|
|
150
155
|
"copilot",
|
|
151
|
-
"pi"
|
|
156
|
+
"pi",
|
|
157
|
+
"devin"
|
|
152
158
|
]);
|
|
153
159
|
for (const [agent, report] of Object.entries(reports)){
|
|
154
160
|
expect(report.type).toBe(agent);
|
|
@@ -337,6 +343,63 @@ describe("agent readiness", ()=>{
|
|
|
337
343
|
status: "pass"
|
|
338
344
|
});
|
|
339
345
|
});
|
|
346
|
+
it("checks Devin auth from devin auth status output", async ()=>{
|
|
347
|
+
const { options } = fixture({
|
|
348
|
+
runCommand: vi.fn(async (command)=>{
|
|
349
|
+
if (command === "devin") return {
|
|
350
|
+
stdout: "Logged in (via Devin).\n\nUser:\n Name: test-user\n",
|
|
351
|
+
stderr: ""
|
|
352
|
+
};
|
|
353
|
+
if (command === "pi") return {
|
|
354
|
+
stdout: "npm:@ai-devkit/pi-session-tracker\n",
|
|
355
|
+
stderr: ""
|
|
356
|
+
};
|
|
357
|
+
if (command === "claude") return {
|
|
358
|
+
stdout: JSON.stringify({
|
|
359
|
+
loggedIn: true
|
|
360
|
+
}),
|
|
361
|
+
stderr: ""
|
|
362
|
+
};
|
|
363
|
+
throw new Error(`unexpected command ${command}`);
|
|
364
|
+
})
|
|
365
|
+
});
|
|
366
|
+
const report = await getAgentReadinessReport("devin", options);
|
|
367
|
+
expect(report.auth).toMatchObject({
|
|
368
|
+
state: "authenticated",
|
|
369
|
+
source: "devin auth status",
|
|
370
|
+
provider: null,
|
|
371
|
+
availableProviders: [
|
|
372
|
+
"devin"
|
|
373
|
+
],
|
|
374
|
+
status: "pass"
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
it("reports Devin as unauthenticated without a login line", async ()=>{
|
|
378
|
+
const { options } = fixture({
|
|
379
|
+
runCommand: vi.fn(async (command)=>{
|
|
380
|
+
if (command === "devin") return {
|
|
381
|
+
stdout: "Not logged in\n",
|
|
382
|
+
stderr: ""
|
|
383
|
+
};
|
|
384
|
+
if (command === "pi") return {
|
|
385
|
+
stdout: "npm:@ai-devkit/pi-session-tracker\n",
|
|
386
|
+
stderr: ""
|
|
387
|
+
};
|
|
388
|
+
if (command === "claude") return {
|
|
389
|
+
stdout: JSON.stringify({
|
|
390
|
+
loggedIn: true
|
|
391
|
+
}),
|
|
392
|
+
stderr: ""
|
|
393
|
+
};
|
|
394
|
+
throw new Error(`unexpected command ${command}`);
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
const report = await getAgentReadinessReport("devin", options);
|
|
398
|
+
expect(report.auth).toMatchObject({
|
|
399
|
+
state: "unauthenticated",
|
|
400
|
+
status: "fail"
|
|
401
|
+
});
|
|
402
|
+
});
|
|
340
403
|
});
|
|
341
404
|
|
|
342
405
|
//# sourceMappingURL=AgentReadiness.test.js.map
|