@ai-devkit/agent-manager 0.40.0 → 0.42.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/AgentManager.d.ts +12 -0
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +45 -10
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +111 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/fixtures.test.js +218 -0
- package/dist/__tests__/fixtures.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
- package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
- package/dist/__tests__/readiness-fixtures.test.js +54 -0
- package/dist/__tests__/readiness-fixtures.test.js.map +1 -0
- package/dist/__tests__/utils/process.test.js +71 -1
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +8 -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/fixtures/bundle.d.ts +67 -0
- package/dist/fixtures/bundle.d.ts.map +1 -0
- package/dist/fixtures/bundle.js +86 -0
- package/dist/fixtures/bundle.js.map +1 -0
- package/dist/fixtures/capture.d.ts +11 -0
- package/dist/fixtures/capture.d.ts.map +1 -0
- package/dist/fixtures/capture.js +318 -0
- package/dist/fixtures/capture.js.map +1 -0
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
- 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/muse/MuseAdapter.d.ts +33 -0
- package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAdapter.js +122 -0
- package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
- package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
- package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.js +350 -0
- package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
- package/dist/harnesses/muse/readiness.d.ts +3 -0
- package/dist/harnesses/muse/readiness.d.ts.map +1 -0
- package/dist/harnesses/muse/readiness.js +37 -0
- package/dist/harnesses/muse/readiness.js.map +1 -0
- 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/harnesses/shared.d.ts +1 -1
- package/dist/harnesses/shared.d.ts.map +1 -1
- package/dist/harnesses/shared.js +7 -6
- package/dist/harnesses/shared.js.map +1 -1
- package/dist/utils/process.d.ts +23 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -8
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +71 -9
- package/src/__tests__/AgentManager.test.ts +84 -0
- package/src/__tests__/fixtures.test.ts +245 -0
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
- package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
- package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
- package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
- package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
- package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
- package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
- package/src/__tests__/readiness-fixtures.test.ts +79 -0
- package/src/__tests__/utils/process.test.ts +30 -0
- package/src/adapters/AgentAdapter.ts +9 -0
- package/src/fixtures/bundle.ts +130 -0
- package/src/fixtures/capture.ts +364 -0
- package/src/harnesses/devin/DevinAdapter.ts +3 -15
- package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
- package/src/harnesses/devin/DevinSessionParser.ts +3 -12
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/muse/MuseAdapter.ts +157 -0
- package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
- package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
- package/src/harnesses/muse/MuseSessionParser.ts +435 -0
- package/src/harnesses/muse/readiness.ts +38 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/harnesses/shared.ts +11 -6
- package/src/utils/process.ts +61 -9
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for harnesses/muse/MuseSessionParser.ts — framed transcript parsing.
|
|
3
|
+
*
|
|
4
|
+
* Muse session.jsonl lines are frames: either a direct inner record, a
|
|
5
|
+
* `record_json` string, or a `retained_frame` wrapper whose children carry
|
|
6
|
+
* `record_json` strings. Inner records dispatch on `payload_type`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { AgentStatus } from "../../../adapters/AgentAdapter.js";
|
|
13
|
+
import { MuseSessionParser } from "../../../harnesses/muse/MuseSessionParser.js";
|
|
14
|
+
|
|
15
|
+
const SID = "01a12618-d918-7b63-91c8-4e896326eb1a";
|
|
16
|
+
|
|
17
|
+
function frame(inner: unknown): string {
|
|
18
|
+
return JSON.stringify(inner);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapped(inner: unknown): string {
|
|
22
|
+
return JSON.stringify({ record_json: JSON.stringify(inner) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function retained(inner: unknown): string {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
retained_frame: "session_permission_transaction",
|
|
28
|
+
frame_schema_version: 1,
|
|
29
|
+
children: [{ child_index: 0, record_json: JSON.stringify(inner) }],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function intent(text: string, sequence: number, recordedAtUs: number): unknown {
|
|
34
|
+
return {
|
|
35
|
+
schema_version: 1,
|
|
36
|
+
stream: { kind: "session", id: SID },
|
|
37
|
+
sequence,
|
|
38
|
+
recorded_at: recordedAtUs,
|
|
39
|
+
record_type: "event",
|
|
40
|
+
payload_type: "runtime.user_intent.accepted",
|
|
41
|
+
payload: {
|
|
42
|
+
intent_id: `intent-${sequence}`,
|
|
43
|
+
refill_blocks: [{ kind: "text", text }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assistantMessage(text: string, sequence: number, recordedAtUs: number): unknown {
|
|
49
|
+
return {
|
|
50
|
+
schema_version: 1,
|
|
51
|
+
stream: { kind: "session", id: SID },
|
|
52
|
+
sequence,
|
|
53
|
+
recorded_at: recordedAtUs,
|
|
54
|
+
record_type: "event",
|
|
55
|
+
payload_type: "runtime.session",
|
|
56
|
+
payload: {
|
|
57
|
+
kind: "run",
|
|
58
|
+
run_id: "run-1",
|
|
59
|
+
event: { kind: "assistant_message_committed", text },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function metadata(recordedAtUs: number): unknown {
|
|
65
|
+
return {
|
|
66
|
+
schema_version: 1,
|
|
67
|
+
stream: { kind: "session", id: SID },
|
|
68
|
+
sequence: 1,
|
|
69
|
+
recorded_at: recordedAtUs,
|
|
70
|
+
record_type: "event",
|
|
71
|
+
payload_type: "runtime.session.metadata",
|
|
72
|
+
payload: {
|
|
73
|
+
kind: "metadata",
|
|
74
|
+
record: { workspace_root: "/Users/tester/proj", model_id: "muse-spark-1.3" },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nameChanged(name: string, sequence: number, recordedAtUs: number): unknown {
|
|
80
|
+
return {
|
|
81
|
+
schema_version: 1,
|
|
82
|
+
stream: { kind: "session", id: SID },
|
|
83
|
+
sequence,
|
|
84
|
+
recorded_at: recordedAtUs,
|
|
85
|
+
record_type: "event",
|
|
86
|
+
payload_type: "session.name.changed",
|
|
87
|
+
payload: { session_id: SID, previous_name: null, new_name: name },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe("MuseSessionParser", () => {
|
|
92
|
+
let parser: MuseSessionParser;
|
|
93
|
+
const tempDirs: string[] = [];
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
parser = new MuseSessionParser();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
for (const dir of tempDirs) {
|
|
101
|
+
try {
|
|
102
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
} catch {
|
|
104
|
+
/* best effort */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
tempDirs.length = 0;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function writeTranscript(lines: string[]): string {
|
|
111
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "muse-parser-test-"));
|
|
112
|
+
tempDirs.push(dir);
|
|
113
|
+
const filePath = path.join(dir, "session.jsonl");
|
|
114
|
+
fs.writeFileSync(filePath, lines.join("\n"));
|
|
115
|
+
return filePath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it("returns null for missing or empty files", () => {
|
|
119
|
+
expect(parser.readSession("/nonexistent/session.jsonl", "/proj")).toBeNull();
|
|
120
|
+
expect(parser.readSession(writeTranscript([]), "/proj")).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("skips unparseable lines and wrapper frames without records", () => {
|
|
124
|
+
const filePath = writeTranscript([
|
|
125
|
+
"not json at all",
|
|
126
|
+
JSON.stringify({ retained_frame: "x", children: [] }),
|
|
127
|
+
frame(intent("hello", 2, 1791640525332173)),
|
|
128
|
+
]);
|
|
129
|
+
const session = parser.readSession(filePath, "/proj");
|
|
130
|
+
expect(session?.lastUserMessage).toBe("hello");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("unwraps record_json strings and retained children", () => {
|
|
134
|
+
const filePath = writeTranscript([
|
|
135
|
+
wrapped(intent("first", 2, 1791640525332173)),
|
|
136
|
+
retained(intent("second", 3, 1791640525332174)),
|
|
137
|
+
]);
|
|
138
|
+
const session = parser.readSession(filePath, "/proj");
|
|
139
|
+
expect(session?.firstUserMessage).toBe("first");
|
|
140
|
+
expect(session?.lastUserMessage).toBe("second");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("reads workspace root, model, and display name", () => {
|
|
144
|
+
const filePath = writeTranscript([
|
|
145
|
+
frame(metadata(1791640525332173)),
|
|
146
|
+
frame(intent("hi", 2, 1791640525332174)),
|
|
147
|
+
frame(nameChanged("flowing-monoceros", 3, 1791640525332175)),
|
|
148
|
+
]);
|
|
149
|
+
// Empty project path lets the recorded workspace root win (archive sessions).
|
|
150
|
+
const session = parser.readSession(filePath, "");
|
|
151
|
+
expect(session?.projectPath).toBe("/Users/tester/proj");
|
|
152
|
+
expect(session?.modelId).toBe("muse-spark-1.3");
|
|
153
|
+
expect(session?.displayName).toBe("flowing-monoceros");
|
|
154
|
+
// First user message is found even when it is not on the first line.
|
|
155
|
+
expect(session?.firstUserMessage).toBe("hi");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("normalizes microsecond timestamps to millisecond dates", () => {
|
|
159
|
+
const filePath = writeTranscript([frame(intent("hi", 2, 1791640525332173))]);
|
|
160
|
+
const session = parser.readSession(filePath, "/proj");
|
|
161
|
+
expect(session?.lastActive).toEqual(new Date(1791640525332));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("maps last user intent to RUNNING and committed reply to WAITING", () => {
|
|
165
|
+
const running = parser.readSession(
|
|
166
|
+
writeTranscript([
|
|
167
|
+
frame(intent("q", 2, 1791640525332173)),
|
|
168
|
+
frame(assistantMessage("a", 3, 1791640525332174)),
|
|
169
|
+
frame(intent("q2", 4, 1791640525332175)),
|
|
170
|
+
]),
|
|
171
|
+
"/proj",
|
|
172
|
+
);
|
|
173
|
+
expect(parser.determineStatus(running!)).toBe(AgentStatus.RUNNING);
|
|
174
|
+
|
|
175
|
+
const waiting = parser.readSession(
|
|
176
|
+
writeTranscript([
|
|
177
|
+
frame(intent("q", 2, 1791640525332173)),
|
|
178
|
+
frame(assistantMessage("a", 3, 1791640525332174)),
|
|
179
|
+
]),
|
|
180
|
+
"/proj",
|
|
181
|
+
);
|
|
182
|
+
expect(parser.determineStatus(waiting!)).toBe(AgentStatus.WAITING);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("maps approval waits and terminal runs to WAITING and IDLE", () => {
|
|
186
|
+
const waiting = parser.readSession(
|
|
187
|
+
writeTranscript([
|
|
188
|
+
frame({
|
|
189
|
+
schema_version: 1,
|
|
190
|
+
stream: { kind: "session", id: SID },
|
|
191
|
+
sequence: 2,
|
|
192
|
+
recorded_at: 1791640525332173,
|
|
193
|
+
record_type: "event",
|
|
194
|
+
payload_type: "runtime.session",
|
|
195
|
+
payload: {
|
|
196
|
+
kind: "run",
|
|
197
|
+
run_id: "run-1",
|
|
198
|
+
event: { kind: "approval_wait.effect.started", tool_name: "edit" },
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
]),
|
|
202
|
+
"/proj",
|
|
203
|
+
);
|
|
204
|
+
expect(parser.determineStatus(waiting!)).toBe(AgentStatus.WAITING);
|
|
205
|
+
|
|
206
|
+
const idle = parser.readSession(
|
|
207
|
+
writeTranscript([
|
|
208
|
+
frame({
|
|
209
|
+
schema_version: 1,
|
|
210
|
+
stream: { kind: "session", id: SID },
|
|
211
|
+
sequence: 2,
|
|
212
|
+
recorded_at: 1791640525332173,
|
|
213
|
+
record_type: "event",
|
|
214
|
+
payload_type: "runtime.session",
|
|
215
|
+
payload: { kind: "run", run_id: "run-1", event: { kind: "terminal" } },
|
|
216
|
+
}),
|
|
217
|
+
]),
|
|
218
|
+
"/proj",
|
|
219
|
+
);
|
|
220
|
+
expect(parser.determineStatus(idle!)).toBe(AgentStatus.IDLE);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("returns UNKNOWN when no conversation signal exists", () => {
|
|
224
|
+
const session = parser.readSession(
|
|
225
|
+
writeTranscript([frame(metadata(1791640525332173))]),
|
|
226
|
+
"/proj",
|
|
227
|
+
);
|
|
228
|
+
expect(parser.determineStatus(session!)).toBe(AgentStatus.UNKNOWN);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("getConversation returns ordered messages and honors tail", () => {
|
|
232
|
+
const filePath = writeTranscript([
|
|
233
|
+
frame(metadata(1791640525332173)),
|
|
234
|
+
frame(intent("first question", 2, 1791640525332174)),
|
|
235
|
+
frame(assistantMessage("first answer", 3, 1791640525332175)),
|
|
236
|
+
frame(intent("second question", 4, 1791640525332176)),
|
|
237
|
+
]);
|
|
238
|
+
const full = parser.getConversation(filePath);
|
|
239
|
+
expect(full.map((m) => [m.role, m.content])).toEqual([
|
|
240
|
+
["user", "first question"],
|
|
241
|
+
["assistant", "first answer"],
|
|
242
|
+
["user", "second question"],
|
|
243
|
+
]);
|
|
244
|
+
const tail = parser.getConversation(filePath, { tail: 1 });
|
|
245
|
+
expect(tail).toEqual([
|
|
246
|
+
{ role: "user", content: "second question", timestamp: expect.anything() },
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns [] for a missing transcript and tool calls in verbose mode", () => {
|
|
251
|
+
expect(parser.getConversation("/nonexistent/session.jsonl")).toEqual([]);
|
|
252
|
+
const filePath = writeTranscript([
|
|
253
|
+
frame({
|
|
254
|
+
schema_version: 1,
|
|
255
|
+
stream: { kind: "session", id: SID },
|
|
256
|
+
sequence: 2,
|
|
257
|
+
recorded_at: 1791640525332173,
|
|
258
|
+
record_type: "event",
|
|
259
|
+
payload_type: "runtime.session",
|
|
260
|
+
payload: {
|
|
261
|
+
kind: "run",
|
|
262
|
+
run_id: "run-1",
|
|
263
|
+
event: {
|
|
264
|
+
kind: "assistant_tool_calls_committed",
|
|
265
|
+
tool_calls: [{ tool_name: "read_file" }, { tool_name: "search" }],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
]);
|
|
270
|
+
expect(parser.getConversation(filePath)).toEqual([]);
|
|
271
|
+
expect(parser.getConversation(filePath, { verbose: true })).toEqual([
|
|
272
|
+
{
|
|
273
|
+
role: "assistant",
|
|
274
|
+
content: "tool calls: read_file, search",
|
|
275
|
+
timestamp: expect.anything(),
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -143,6 +143,7 @@ function insertPart(
|
|
|
143
143
|
db: Database.Database,
|
|
144
144
|
input: { messageId: string; sessionId: string; data: unknown; timeCreated: number },
|
|
145
145
|
): void {
|
|
146
|
-
db.prepare(
|
|
147
|
-
|
|
146
|
+
db.prepare(
|
|
147
|
+
"INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)",
|
|
148
|
+
).run(input.messageId, input.sessionId, JSON.stringify(input.data), input.timeCreated);
|
|
148
149
|
}
|
|
@@ -68,6 +68,7 @@ function fixture(overrides: Partial<AgentReadinessOptions> = {}) {
|
|
|
68
68
|
".gemini/antigravity-cli",
|
|
69
69
|
".config/opencode",
|
|
70
70
|
".config/devin",
|
|
71
|
+
".config/muse",
|
|
71
72
|
".pi",
|
|
72
73
|
]) {
|
|
73
74
|
files[path.join(homeDir, directory)] = "<dir>";
|
|
@@ -128,6 +129,7 @@ describe("agent readiness", () => {
|
|
|
128
129
|
"copilot",
|
|
129
130
|
"pi",
|
|
130
131
|
"devin",
|
|
132
|
+
"muse",
|
|
131
133
|
]);
|
|
132
134
|
for (const [agent, report] of Object.entries(reports) as Array<
|
|
133
135
|
[ReadinessAgentType, (typeof reports)[ReadinessAgentType]]
|
|
@@ -166,6 +168,7 @@ describe("agent readiness", () => {
|
|
|
166
168
|
expect(reports.kiro.integration).toBeUndefined();
|
|
167
169
|
expect(reports.antigravity_cli.integration).toBeUndefined();
|
|
168
170
|
expect(reports.opencode.integration).toBeUndefined();
|
|
171
|
+
expect(reports.muse.integration).toBeUndefined();
|
|
169
172
|
});
|
|
170
173
|
|
|
171
174
|
it("resolves agent executables from PATH directories containing spaces", async () => {
|
|
@@ -309,6 +312,39 @@ describe("agent readiness", () => {
|
|
|
309
312
|
});
|
|
310
313
|
});
|
|
311
314
|
|
|
315
|
+
it("checks Muse auth from the muse auth.json file", async () => {
|
|
316
|
+
const authPath = path.join("/home/test", ".config", "muse", "auth.json");
|
|
317
|
+
const { options, files } = fixture();
|
|
318
|
+
files[authPath] = JSON.stringify({ meta: { token: "secret" } });
|
|
319
|
+
|
|
320
|
+
const report = await getAgentReadinessReport("muse", options);
|
|
321
|
+
|
|
322
|
+
expect(report.auth).toMatchObject({ state: "authenticated", status: "pass" });
|
|
323
|
+
expect(report.globalConfig).toMatchObject({ status: "pass" });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("reports Muse as unauthenticated with an empty auth file", async () => {
|
|
327
|
+
const authPath = path.join("/home/test", ".config", "muse", "auth.json");
|
|
328
|
+
const { options, files } = fixture();
|
|
329
|
+
files[authPath] = JSON.stringify({});
|
|
330
|
+
|
|
331
|
+
const report = await getAgentReadinessReport("muse", options);
|
|
332
|
+
|
|
333
|
+
expect(report.auth).toMatchObject({
|
|
334
|
+
state: "unauthenticated",
|
|
335
|
+
status: "fail",
|
|
336
|
+
errors: ["Muse credential file has no entries"],
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("reports Muse as unauthenticated without an auth file", async () => {
|
|
341
|
+
const { options } = fixture();
|
|
342
|
+
|
|
343
|
+
const report = await getAgentReadinessReport("muse", options);
|
|
344
|
+
|
|
345
|
+
expect(report.auth).toMatchObject({ state: "unauthenticated", status: "fail" });
|
|
346
|
+
});
|
|
347
|
+
|
|
312
348
|
it("reports Devin as unauthenticated without a login line", async () => {
|
|
313
349
|
const { options } = fixture({
|
|
314
350
|
runCommand: vi.fn(async (command) => {
|
|
@@ -36,6 +36,19 @@ describe("harness runtime profiles", () => {
|
|
|
36
36
|
expect(runtimeAgentMatchesHarness("gemini", "gemini_cli")).toBe(true);
|
|
37
37
|
expect(runtimeAgentMatchesHarness("codex", "claude")).toBe(false);
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
it("profiles the versioned muse launcher", () => {
|
|
41
|
+
expect(HARNESS_RUNTIME_PROFILES.muse).toMatchObject({
|
|
42
|
+
command: "muse",
|
|
43
|
+
runtimeKind: "muse",
|
|
44
|
+
discoveryAliases: ["muse"],
|
|
45
|
+
});
|
|
46
|
+
expect(
|
|
47
|
+
HARNESS_RUNTIME_PROFILES.muse.matches("/Users/dev/.local/bin/muse-bin-1.4.4-R5419.1"),
|
|
48
|
+
).toBe(true);
|
|
49
|
+
expect(HARNESS_RUNTIME_PROFILES.muse.matches("/usr/local/bin/muse")).toBe(true);
|
|
50
|
+
expect(runtimeAgentMatchesHarness("muse", "muse")).toBe(true);
|
|
51
|
+
});
|
|
39
52
|
});
|
|
40
53
|
|
|
41
54
|
describe("harness matchers with executables under paths containing spaces", () => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness fixture replay: each committed bundle under
|
|
3
|
+
* `fixtures/readiness/` provides the injectable seams (files,
|
|
4
|
+
* executables, command outputs, codexAuth) plus `expected` — the
|
|
5
|
+
* byte-identical reports the readiness probes must produce replaying
|
|
6
|
+
* the same bundle through their injectable seams.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
getAgentReadinessReports,
|
|
14
|
+
type AgentReadinessOptions,
|
|
15
|
+
} from "../harnesses/readiness/AgentReadiness.js";
|
|
16
|
+
|
|
17
|
+
interface ReadinessBundle {
|
|
18
|
+
options: {
|
|
19
|
+
homeDir: string;
|
|
20
|
+
path: string;
|
|
21
|
+
assetRoot?: string;
|
|
22
|
+
builtInSkillNames?: string[];
|
|
23
|
+
skillRoots?: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
/** Absolute path → contents, or "<dir>" for a directory entry. */
|
|
26
|
+
files: Record<string, string>;
|
|
27
|
+
/** Paths that answer X_OK access. */
|
|
28
|
+
executables: string[];
|
|
29
|
+
/** `"command arg1 arg2"` → output; `ok: false` rejects like execFile. */
|
|
30
|
+
commands: Record<string, { stdout: string; stderr?: string; ok?: boolean }>;
|
|
31
|
+
codexAuth?: boolean | null;
|
|
32
|
+
expected: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const READINESS_ROOT = path.resolve(__dirname, "../../../..", "fixtures", "readiness");
|
|
36
|
+
|
|
37
|
+
const bundlePaths = fs.existsSync(READINESS_ROOT)
|
|
38
|
+
? fs
|
|
39
|
+
.readdirSync(READINESS_ROOT)
|
|
40
|
+
.filter((f) => f.endsWith(".json") && !f.startsWith("live"))
|
|
41
|
+
.sort()
|
|
42
|
+
.map((f) => path.join(READINESS_ROOT, f))
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
function replay(bundle: ReadinessBundle): Promise<Record<string, unknown>> {
|
|
46
|
+
const executableSet = new Set(bundle.executables);
|
|
47
|
+
const options: AgentReadinessOptions = {
|
|
48
|
+
homeDir: bundle.options.homeDir,
|
|
49
|
+
path: bundle.options.path,
|
|
50
|
+
assetRoot: bundle.options.assetRoot,
|
|
51
|
+
builtInSkillNames: bundle.options.builtInSkillNames ?? [],
|
|
52
|
+
skillRoots: bundle.options.skillRoots ?? {},
|
|
53
|
+
readFile: async (target) => {
|
|
54
|
+
if (!(target in bundle.files)) throw new Error("missing");
|
|
55
|
+
return bundle.files[target];
|
|
56
|
+
},
|
|
57
|
+
access: async (target) => {
|
|
58
|
+
if (executableSet.has(target) || target in bundle.files) return;
|
|
59
|
+
throw new Error("missing");
|
|
60
|
+
},
|
|
61
|
+
runCommand: async (command, args) => {
|
|
62
|
+
const key = [command, ...args].join(" ");
|
|
63
|
+
const stub = bundle.commands[key];
|
|
64
|
+
if (!stub) throw new Error(`unexpected command ${key}`);
|
|
65
|
+
if (stub.ok === false) throw new Error(`command failed: ${key}`);
|
|
66
|
+
return { stdout: stub.stdout, stderr: stub.stderr ?? "" };
|
|
67
|
+
},
|
|
68
|
+
codexAuth: async () => bundle.codexAuth ?? null,
|
|
69
|
+
};
|
|
70
|
+
return getAgentReadinessReports(options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("readiness fixture replay", () => {
|
|
74
|
+
it.each(bundlePaths)("%s", async (bundlePath) => {
|
|
75
|
+
const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8")) as ReadinessBundle;
|
|
76
|
+
const reports = await replay(bundle);
|
|
77
|
+
expect(reports).toEqual(bundle.expected);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
captureProcessSnapshot,
|
|
19
19
|
createProcessSnapshotCapture,
|
|
20
20
|
filterByProcessNames,
|
|
21
|
+
matchesExecutablePrefix,
|
|
22
|
+
snapshotOptions,
|
|
21
23
|
executableBasename,
|
|
22
24
|
executablePath,
|
|
23
25
|
type ProcessExec,
|
|
@@ -259,6 +261,34 @@ describe("filterByProcessNames", () => {
|
|
|
259
261
|
expect(filterByProcessNames(processes, ["node"])).toEqual([processes[0], processes[1]]);
|
|
260
262
|
expect(filterByProcessNames(processes, ["node.exe"])).toEqual([processes[0], processes[1]]);
|
|
261
263
|
});
|
|
264
|
+
|
|
265
|
+
it("matches versioned launchers by basename prefix without leaking to siblings", () => {
|
|
266
|
+
const processes: ProcessInfo[] = [
|
|
267
|
+
{ pid: 1, command: "/Users/dev/.local/bin/muse-bin-1.4.4-R5419.1", cwd: "", tty: "" },
|
|
268
|
+
{ pid: 2, command: "/usr/local/bin/muse", cwd: "", tty: "" },
|
|
269
|
+
{ pid: 3, command: "/usr/bin/museums", cwd: "", tty: "" },
|
|
270
|
+
{ pid: 4, command: "/usr/local/bin/claude", cwd: "", tty: "" },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
expect(filterByProcessNames(processes, ["muse"], ["muse"])).toEqual([
|
|
274
|
+
processes[0],
|
|
275
|
+
processes[1],
|
|
276
|
+
]);
|
|
277
|
+
// Exact names alone never match the versioned launcher.
|
|
278
|
+
expect(filterByProcessNames(processes, ["muse"])).toEqual([processes[1]]);
|
|
279
|
+
// No prefixes: previous behavior is unchanged.
|
|
280
|
+
expect(filterByProcessNames(processes, ["claude"])).toEqual([processes[3]]);
|
|
281
|
+
expect(matchesExecutablePrefix(processes[0].command, new Set(["muse"]))).toBe(true);
|
|
282
|
+
expect(matchesExecutablePrefix(processes[2].command, new Set(["muse"]))).toBe(false);
|
|
283
|
+
expect(matchesExecutablePrefix(processes[0].command, new Set())).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("builds snapshot options with prefixes only when present", () => {
|
|
287
|
+
const isCandidate = () => true;
|
|
288
|
+
expect(snapshotOptions([], isCandidate)).toEqual({ isCandidate });
|
|
289
|
+
const withPrefixes = snapshotOptions(["muse"], isCandidate);
|
|
290
|
+
expect(withPrefixes).toMatchObject({ namePrefixes: ["muse"], isCandidate });
|
|
291
|
+
});
|
|
262
292
|
});
|
|
263
293
|
|
|
264
294
|
describe("executables installed under paths containing spaces", () => {
|
|
@@ -17,6 +17,7 @@ export const AGENT_TYPES = [
|
|
|
17
17
|
"copilot",
|
|
18
18
|
"pi",
|
|
19
19
|
"devin",
|
|
20
|
+
"muse",
|
|
20
21
|
] as const;
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -201,6 +202,14 @@ export interface AgentAdapter {
|
|
|
201
202
|
/** Executable basenames required for shared process discovery. */
|
|
202
203
|
readonly processNames?: readonly string[];
|
|
203
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Basename prefixes for shared process discovery: an argv[0] basename
|
|
207
|
+
* matches when it equals the prefix or starts with `<prefix>-`.
|
|
208
|
+
* For versioned launchers such as `muse-bin-1.4.4-R5419.1`.
|
|
209
|
+
* Recall-oriented like `processNames`; `canHandle` stays the precision gate.
|
|
210
|
+
*/
|
|
211
|
+
readonly processNamePrefixes?: readonly string[];
|
|
212
|
+
|
|
204
213
|
/**
|
|
205
214
|
* Detect running agents of this type
|
|
206
215
|
* @returns List of detected agents
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden-fixture bundles: the parity oracle for the devkit-harness Rust port.
|
|
3
|
+
* A bundle records everything `detectAgents` consumed and produced so both
|
|
4
|
+
* the TS adapter and its Rust port can be run against identical inputs.
|
|
5
|
+
*
|
|
6
|
+
* Format (committed under fixtures/harness/<type>/<case>.json):
|
|
7
|
+
* {
|
|
8
|
+
* adapter, capturedAt, frozenNow,
|
|
9
|
+
* processes: ProcessInfo[],
|
|
10
|
+
* home: { "<relpath under $FIXTURE_HOME>": "<content with $FIXTURE_HOME refs>" },
|
|
11
|
+
* expected: AgentInfo[] (dates as ISO strings)
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentInfo, ProcessInfo } from "../adapters/AgentAdapter.js";
|
|
16
|
+
|
|
17
|
+
export const HOME_PLACEHOLDER = "$FIXTURE_HOME";
|
|
18
|
+
/** Resolves to the instant home files were written (birthtime matching). */
|
|
19
|
+
export const NOW_PLACEHOLDER = "$NOW";
|
|
20
|
+
/** Resolves to the local `YYYY/MM/DD` day dir at replay (Codex sessions). */
|
|
21
|
+
export const TODAY_PLACEHOLDER = "$TODAY";
|
|
22
|
+
|
|
23
|
+
export interface FixtureBundle {
|
|
24
|
+
adapter: string;
|
|
25
|
+
capturedAt: string;
|
|
26
|
+
/** Epoch ms the clock was frozen at; `"$NOW"` = materialization instant. */
|
|
27
|
+
frozenNow: number | string;
|
|
28
|
+
processes: ProcessInfo[];
|
|
29
|
+
/** Files under the fixture HOME; content may embed HOME_PLACEHOLDER. */
|
|
30
|
+
home: Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* mtime overrides keyed by `home` relpath — epoch ms, or "$NOW" for the
|
|
33
|
+
* materialization instant. Adapters whose output derives from file mtimes
|
|
34
|
+
* (Grok's lastActive / latest-session pick) need deterministic mtimes.
|
|
35
|
+
*/
|
|
36
|
+
mtimes?: Record<string, number | string>;
|
|
37
|
+
/**
|
|
38
|
+
* AgentRegistry rows (pid→sessionFilePath etc.) visible at capture time,
|
|
39
|
+
* restricted to captured process pids — adapters' registry caches read
|
|
40
|
+
* this state, so replay seeds an isolated registry with it.
|
|
41
|
+
*/
|
|
42
|
+
registry?: Record<string, unknown>[];
|
|
43
|
+
/**
|
|
44
|
+
* SQLite stores adapters consult (OpenCode's opencode.db), keyed by
|
|
45
|
+
* relpath under $FIXTURE_HOME: SQL statements (DDL + INSERTs) executed
|
|
46
|
+
* in order to materialize each db. Statements may embed HOME_PLACEHOLDER.
|
|
47
|
+
*/
|
|
48
|
+
sqlite?: Record<string, string[]>;
|
|
49
|
+
expected: Record<string, unknown>[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Rewrite harness dot-dir paths (`<home>/.claude`, `<home>/.codex`, …) to the
|
|
54
|
+
* placeholder — nothing else. Project cwds share the home prefix
|
|
55
|
+
* (`<home>/Code/proj`) and MUST stay literal: harnesses encode them into
|
|
56
|
+
* session-dir names (`-Users-foo-proj`), so rewriting them breaks
|
|
57
|
+
* cwd↔session matching on replay.
|
|
58
|
+
*/
|
|
59
|
+
export function sanitize<T>(value: T, realHome: string): T {
|
|
60
|
+
const homeDot = `${realHome}/.`;
|
|
61
|
+
if (typeof value === "string") {
|
|
62
|
+
return value.split(homeDot).join(`${HOME_PLACEHOLDER}/.`) as T;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) return value.map((v) => sanitize(v, realHome)) as T;
|
|
65
|
+
if (value && typeof value === "object") {
|
|
66
|
+
if (value instanceof Date) return value.toISOString() as T;
|
|
67
|
+
const out: Record<string, unknown> = {};
|
|
68
|
+
for (const [k, v] of Object.entries(value)) out[k] = sanitize(v, realHome);
|
|
69
|
+
return out as T;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Inverse of sanitize: expand the placeholder to a concrete dir. */
|
|
75
|
+
export function expand<T>(value: T, fixtureHome: string): T {
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
return value.split(HOME_PLACEHOLDER).join(fixtureHome) as T;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) return value.map((v) => expand(v, fixtureHome)) as T;
|
|
80
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
81
|
+
const out: Record<string, unknown> = {};
|
|
82
|
+
for (const [k, v] of Object.entries(value)) out[k] = expand(v, fixtureHome);
|
|
83
|
+
return out as T;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Replace NOW_PLACEHOLDER strings with a resolved ISO instant. */
|
|
89
|
+
export function expandNow<T>(value: T, isoInstant: string): T {
|
|
90
|
+
if (typeof value === "string") {
|
|
91
|
+
return value.split(NOW_PLACEHOLDER).join(isoInstant) as T;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) return value.map((v) => expandNow(v, isoInstant)) as T;
|
|
94
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
95
|
+
const out: Record<string, unknown> = {};
|
|
96
|
+
for (const [k, v] of Object.entries(value)) out[k] = expandNow(v, isoInstant);
|
|
97
|
+
return out as T;
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Agents normalized the way the wire carries them (Date → ISO string). */
|
|
103
|
+
export function normalizeAgents(agents: AgentInfo[], realHome: string) {
|
|
104
|
+
return sanitize(JSON.parse(JSON.stringify(agents)), realHome);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Freeze `Date` at `epochMs` for the duration of `fn`. Adapters stamp
|
|
109
|
+
* `new Date()` in several places; parity requires a fixed clock.
|
|
110
|
+
*/
|
|
111
|
+
export async function withFrozenClock<T>(epochMs: number, fn: () => Promise<T>): Promise<T> {
|
|
112
|
+
const RealDate = Date;
|
|
113
|
+
class FrozenDate extends RealDate {
|
|
114
|
+
constructor(...args: unknown[]) {
|
|
115
|
+
if (args.length === 0) super(epochMs);
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
else super(...(args as [any]));
|
|
118
|
+
}
|
|
119
|
+
static override now() {
|
|
120
|
+
return epochMs;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const g = globalThis as { Date: DateConstructor };
|
|
124
|
+
g.Date = FrozenDate as DateConstructor;
|
|
125
|
+
try {
|
|
126
|
+
return await fn();
|
|
127
|
+
} finally {
|
|
128
|
+
g.Date = RealDate;
|
|
129
|
+
}
|
|
130
|
+
}
|