@katyella/legio 0.1.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/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for the chat flow: POST and GET routes for coordinator and agent chat.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. POST /api/coordinator/chat stores message with from=human, to=coordinator, audience=human
|
|
6
|
+
* 2. POST /api/agents/:name/chat stores message in mail.db
|
|
7
|
+
* 3. GET /api/coordinator/chat/history returns bidirectional messages
|
|
8
|
+
* 4. GET /api/agents/:name/chat/history returns bidirectional messages
|
|
9
|
+
* 5. History excludes audience=agent messages
|
|
10
|
+
* 6. Limit param works
|
|
11
|
+
*
|
|
12
|
+
* Uses real SQLite databases in temp directories. No mocking of store logic.
|
|
13
|
+
* Mock setup mirrors routes.test.ts (see header there for rationale).
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { accessSync, constants as fsConstants, readFileSync } from "node:fs";
|
|
18
|
+
import { vi } from "vitest";
|
|
19
|
+
|
|
20
|
+
// Stub the global Bun object because production modules (e.g., config.ts) still use Bun APIs
|
|
21
|
+
// and have not yet been migrated to Node.js equivalents. This shim provides only the subset
|
|
22
|
+
// of the Bun API surface required by the code paths exercised by these chat tests.
|
|
23
|
+
vi.stubGlobal("Bun", {
|
|
24
|
+
file: (path: string) => ({
|
|
25
|
+
exists: async () => {
|
|
26
|
+
try {
|
|
27
|
+
accessSync(path, fsConstants.F_OK);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
text: async () => readFileSync(path, "utf-8"),
|
|
34
|
+
json: async () => JSON.parse(readFileSync(path, "utf-8")),
|
|
35
|
+
}),
|
|
36
|
+
spawn: () => {
|
|
37
|
+
throw new Error("Bun.spawn not available in Node.js/vitest environment");
|
|
38
|
+
},
|
|
39
|
+
sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
40
|
+
write: async (path: string, content: string) => {
|
|
41
|
+
const { writeFileSync } = await import("node:fs");
|
|
42
|
+
writeFileSync(path, content, "utf-8");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Mock the beads client so tests can run without `bd` on PATH.
|
|
47
|
+
vi.mock("../beads/client.ts", () => ({
|
|
48
|
+
createBeadsClient: () => ({
|
|
49
|
+
ready: async () => [],
|
|
50
|
+
list: async () => [],
|
|
51
|
+
show: async (id: string) => {
|
|
52
|
+
throw new Error(`bd not available: ${id}`);
|
|
53
|
+
},
|
|
54
|
+
create: async () => "bead-test-001",
|
|
55
|
+
claim: async () => {},
|
|
56
|
+
close: async () => {},
|
|
57
|
+
}),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Mock tmux so tests don't interfere with real developer tmux sessions.
|
|
61
|
+
const { mockIsSessionAlive, mockSendKeys } = vi.hoisted(() => ({
|
|
62
|
+
mockIsSessionAlive: vi.fn().mockResolvedValue(true),
|
|
63
|
+
mockSendKeys: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
}));
|
|
65
|
+
vi.mock("../worktree/tmux.ts", () => ({
|
|
66
|
+
isSessionAlive: mockIsSessionAlive,
|
|
67
|
+
sendKeys: mockSendKeys,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
71
|
+
import { tmpdir } from "node:os";
|
|
72
|
+
import { join } from "node:path";
|
|
73
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
74
|
+
import { createMailStore } from "../mail/store.ts";
|
|
75
|
+
import { handleApiRequest } from "../server/routes.ts";
|
|
76
|
+
import { createSessionStore } from "../sessions/store.ts";
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Helpers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function makeRequest(path: string, query?: Record<string, string>): Request {
|
|
83
|
+
const url = new URL(`http://localhost${path}`);
|
|
84
|
+
if (query) {
|
|
85
|
+
for (const [k, v] of Object.entries(query)) {
|
|
86
|
+
url.searchParams.set(k, v);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return new Request(url.toString(), { method: "GET" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makePostRequest(path: string, body: unknown): Request {
|
|
93
|
+
return new Request(`http://localhost${path}`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Test setup
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
let tempDir: string;
|
|
105
|
+
let legioDir: string;
|
|
106
|
+
let projectRoot: string;
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
tempDir = await (async () => {
|
|
110
|
+
const base = join(tmpdir(), `chat-flow-test-${Date.now()}`);
|
|
111
|
+
await mkdir(base, { recursive: true });
|
|
112
|
+
return base;
|
|
113
|
+
})();
|
|
114
|
+
legioDir = join(tempDir, ".legio");
|
|
115
|
+
projectRoot = tempDir;
|
|
116
|
+
await mkdir(legioDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
// Write minimal config.yaml for loadConfig
|
|
119
|
+
await writeFile(
|
|
120
|
+
join(legioDir, "config.yaml"),
|
|
121
|
+
[
|
|
122
|
+
"project:",
|
|
123
|
+
" name: test",
|
|
124
|
+
" canonicalBranch: main",
|
|
125
|
+
"agents:",
|
|
126
|
+
" maxDepth: 2",
|
|
127
|
+
"coordinator:",
|
|
128
|
+
" model: claude-sonnet-4-6",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(async () => {
|
|
134
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
async function dispatch(path: string, query?: Record<string, string>): Promise<Response> {
|
|
138
|
+
return handleApiRequest(makeRequest(path, query), legioDir, projectRoot);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function dispatchPost(path: string, body: unknown): Promise<Response> {
|
|
142
|
+
return handleApiRequest(makePostRequest(path, body), legioDir, projectRoot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Tests
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
describe("E2E: chat flow", () => {
|
|
150
|
+
it("POST /api/coordinator/chat stores message with from=human, to=coordinator, audience=human", async () => {
|
|
151
|
+
// Seed orchestrator-tmux.json so resolveTerminalSession returns a session name
|
|
152
|
+
await writeFile(
|
|
153
|
+
join(legioDir, "orchestrator-tmux.json"),
|
|
154
|
+
JSON.stringify({ tmuxSession: "legio-fake-orchestrator" }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const res = await dispatchPost("/api/coordinator/chat", { text: "hello coordinator" });
|
|
158
|
+
expect(res.status).toBe(201);
|
|
159
|
+
|
|
160
|
+
const body = (await res.json()) as { from: string; to: string; audience: string; body: string };
|
|
161
|
+
expect(body.from).toBe("human");
|
|
162
|
+
expect(body.to).toBe("coordinator");
|
|
163
|
+
expect(body.audience).toBe("human");
|
|
164
|
+
expect(body.body).toBe("hello coordinator");
|
|
165
|
+
|
|
166
|
+
// Verify the message was persisted in mail.db
|
|
167
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
168
|
+
const messages = mailStore.getAll({ from: "human", to: "coordinator" });
|
|
169
|
+
mailStore.close();
|
|
170
|
+
expect(messages.length).toBe(1);
|
|
171
|
+
expect(messages[0]?.audience).toBe("human");
|
|
172
|
+
expect(messages[0]?.body).toBe("hello coordinator");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("POST /api/agents/:name/chat stores message in mail.db", async () => {
|
|
176
|
+
// Seed sessions.db with test-agent in working state
|
|
177
|
+
const sessStore = createSessionStore(join(legioDir, "sessions.db"));
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
sessStore.upsert({
|
|
180
|
+
id: "sess-chat-001",
|
|
181
|
+
agentName: "test-agent",
|
|
182
|
+
capability: "builder",
|
|
183
|
+
worktreePath: "/tmp/wt/test-agent",
|
|
184
|
+
branchName: "legio/test-agent/task-1",
|
|
185
|
+
beadId: "task-1",
|
|
186
|
+
tmuxSession: "legio-fake-test-agent",
|
|
187
|
+
state: "working",
|
|
188
|
+
pid: 12345,
|
|
189
|
+
parentAgent: null,
|
|
190
|
+
depth: 1,
|
|
191
|
+
runId: "run-001",
|
|
192
|
+
startedAt: now,
|
|
193
|
+
lastActivity: now,
|
|
194
|
+
escalationLevel: 0,
|
|
195
|
+
stalledSince: null,
|
|
196
|
+
});
|
|
197
|
+
sessStore.close();
|
|
198
|
+
|
|
199
|
+
const res = await dispatchPost("/api/agents/test-agent/chat", { text: "hello agent" });
|
|
200
|
+
expect(res.status).toBe(201);
|
|
201
|
+
|
|
202
|
+
const body = (await res.json()) as { from: string; to: string; audience: string };
|
|
203
|
+
expect(body.from).toBe("human");
|
|
204
|
+
expect(body.to).toBe("test-agent");
|
|
205
|
+
expect(body.audience).toBe("human");
|
|
206
|
+
|
|
207
|
+
// Verify persisted in mail.db
|
|
208
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
209
|
+
const messages = mailStore.getAll({ from: "human", to: "test-agent" });
|
|
210
|
+
mailStore.close();
|
|
211
|
+
expect(messages.length).toBe(1);
|
|
212
|
+
expect(messages[0]?.body).toBe("hello agent");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("GET /api/coordinator/chat/history returns bidirectional messages", async () => {
|
|
216
|
+
// Seed both directions: human→coordinator and coordinator→human
|
|
217
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
218
|
+
mailStore.insert({
|
|
219
|
+
id: "msg-human-to-coord",
|
|
220
|
+
from: "human",
|
|
221
|
+
to: "coordinator",
|
|
222
|
+
subject: "chat",
|
|
223
|
+
body: "user message",
|
|
224
|
+
type: "status",
|
|
225
|
+
priority: "normal",
|
|
226
|
+
threadId: null,
|
|
227
|
+
audience: "human",
|
|
228
|
+
});
|
|
229
|
+
mailStore.insert({
|
|
230
|
+
id: "msg-coord-to-human",
|
|
231
|
+
from: "coordinator",
|
|
232
|
+
to: "human",
|
|
233
|
+
subject: "response",
|
|
234
|
+
body: "coordinator reply",
|
|
235
|
+
type: "result",
|
|
236
|
+
priority: "normal",
|
|
237
|
+
threadId: null,
|
|
238
|
+
audience: "human",
|
|
239
|
+
});
|
|
240
|
+
mailStore.close();
|
|
241
|
+
|
|
242
|
+
const res = await dispatch("/api/coordinator/chat/history");
|
|
243
|
+
expect(res.status).toBe(200);
|
|
244
|
+
|
|
245
|
+
const messages = (await res.json()) as Array<{ from: string; to: string }>;
|
|
246
|
+
expect(messages.length).toBe(2);
|
|
247
|
+
expect(messages.some((m) => m.from === "human" && m.to === "coordinator")).toBe(true);
|
|
248
|
+
expect(messages.some((m) => m.from === "coordinator" && m.to === "human")).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("GET /api/agents/:name/chat/history returns bidirectional messages", async () => {
|
|
252
|
+
// Seed both directions: human→agent and agent→human
|
|
253
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
254
|
+
mailStore.insert({
|
|
255
|
+
id: "msg-human-to-agent",
|
|
256
|
+
from: "human",
|
|
257
|
+
to: "my-agent",
|
|
258
|
+
subject: "chat",
|
|
259
|
+
body: "user asks",
|
|
260
|
+
type: "status",
|
|
261
|
+
priority: "normal",
|
|
262
|
+
threadId: null,
|
|
263
|
+
audience: "human",
|
|
264
|
+
});
|
|
265
|
+
mailStore.insert({
|
|
266
|
+
id: "msg-agent-to-human",
|
|
267
|
+
from: "my-agent",
|
|
268
|
+
to: "human",
|
|
269
|
+
subject: "response",
|
|
270
|
+
body: "agent responds",
|
|
271
|
+
type: "result",
|
|
272
|
+
priority: "normal",
|
|
273
|
+
threadId: null,
|
|
274
|
+
audience: "human",
|
|
275
|
+
});
|
|
276
|
+
mailStore.close();
|
|
277
|
+
|
|
278
|
+
const res = await dispatch("/api/agents/my-agent/chat/history");
|
|
279
|
+
expect(res.status).toBe(200);
|
|
280
|
+
|
|
281
|
+
const messages = (await res.json()) as Array<{ from: string; to: string }>;
|
|
282
|
+
expect(messages.length).toBe(2);
|
|
283
|
+
expect(messages.some((m) => m.from === "human" && m.to === "my-agent")).toBe(true);
|
|
284
|
+
expect(messages.some((m) => m.from === "my-agent" && m.to === "human")).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("GET /api/coordinator/chat/history excludes audience=agent messages", async () => {
|
|
288
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
289
|
+
mailStore.insert({
|
|
290
|
+
id: "msg-human-visible",
|
|
291
|
+
from: "human",
|
|
292
|
+
to: "coordinator",
|
|
293
|
+
subject: "chat",
|
|
294
|
+
body: "visible message",
|
|
295
|
+
type: "status",
|
|
296
|
+
priority: "normal",
|
|
297
|
+
threadId: null,
|
|
298
|
+
audience: "human",
|
|
299
|
+
});
|
|
300
|
+
mailStore.insert({
|
|
301
|
+
id: "msg-agent-hidden",
|
|
302
|
+
from: "human",
|
|
303
|
+
to: "coordinator",
|
|
304
|
+
subject: "internal",
|
|
305
|
+
body: "agent-only message",
|
|
306
|
+
type: "status",
|
|
307
|
+
priority: "normal",
|
|
308
|
+
threadId: null,
|
|
309
|
+
audience: "agent",
|
|
310
|
+
});
|
|
311
|
+
mailStore.close();
|
|
312
|
+
|
|
313
|
+
const res = await dispatch("/api/coordinator/chat/history");
|
|
314
|
+
expect(res.status).toBe(200);
|
|
315
|
+
|
|
316
|
+
const messages = (await res.json()) as Array<{ id: string; audience: string }>;
|
|
317
|
+
// Only the human-audience message should appear
|
|
318
|
+
expect(messages.length).toBe(1);
|
|
319
|
+
expect(messages[0]?.id).toBe("msg-human-visible");
|
|
320
|
+
expect(messages.some((m) => m.id === "msg-agent-hidden")).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("GET /api/coordinator/chat/history respects limit param", async () => {
|
|
324
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
325
|
+
for (let i = 0; i < 5; i++) {
|
|
326
|
+
mailStore.insert({
|
|
327
|
+
id: `msg-limit-${i}`,
|
|
328
|
+
from: "human",
|
|
329
|
+
to: "coordinator",
|
|
330
|
+
subject: "chat",
|
|
331
|
+
body: `message ${i}`,
|
|
332
|
+
type: "status",
|
|
333
|
+
priority: "normal",
|
|
334
|
+
threadId: null,
|
|
335
|
+
audience: "human",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
mailStore.close();
|
|
339
|
+
|
|
340
|
+
const res = await dispatch("/api/coordinator/chat/history", { limit: "2" });
|
|
341
|
+
expect(res.status).toBe(200);
|
|
342
|
+
|
|
343
|
+
const messages = (await res.json()) as unknown[];
|
|
344
|
+
expect(messages.length).toBe(2);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { access, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
/** Test helper: check whether a file exists using Node.js fs/promises. */
|
|
6
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
7
|
+
return access(path).then(
|
|
8
|
+
() => true,
|
|
9
|
+
() => false,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Test helper: read a file as UTF-8 text using Node.js fs/promises. */
|
|
14
|
+
function fileText(path: string): Promise<string> {
|
|
15
|
+
return readFile(path, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
import { createManifestLoader } from "../agents/manifest.ts";
|
|
19
|
+
import { writeOverlay } from "../agents/overlay.ts";
|
|
20
|
+
import { initCommand } from "../commands/init.ts";
|
|
21
|
+
import { loadConfig } from "../config.ts";
|
|
22
|
+
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
23
|
+
import type { OverlayConfig } from "../types.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* E2E test: init→sling lifecycle on a throwaway external project.
|
|
27
|
+
*
|
|
28
|
+
* Validates the "project-agnostic" promise by running legio init on a
|
|
29
|
+
* fresh temp git repo (NOT the legio repo itself), then verifying all
|
|
30
|
+
* artifacts, loading config + manifest via real APIs, and generating an overlay.
|
|
31
|
+
*
|
|
32
|
+
* Uses real filesystem and real git repos. No mocks.
|
|
33
|
+
* Suppresses stdout because initCommand prints status lines.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const EXPECTED_AGENT_DEFS = [
|
|
37
|
+
"builder.md",
|
|
38
|
+
"coordinator.md",
|
|
39
|
+
"cto.md",
|
|
40
|
+
"gateway.md",
|
|
41
|
+
"lead.md",
|
|
42
|
+
"merger.md",
|
|
43
|
+
"monitor.md",
|
|
44
|
+
"reviewer.md",
|
|
45
|
+
"scout.md",
|
|
46
|
+
"supervisor.md",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
describe("E2E: init→sling lifecycle on external project", () => {
|
|
50
|
+
let tempDir: string;
|
|
51
|
+
let originalCwd: string;
|
|
52
|
+
let originalWrite: typeof process.stdout.write;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
tempDir = await createTempGitRepo();
|
|
56
|
+
originalCwd = process.cwd();
|
|
57
|
+
process.chdir(tempDir);
|
|
58
|
+
|
|
59
|
+
// Suppress stdout noise from initCommand
|
|
60
|
+
originalWrite = process.stdout.write;
|
|
61
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
process.chdir(originalCwd);
|
|
66
|
+
process.stdout.write = originalWrite;
|
|
67
|
+
await cleanupTempDir(tempDir);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("init creates all expected artifacts", async () => {
|
|
71
|
+
await initCommand([]);
|
|
72
|
+
|
|
73
|
+
const legioDir = join(tempDir, ".legio");
|
|
74
|
+
|
|
75
|
+
// config.yaml exists
|
|
76
|
+
expect(await fileExists(join(legioDir, "config.yaml"))).toBe(true);
|
|
77
|
+
|
|
78
|
+
// agent-manifest.json exists and is valid JSON
|
|
79
|
+
expect(await fileExists(join(legioDir, "agent-manifest.json"))).toBe(true);
|
|
80
|
+
const manifestText = await fileText(join(legioDir, "agent-manifest.json"));
|
|
81
|
+
const manifestJson = JSON.parse(manifestText);
|
|
82
|
+
expect(manifestJson).toBeDefined();
|
|
83
|
+
expect(manifestJson.version).toBe("1.0");
|
|
84
|
+
expect(typeof manifestJson.agents).toBe("object");
|
|
85
|
+
|
|
86
|
+
// hooks.json exists
|
|
87
|
+
expect(await fileExists(join(legioDir, "hooks.json"))).toBe(true);
|
|
88
|
+
|
|
89
|
+
// .gitignore exists
|
|
90
|
+
expect(await fileExists(join(legioDir, ".gitignore"))).toBe(true);
|
|
91
|
+
|
|
92
|
+
// agent-defs/ contains all 9 agent definition files
|
|
93
|
+
const agentDefsDir = join(legioDir, "agent-defs");
|
|
94
|
+
const agentDefFiles = (await readdir(agentDefsDir)).filter((f) => f.endsWith(".md")).sort();
|
|
95
|
+
expect(agentDefFiles).toEqual(EXPECTED_AGENT_DEFS);
|
|
96
|
+
|
|
97
|
+
// Required subdirectories exist
|
|
98
|
+
const expectedDirs = ["agents", "worktrees", "specs", "logs"];
|
|
99
|
+
for (const dirName of expectedDirs) {
|
|
100
|
+
const dirPath = join(legioDir, dirName);
|
|
101
|
+
const dirStat = await stat(dirPath);
|
|
102
|
+
expect(dirStat.isDirectory()).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("loadConfig returns valid config pointing to temp dir", async () => {
|
|
107
|
+
await initCommand([]);
|
|
108
|
+
|
|
109
|
+
const config = await loadConfig(tempDir);
|
|
110
|
+
|
|
111
|
+
// project.root should point to the temp directory
|
|
112
|
+
expect(config.project.root).toBe(tempDir);
|
|
113
|
+
|
|
114
|
+
// agents.baseDir should be the relative path to agent-defs
|
|
115
|
+
expect(config.agents.baseDir).toBe(".legio/agent-defs");
|
|
116
|
+
|
|
117
|
+
// canonicalBranch should be detected (main for our test repos)
|
|
118
|
+
expect(config.project.canonicalBranch).toBeTruthy();
|
|
119
|
+
|
|
120
|
+
// name should be set (from dir basename or git remote)
|
|
121
|
+
expect(config.project.name).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("manifest loads successfully with all 9 agents", async () => {
|
|
125
|
+
await initCommand([]);
|
|
126
|
+
|
|
127
|
+
const manifestPath = join(tempDir, ".legio", "agent-manifest.json");
|
|
128
|
+
const agentDefsDir = join(tempDir, ".legio", "agent-defs");
|
|
129
|
+
const loader = createManifestLoader(manifestPath, agentDefsDir);
|
|
130
|
+
|
|
131
|
+
const manifest = await loader.load();
|
|
132
|
+
|
|
133
|
+
// All 9 agents present
|
|
134
|
+
const agentNames = Object.keys(manifest.agents).sort();
|
|
135
|
+
expect(agentNames).toEqual([
|
|
136
|
+
"builder",
|
|
137
|
+
"coordinator",
|
|
138
|
+
"cto",
|
|
139
|
+
"lead",
|
|
140
|
+
"merger",
|
|
141
|
+
"monitor",
|
|
142
|
+
"reviewer",
|
|
143
|
+
"scout",
|
|
144
|
+
"supervisor",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Each agent has a valid file reference
|
|
148
|
+
for (const [_name, def] of Object.entries(manifest.agents)) {
|
|
149
|
+
expect(def.file).toMatch(/\.md$/);
|
|
150
|
+
// Verify the referenced .md file actually exists
|
|
151
|
+
expect(await fileExists(join(agentDefsDir, def.file))).toBe(true);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validation returns no errors
|
|
155
|
+
const errors = loader.validate();
|
|
156
|
+
expect(errors).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("manifest capability index is consistent", async () => {
|
|
160
|
+
await initCommand([]);
|
|
161
|
+
|
|
162
|
+
const manifestPath = join(tempDir, ".legio", "agent-manifest.json");
|
|
163
|
+
const agentDefsDir = join(tempDir, ".legio", "agent-defs");
|
|
164
|
+
const loader = createManifestLoader(manifestPath, agentDefsDir);
|
|
165
|
+
|
|
166
|
+
const manifest = await loader.load();
|
|
167
|
+
|
|
168
|
+
// capabilityIndex should map capabilities to agent names
|
|
169
|
+
expect(Object.keys(manifest.capabilityIndex).length).toBeGreaterThan(0);
|
|
170
|
+
|
|
171
|
+
// Each capability in the index should reference agents that declare it
|
|
172
|
+
for (const [cap, names] of Object.entries(manifest.capabilityIndex)) {
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
const agent = manifest.agents[name];
|
|
175
|
+
expect(agent).toBeDefined();
|
|
176
|
+
expect(agent?.capabilities).toContain(cap);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("overlay generation works for external project", async () => {
|
|
182
|
+
await initCommand([]);
|
|
183
|
+
|
|
184
|
+
const agentDefsDir = join(tempDir, ".legio", "agent-defs");
|
|
185
|
+
const baseDefinition = await fileText(join(agentDefsDir, "builder.md"));
|
|
186
|
+
|
|
187
|
+
const overlayConfig: OverlayConfig = {
|
|
188
|
+
agentName: "test-agent",
|
|
189
|
+
beadId: "test-bead-001",
|
|
190
|
+
specPath: null,
|
|
191
|
+
branchName: "legio/test-agent/test-bead-001",
|
|
192
|
+
worktreePath: join(tempDir, ".legio", "worktrees", "test-agent"),
|
|
193
|
+
fileScope: [],
|
|
194
|
+
mulchDomains: [],
|
|
195
|
+
parentAgent: null,
|
|
196
|
+
depth: 0,
|
|
197
|
+
canSpawn: false,
|
|
198
|
+
capability: "builder",
|
|
199
|
+
baseDefinition,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Write the overlay into a subdirectory of the temp dir (simulating a worktree)
|
|
203
|
+
const worktreePath = join(tempDir, ".legio", "worktrees", "test-agent");
|
|
204
|
+
const { mkdir } = await import("node:fs/promises");
|
|
205
|
+
await mkdir(worktreePath, { recursive: true });
|
|
206
|
+
|
|
207
|
+
await writeOverlay(worktreePath, overlayConfig, tempDir);
|
|
208
|
+
|
|
209
|
+
// Verify the overlay was written
|
|
210
|
+
const overlayPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
211
|
+
expect(await fileExists(overlayPath)).toBe(true);
|
|
212
|
+
|
|
213
|
+
const content = await fileText(overlayPath);
|
|
214
|
+
|
|
215
|
+
// Verify template placeholders were replaced
|
|
216
|
+
expect(content).toContain("test-agent");
|
|
217
|
+
expect(content).toContain("test-bead-001");
|
|
218
|
+
expect(content).toContain("legio/test-agent/test-bead-001");
|
|
219
|
+
expect(content).not.toContain("{{AGENT_NAME}}");
|
|
220
|
+
expect(content).not.toContain("{{BEAD_ID}}");
|
|
221
|
+
expect(content).not.toContain("{{BRANCH_NAME}}");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("full init→config→manifest→overlay pipeline succeeds", async () => {
|
|
225
|
+
// This test validates the entire lifecycle in sequence:
|
|
226
|
+
// init → load config → load manifest → generate overlay
|
|
227
|
+
|
|
228
|
+
// Step 1: Init
|
|
229
|
+
await initCommand([]);
|
|
230
|
+
|
|
231
|
+
// Step 2: Load config
|
|
232
|
+
const config = await loadConfig(tempDir);
|
|
233
|
+
expect(config.project.root).toBe(tempDir);
|
|
234
|
+
|
|
235
|
+
// Step 3: Load manifest using config paths
|
|
236
|
+
const manifestPath = join(config.project.root, config.agents.manifestPath);
|
|
237
|
+
const agentDefsDir = join(config.project.root, config.agents.baseDir);
|
|
238
|
+
const loader = createManifestLoader(manifestPath, agentDefsDir);
|
|
239
|
+
await loader.load();
|
|
240
|
+
|
|
241
|
+
// Verify builder agent exists (the one we'll use for overlay)
|
|
242
|
+
const builder = loader.getAgent("builder");
|
|
243
|
+
expect(builder).toBeDefined();
|
|
244
|
+
expect(builder?.canSpawn).toBe(false);
|
|
245
|
+
|
|
246
|
+
// Verify lead agent can spawn
|
|
247
|
+
const lead = loader.getAgent("lead");
|
|
248
|
+
expect(lead).toBeDefined();
|
|
249
|
+
expect(lead?.canSpawn).toBe(true);
|
|
250
|
+
|
|
251
|
+
// Step 4: Generate overlay using a realistic config
|
|
252
|
+
const builderDef = await fileText(join(agentDefsDir, "builder.md"));
|
|
253
|
+
const overlayConfig: OverlayConfig = {
|
|
254
|
+
agentName: "lifecycle-builder",
|
|
255
|
+
beadId: "lifecycle-001",
|
|
256
|
+
specPath: join(tempDir, ".legio", "specs", "lifecycle-001.md"),
|
|
257
|
+
branchName: "legio/lifecycle-builder/lifecycle-001",
|
|
258
|
+
worktreePath: join(tempDir, ".legio", "worktrees", "lifecycle-builder"),
|
|
259
|
+
fileScope: ["src/main.ts", "src/utils.ts"],
|
|
260
|
+
mulchDomains: ["typescript"],
|
|
261
|
+
parentAgent: "orchestrator",
|
|
262
|
+
depth: 0,
|
|
263
|
+
canSpawn: false,
|
|
264
|
+
capability: "builder",
|
|
265
|
+
baseDefinition: builderDef,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const worktreePath = join(tempDir, ".legio", "worktrees", "lifecycle-builder");
|
|
269
|
+
const { mkdir } = await import("node:fs/promises");
|
|
270
|
+
await mkdir(worktreePath, { recursive: true });
|
|
271
|
+
|
|
272
|
+
await writeOverlay(worktreePath, overlayConfig, tempDir);
|
|
273
|
+
|
|
274
|
+
const overlayContent = await fileText(join(worktreePath, ".claude", "CLAUDE.md"));
|
|
275
|
+
|
|
276
|
+
// Verify all overlay fields rendered correctly
|
|
277
|
+
expect(overlayContent).toContain("lifecycle-builder");
|
|
278
|
+
expect(overlayContent).toContain("lifecycle-001");
|
|
279
|
+
expect(overlayContent).toContain("legio/lifecycle-builder/lifecycle-001");
|
|
280
|
+
expect(overlayContent).toContain("orchestrator");
|
|
281
|
+
expect(overlayContent).toContain("`src/main.ts`");
|
|
282
|
+
expect(overlayContent).toContain("`src/utils.ts`");
|
|
283
|
+
expect(overlayContent).toContain("mulch prime typescript");
|
|
284
|
+
|
|
285
|
+
// No unresolved placeholders
|
|
286
|
+
expect(overlayContent).not.toMatch(/\{\{[A-Z_]+\}\}/);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { isRunningAsRoot } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
describe("isRunningAsRoot", () => {
|
|
5
|
+
test("returns false in normal test environment (not root)", () => {
|
|
6
|
+
// This test runs as a regular user during development and CI.
|
|
7
|
+
// process.getuid() should return a non-zero UID.
|
|
8
|
+
if (typeof process.getuid === "function") {
|
|
9
|
+
expect(process.getuid()).not.toBe(0);
|
|
10
|
+
}
|
|
11
|
+
expect(isRunningAsRoot()).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("returns false when process.getuid is not available (Windows)", () => {
|
|
15
|
+
// Simulate a platform without process.getuid by overriding it to undefined
|
|
16
|
+
const original = process.getuid;
|
|
17
|
+
(process as unknown as Record<string, unknown>).getuid = undefined;
|
|
18
|
+
expect(isRunningAsRoot()).toBe(false);
|
|
19
|
+
process.getuid = original;
|
|
20
|
+
});
|
|
21
|
+
});
|