chapterhouse 0.3.24 → 0.3.26
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/api/server-runtime.js +1 -1
- package/dist/api/server.js +53 -16
- package/dist/api/server.test.js +31 -56
- package/dist/api/sse.integration.test.js +4 -46
- package/dist/api/turn-sse.integration.test.js +20 -47
- package/dist/api/worker-events-sse.integration.test.js +241 -1
- package/dist/config.js +11 -1
- package/dist/config.test.js +14 -0
- package/dist/copilot/orchestrator.js +3 -1
- package/dist/copilot/orchestrator.test.js +1 -1
- package/dist/copilot/task-event-log.js +6 -1
- package/dist/copilot/task-event-log.test.js +45 -0
- package/dist/copilot/tools.js +39 -4
- package/dist/store/db.js +56 -16
- package/dist/store/db.test.js +67 -7
- package/dist/test/api-server.js +50 -0
- package/dist/test/api-server.test.js +57 -0
- package/dist/test/setup-env.js +9 -0
- package/dist/test/setup-env.test.js +34 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-BK-hInnO.js → index-BRPJa1DK.js} +83 -83
- package/web/dist/assets/{index-BK-hInnO.js.map → index-BRPJa1DK.js.map} +1 -1
- package/web/dist/assets/index-DhY5yWmC.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D__tBB0X.css +0 -10
package/dist/store/db.test.js
CHANGED
|
@@ -165,28 +165,32 @@ test("getSessionMessages returns empty array for unknown session", async () => {
|
|
|
165
165
|
dbModule.closeDb();
|
|
166
166
|
}
|
|
167
167
|
});
|
|
168
|
-
test("getSessionMessages returns structured messages in chronological order, excludes system rows, respects limit", async () => {
|
|
168
|
+
test("getSessionMessages returns structured messages in chronological order, includes agent completions, excludes system rows, respects limit", async () => {
|
|
169
169
|
const dbModule = await loadDbModule();
|
|
170
170
|
try {
|
|
171
|
-
dbModule.getDb();
|
|
171
|
+
const db = dbModule.getDb();
|
|
172
172
|
dbModule.logConversation("user", "hello", "web", "test-session");
|
|
173
173
|
dbModule.logConversation("assistant", "hi there", "web", "test-session");
|
|
174
174
|
dbModule.logConversation("system", "system noise", "worker", "test-session");
|
|
175
|
+
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key)
|
|
176
|
+
VALUES ('agent_completion', ?, 'background', 'test-session')`).run("[Agent task completed] @coder finished task task-1:\n\nDone");
|
|
175
177
|
dbModule.logConversation("user", "second message", "web", "test-session");
|
|
176
178
|
dbModule.logConversation("user", "from other session", "web", "other-session");
|
|
177
179
|
const all = dbModule.getSessionMessages("test-session");
|
|
178
|
-
assert.equal(all.length,
|
|
180
|
+
assert.equal(all.length, 4, "user/assistant rows plus agent completion, system excluded");
|
|
179
181
|
assert.equal(all[0].role, "user");
|
|
180
182
|
assert.equal(all[0].content, "hello");
|
|
181
183
|
assert.equal(all[1].role, "assistant");
|
|
182
184
|
assert.equal(all[1].content, "hi there");
|
|
183
|
-
assert.equal(all[2].role, "
|
|
184
|
-
assert.equal(all[2].content, "
|
|
185
|
+
assert.equal(all[2].role, "assistant");
|
|
186
|
+
assert.equal(all[2].content, "[Agent task completed] @coder finished task task-1:\n\nDone");
|
|
187
|
+
assert.equal(all[3].role, "user");
|
|
188
|
+
assert.equal(all[3].content, "second message");
|
|
185
189
|
// Limit clamping
|
|
186
190
|
const limited = dbModule.getSessionMessages("test-session", 2);
|
|
187
191
|
assert.equal(limited.length, 2, "limit=2 returns 2 most recent rows");
|
|
188
|
-
// After reversal, these should be the 2 most-recent
|
|
189
|
-
assert.equal(limited[0].content, "
|
|
192
|
+
// After reversal, these should be the 2 most-recent renderable rows.
|
|
193
|
+
assert.equal(limited[0].content, "[Agent task completed] @coder finished task task-1:\n\nDone");
|
|
190
194
|
assert.equal(limited[1].content, "second message");
|
|
191
195
|
// Other session not leaked
|
|
192
196
|
const other = dbModule.getSessionMessages("other-session");
|
|
@@ -266,6 +270,62 @@ test("#86: agent_task_events table exists in schema after getDb()", async () =>
|
|
|
266
270
|
dbModule.closeDb();
|
|
267
271
|
}
|
|
268
272
|
});
|
|
273
|
+
test("#158: appendTaskOutputDeltaEvent writes output_delta event with text", async () => {
|
|
274
|
+
const dbModule = await loadDbModule();
|
|
275
|
+
try {
|
|
276
|
+
const db = dbModule.getDb();
|
|
277
|
+
db.prepare("INSERT INTO agent_tasks (task_id, agent_slug, description, status) VALUES (?, ?, ?, ?)").run("task-delta-001", "coder", "delta test", "running");
|
|
278
|
+
const ev = dbModule.appendTaskOutputDeltaEvent("task-delta-001", "Hello world");
|
|
279
|
+
assert.ok(ev, "appendTaskOutputDeltaEvent must return the event");
|
|
280
|
+
assert.equal(ev.kind, "output_delta");
|
|
281
|
+
assert.equal(ev.text, "Hello world");
|
|
282
|
+
assert.equal(ev.toolName, null);
|
|
283
|
+
assert.equal(ev.status, null);
|
|
284
|
+
assert.equal(ev.seq, 1);
|
|
285
|
+
const events = dbModule.getTaskEvents("task-delta-001");
|
|
286
|
+
assert.equal(events.length, 1);
|
|
287
|
+
assert.equal(events[0].text, "Hello world");
|
|
288
|
+
assert.equal(events[0].kind, "output_delta");
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
dbModule.closeDb();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
test("#158: appendTaskStatusEvent writes task_status event with status field", async () => {
|
|
295
|
+
const dbModule = await loadDbModule();
|
|
296
|
+
try {
|
|
297
|
+
const db = dbModule.getDb();
|
|
298
|
+
db.prepare("INSERT INTO agent_tasks (task_id, agent_slug, description, status) VALUES (?, ?, ?, ?)").run("task-status-001", "coder", "status test", "running");
|
|
299
|
+
const ev = dbModule.appendTaskStatusEvent("task-status-001", "completed", "All done");
|
|
300
|
+
assert.ok(ev, "appendTaskStatusEvent must return the event");
|
|
301
|
+
assert.equal(ev.kind, "task_status");
|
|
302
|
+
assert.equal(ev.status, "completed");
|
|
303
|
+
assert.equal(ev.summary, "All done");
|
|
304
|
+
assert.equal(ev.text, null);
|
|
305
|
+
assert.equal(ev.toolName, null);
|
|
306
|
+
const events = dbModule.getTaskEvents("task-status-001");
|
|
307
|
+
assert.equal(events.length, 1);
|
|
308
|
+
assert.equal(events[0].status, "completed");
|
|
309
|
+
assert.equal(events[0].kind, "task_status");
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
dbModule.closeDb();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
test("#158: updateTaskResult updates agent_tasks status and result", async () => {
|
|
316
|
+
const dbModule = await loadDbModule();
|
|
317
|
+
try {
|
|
318
|
+
const db = dbModule.getDb();
|
|
319
|
+
db.prepare("INSERT INTO agent_tasks (task_id, agent_slug, description, status) VALUES (?, ?, ?, ?)").run("task-result-001", "coder", "result test", "running");
|
|
320
|
+
dbModule.updateTaskResult("task-result-001", "completed", "output text");
|
|
321
|
+
const row = db.prepare("SELECT status, result FROM agent_tasks WHERE task_id = ?").get("task-result-001");
|
|
322
|
+
assert.equal(row.status, "completed");
|
|
323
|
+
assert.equal(row.result, "output text");
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
dbModule.closeDb();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
269
329
|
// ---------------------------------------------------------------------------
|
|
270
330
|
// normalizeSqliteTsToIso — unit tests
|
|
271
331
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
export const DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS = 60_000;
|
|
5
|
+
export const STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS = 60_000;
|
|
6
|
+
export const API_SERVER_STARTUP_POLL_INTERVAL_MS = 100;
|
|
7
|
+
export async function getFreePort() {
|
|
8
|
+
const server = createServer();
|
|
9
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
10
|
+
const address = server.address();
|
|
11
|
+
assert.ok(address && typeof address === "object", "server should expose a bound address");
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
14
|
+
});
|
|
15
|
+
return address.port;
|
|
16
|
+
}
|
|
17
|
+
export async function stopChild(child) {
|
|
18
|
+
if (child.exitCode !== null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
child.kill("SIGTERM");
|
|
22
|
+
await new Promise((resolve) => {
|
|
23
|
+
child.once("exit", () => resolve());
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (child.exitCode === null) {
|
|
26
|
+
child.kill("SIGKILL");
|
|
27
|
+
}
|
|
28
|
+
}, 2_000);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function waitForApiServerReady({ child, baseUrl, logs, timeoutMs = DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS, pollIntervalMs = API_SERVER_STARTUP_POLL_INTERVAL_MS, timeoutMessage = "Timed out waiting for API server to start", exitMessage = "API server exited early", }) {
|
|
32
|
+
const deadline = Date.now() + timeoutMs;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
if (child.exitCode !== null) {
|
|
35
|
+
throw new Error(`${exitMessage}:\n${logs.join("")}`);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`${baseUrl}/status`);
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Server is still starting.
|
|
45
|
+
}
|
|
46
|
+
await delay(pollIntervalMs);
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`${timeoutMessage}:\n${logs.join("")}`);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=api-server.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS, STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS, waitForApiServerReady, } from "./api-server.js";
|
|
5
|
+
async function listen(server) {
|
|
6
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
7
|
+
const address = server.address();
|
|
8
|
+
assert.ok(address && typeof address === "object");
|
|
9
|
+
return address.port;
|
|
10
|
+
}
|
|
11
|
+
async function close(server) {
|
|
12
|
+
if (!server.listening) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
test("waitForApiServerReady waits until /status responds with ok", async () => {
|
|
20
|
+
let ready = false;
|
|
21
|
+
const server = createServer((req, res) => {
|
|
22
|
+
if (req.url !== "/status") {
|
|
23
|
+
res.writeHead(404).end();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!ready) {
|
|
27
|
+
res.writeHead(503).end("starting");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
31
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
32
|
+
});
|
|
33
|
+
const port = await listen(server);
|
|
34
|
+
const child = { exitCode: null };
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
ready = true;
|
|
37
|
+
}, 200);
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
await waitForApiServerReady({
|
|
41
|
+
child,
|
|
42
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
43
|
+
logs: [],
|
|
44
|
+
timeoutMs: 1_000,
|
|
45
|
+
pollIntervalMs: 25,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await close(server);
|
|
50
|
+
}
|
|
51
|
+
assert.ok(Date.now() - startedAt >= 150);
|
|
52
|
+
});
|
|
53
|
+
test("API server startup timeouts leave headroom for slow CI runners", () => {
|
|
54
|
+
assert.equal(DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS, 60_000);
|
|
55
|
+
assert.equal(STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS, 60_000);
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=api-server.test.js.map
|
package/dist/test/setup-env.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const RUNTIME_OVERRIDE_ENV_VARS = [
|
|
2
|
+
"CHAPTERHOUSE_MODE",
|
|
3
|
+
"CHAPTERHOUSE_SELF_EDIT",
|
|
4
|
+
"CHAPTERHOUSE_WORKIQ_AUTO_INSTALL",
|
|
5
|
+
"CHAPTERHOUSE_CHAT_SSE",
|
|
6
|
+
];
|
|
7
|
+
for (const name of RUNTIME_OVERRIDE_ENV_VARS) {
|
|
8
|
+
delete process.env[name];
|
|
9
|
+
}
|
|
1
10
|
process.env.CHAPTERHOUSE_DISABLE_DOTENV = "1";
|
|
2
11
|
export {};
|
|
3
12
|
//# sourceMappingURL=setup-env.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
const RUNTIME_OVERRIDE_ENV_VARS = [
|
|
4
|
+
"CHAPTERHOUSE_MODE",
|
|
5
|
+
"CHAPTERHOUSE_SELF_EDIT",
|
|
6
|
+
"CHAPTERHOUSE_WORKIQ_AUTO_INSTALL",
|
|
7
|
+
"CHAPTERHOUSE_CHAT_SSE",
|
|
8
|
+
];
|
|
9
|
+
test("setup-env clears ambient Chapterhouse runtime overrides", async () => {
|
|
10
|
+
const originalValues = new Map(["CHAPTERHOUSE_DISABLE_DOTENV", ...RUNTIME_OVERRIDE_ENV_VARS].map((name) => [name, process.env[name]]));
|
|
11
|
+
try {
|
|
12
|
+
process.env.CHAPTERHOUSE_DISABLE_DOTENV = "0";
|
|
13
|
+
process.env.CHAPTERHOUSE_MODE = "team";
|
|
14
|
+
process.env.CHAPTERHOUSE_SELF_EDIT = "1";
|
|
15
|
+
process.env.CHAPTERHOUSE_WORKIQ_AUTO_INSTALL = "false";
|
|
16
|
+
process.env.CHAPTERHOUSE_CHAT_SSE = "0";
|
|
17
|
+
await import(`./setup-env.js?cache-bust=${Date.now()}`);
|
|
18
|
+
assert.equal(process.env.CHAPTERHOUSE_DISABLE_DOTENV, "1");
|
|
19
|
+
for (const name of RUNTIME_OVERRIDE_ENV_VARS) {
|
|
20
|
+
assert.equal(process.env[name], undefined, `${name} should be cleared by setup-env`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
for (const [name, value] of originalValues) {
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
delete process.env[name];
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
process.env[name] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=setup-env.test.js.map
|
package/package.json
CHANGED