@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,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for legio up command.
|
|
3
|
+
*
|
|
4
|
+
* Uses DI (UpDeps) to inject mock subprocess calls, filesystem checks,
|
|
5
|
+
* and PID reads. No real init/server/coordinator in tests.
|
|
6
|
+
*
|
|
7
|
+
* WHY DI instead of mock.module: mock.module() in vitest is process-global
|
|
8
|
+
* and leaks across test files. DI keeps mocks scoped to each test invocation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { ServerError, ValidationError } from "../errors.ts";
|
|
13
|
+
import type { UpDeps } from "./up.ts";
|
|
14
|
+
import { upCommand } from "./up.ts";
|
|
15
|
+
|
|
16
|
+
/** Builds a mock runCommand with configurable results per command prefix. */
|
|
17
|
+
function makeRunCommand(
|
|
18
|
+
responses: Record<string, { stdout: string; stderr: string; exitCode: number }>,
|
|
19
|
+
): NonNullable<UpDeps["_runCommand"]> {
|
|
20
|
+
return async (cmd) => {
|
|
21
|
+
const key = cmd.join(" ");
|
|
22
|
+
// Find matching prefix
|
|
23
|
+
for (const [prefix, result] of Object.entries(responses)) {
|
|
24
|
+
if (key.startsWith(prefix)) return result;
|
|
25
|
+
}
|
|
26
|
+
return { stdout: "", stderr: `Unexpected command: ${key}`, exitCode: 1 };
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Standard successful subprocess responses. */
|
|
31
|
+
const GIT_OK = { stdout: "true\n", stderr: "", exitCode: 0 };
|
|
32
|
+
const INIT_OK = { stdout: "Initialized .legio/\n", stderr: "", exitCode: 0 };
|
|
33
|
+
const SERVER_START_OK = { stdout: "Server started\n", stderr: "", exitCode: 0 };
|
|
34
|
+
const GATEWAY_STATUS_NOT_RUNNING = {
|
|
35
|
+
stdout: JSON.stringify({ running: false }),
|
|
36
|
+
stderr: "",
|
|
37
|
+
exitCode: 0,
|
|
38
|
+
};
|
|
39
|
+
const GATEWAY_START_OK = { stdout: "Gateway started\n", stderr: "", exitCode: 0 };
|
|
40
|
+
|
|
41
|
+
describe("upCommand", () => {
|
|
42
|
+
let capturedStdout: string;
|
|
43
|
+
let _capturedStderr: string;
|
|
44
|
+
let originalStdout: typeof process.stdout.write;
|
|
45
|
+
let originalStderr: typeof process.stderr.write;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
capturedStdout = "";
|
|
49
|
+
_capturedStderr = "";
|
|
50
|
+
originalStdout = process.stdout.write;
|
|
51
|
+
originalStderr = process.stderr.write;
|
|
52
|
+
process.stdout.write = vi.fn((chunk: unknown) => {
|
|
53
|
+
capturedStdout += String(chunk);
|
|
54
|
+
return true;
|
|
55
|
+
}) as typeof process.stdout.write;
|
|
56
|
+
process.stderr.write = vi.fn((chunk: unknown) => {
|
|
57
|
+
_capturedStderr += String(chunk);
|
|
58
|
+
return true;
|
|
59
|
+
}) as typeof process.stderr.write;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
process.stdout.write = originalStdout;
|
|
64
|
+
process.stderr.write = originalStderr;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("prints help for --help", async () => {
|
|
68
|
+
await upCommand(["--help"]);
|
|
69
|
+
expect(capturedStdout).toContain("legio up");
|
|
70
|
+
expect(capturedStdout).toContain("--port");
|
|
71
|
+
expect(capturedStdout).toContain("--no-open");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("prints help for -h", async () => {
|
|
75
|
+
await upCommand(["-h"]);
|
|
76
|
+
expect(capturedStdout).toContain("legio up");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("throws ValidationError when not in a git repo", async () => {
|
|
80
|
+
const deps: UpDeps = {
|
|
81
|
+
_runCommand: makeRunCommand({
|
|
82
|
+
"git rev-parse": { stdout: "", stderr: "not a git repo", exitCode: 128 },
|
|
83
|
+
}),
|
|
84
|
+
_fileExists: async () => false,
|
|
85
|
+
_projectRoot: "/tmp/not-a-repo",
|
|
86
|
+
};
|
|
87
|
+
await expect(upCommand([], deps)).rejects.toThrow(ValidationError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("throws ValidationError for invalid port", async () => {
|
|
91
|
+
await expect(upCommand(["--port", "99999"])).rejects.toThrow(ValidationError);
|
|
92
|
+
await expect(upCommand(["--port", "abc"])).rejects.toThrow(ValidationError);
|
|
93
|
+
await expect(upCommand(["--port", "0"])).rejects.toThrow(ValidationError);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("runs init when .legio/ not initialized", async () => {
|
|
97
|
+
const commands: string[][] = [];
|
|
98
|
+
const deps: UpDeps = {
|
|
99
|
+
_runCommand: async (cmd) => {
|
|
100
|
+
commands.push(cmd);
|
|
101
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
102
|
+
if (cmd[0] === "legio" && cmd[1] === "init") return INIT_OK;
|
|
103
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
104
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
105
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
106
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
107
|
+
return GATEWAY_START_OK;
|
|
108
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
109
|
+
},
|
|
110
|
+
_fileExists: async () => false, // config.yaml not found, no server.pid
|
|
111
|
+
_readPid: async () => null,
|
|
112
|
+
_isProcessRunning: () => false,
|
|
113
|
+
_openBrowser: () => {},
|
|
114
|
+
_projectRoot: "/tmp/test-project",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await upCommand([], deps);
|
|
118
|
+
|
|
119
|
+
const ranInit = commands.some((c) => c[0] === "legio" && c[1] === "init");
|
|
120
|
+
expect(ranInit).toBe(true);
|
|
121
|
+
// Should NOT have --force
|
|
122
|
+
const initCmd = commands.find((c) => c[0] === "legio" && c[1] === "init");
|
|
123
|
+
expect(initCmd).not.toContain("--force");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("skips init when already initialized", async () => {
|
|
127
|
+
const commands: string[][] = [];
|
|
128
|
+
const deps: UpDeps = {
|
|
129
|
+
_runCommand: async (cmd) => {
|
|
130
|
+
commands.push(cmd);
|
|
131
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
132
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
133
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
134
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
135
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
136
|
+
return GATEWAY_START_OK;
|
|
137
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
138
|
+
},
|
|
139
|
+
_fileExists: async (p) => p.endsWith("config.yaml"), // config exists, no server.pid
|
|
140
|
+
_readPid: async () => null,
|
|
141
|
+
_isProcessRunning: () => false,
|
|
142
|
+
_openBrowser: () => {},
|
|
143
|
+
_projectRoot: "/tmp/test-project",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await upCommand([], deps);
|
|
147
|
+
|
|
148
|
+
const ranInit = commands.some((c) => c[0] === "legio" && c[1] === "init");
|
|
149
|
+
expect(ranInit).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("runs init --force when --force flag and already initialized", async () => {
|
|
153
|
+
const commands: string[][] = [];
|
|
154
|
+
const deps: UpDeps = {
|
|
155
|
+
_runCommand: async (cmd) => {
|
|
156
|
+
commands.push(cmd);
|
|
157
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
158
|
+
if (cmd[0] === "legio" && cmd[1] === "init") return INIT_OK;
|
|
159
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
160
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
161
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
162
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
163
|
+
return GATEWAY_START_OK;
|
|
164
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
165
|
+
},
|
|
166
|
+
_fileExists: async (p) => p.endsWith("config.yaml"), // initialized, no server.pid
|
|
167
|
+
_readPid: async () => null,
|
|
168
|
+
_isProcessRunning: () => false,
|
|
169
|
+
_openBrowser: () => {},
|
|
170
|
+
_projectRoot: "/tmp/test-project",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
await upCommand(["--force"], deps);
|
|
174
|
+
|
|
175
|
+
const initCmd = commands.find((c) => c[0] === "legio" && c[1] === "init");
|
|
176
|
+
expect(initCmd).toContain("--force");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("skips server start when server already running", async () => {
|
|
180
|
+
const commands: string[][] = [];
|
|
181
|
+
const deps: UpDeps = {
|
|
182
|
+
_runCommand: async (cmd) => {
|
|
183
|
+
commands.push(cmd);
|
|
184
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
185
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
186
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
187
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
188
|
+
return GATEWAY_START_OK;
|
|
189
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
190
|
+
},
|
|
191
|
+
_fileExists: async () => true, // config.yaml exists AND server.pid exists
|
|
192
|
+
_readPid: async () => 12345,
|
|
193
|
+
_isProcessRunning: () => true, // server is alive
|
|
194
|
+
_openBrowser: () => {},
|
|
195
|
+
_projectRoot: "/tmp/test-project",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await upCommand([], deps);
|
|
199
|
+
|
|
200
|
+
const serverStarted = commands.some(
|
|
201
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "start",
|
|
202
|
+
);
|
|
203
|
+
expect(serverStarted).toBe(false);
|
|
204
|
+
expect(capturedStdout).toContain("already running");
|
|
205
|
+
expect(capturedStdout).toContain("12345");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("starts server when not running (dead PID)", async () => {
|
|
209
|
+
const commands: string[][] = [];
|
|
210
|
+
const deps: UpDeps = {
|
|
211
|
+
_runCommand: async (cmd) => {
|
|
212
|
+
commands.push(cmd);
|
|
213
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
214
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
215
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
216
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
217
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
218
|
+
return GATEWAY_START_OK;
|
|
219
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
220
|
+
},
|
|
221
|
+
_fileExists: async (p) => p.endsWith("config.yaml") || p.endsWith("server.pid"),
|
|
222
|
+
_readPid: async () => 12345,
|
|
223
|
+
_isProcessRunning: () => false, // PID exists but process is dead
|
|
224
|
+
_openBrowser: () => {},
|
|
225
|
+
_projectRoot: "/tmp/test-project",
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await upCommand([], deps);
|
|
229
|
+
|
|
230
|
+
const serverStarted = commands.some(
|
|
231
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "start",
|
|
232
|
+
);
|
|
233
|
+
expect(serverStarted).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("starts server when no PID file", async () => {
|
|
237
|
+
const commands: string[][] = [];
|
|
238
|
+
const deps: UpDeps = {
|
|
239
|
+
_runCommand: async (cmd) => {
|
|
240
|
+
commands.push(cmd);
|
|
241
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
242
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
243
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
244
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
245
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
246
|
+
return GATEWAY_START_OK;
|
|
247
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
248
|
+
},
|
|
249
|
+
_fileExists: async (p) => p.endsWith("config.yaml"), // no server.pid
|
|
250
|
+
_readPid: async () => null,
|
|
251
|
+
_isProcessRunning: () => false,
|
|
252
|
+
_openBrowser: () => {},
|
|
253
|
+
_projectRoot: "/tmp/test-project",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
await upCommand([], deps);
|
|
257
|
+
|
|
258
|
+
const serverCmd = commands.find(
|
|
259
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "start",
|
|
260
|
+
);
|
|
261
|
+
expect(serverCmd).toBeDefined();
|
|
262
|
+
// Verify default port and host
|
|
263
|
+
expect(serverCmd).toContain("4173");
|
|
264
|
+
expect(serverCmd).toContain("127.0.0.1");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("passes custom port and host to server start", async () => {
|
|
268
|
+
const commands: string[][] = [];
|
|
269
|
+
const deps: UpDeps = {
|
|
270
|
+
_runCommand: async (cmd) => {
|
|
271
|
+
commands.push(cmd);
|
|
272
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
273
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
274
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
275
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
276
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
277
|
+
return GATEWAY_START_OK;
|
|
278
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
279
|
+
},
|
|
280
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
281
|
+
_readPid: async () => null,
|
|
282
|
+
_isProcessRunning: () => false,
|
|
283
|
+
_openBrowser: () => {},
|
|
284
|
+
_projectRoot: "/tmp/test-project",
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
await upCommand(["--port", "8080", "--host", "0.0.0.0"], deps);
|
|
288
|
+
|
|
289
|
+
const serverCmd = commands.find(
|
|
290
|
+
(c) => c[0] === "legio" && c[1] === "server" && c[2] === "start",
|
|
291
|
+
);
|
|
292
|
+
expect(serverCmd).toContain("8080");
|
|
293
|
+
expect(serverCmd).toContain("0.0.0.0");
|
|
294
|
+
expect(serverCmd).toContain("--daemon");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does not open browser when --no-open is set", async () => {
|
|
298
|
+
let browserOpened = false;
|
|
299
|
+
const deps: UpDeps = {
|
|
300
|
+
_runCommand: async (cmd) => {
|
|
301
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
302
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
303
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
304
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
305
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
306
|
+
return GATEWAY_START_OK;
|
|
307
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
308
|
+
},
|
|
309
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
310
|
+
_readPid: async () => null,
|
|
311
|
+
_isProcessRunning: () => false,
|
|
312
|
+
_openBrowser: () => {
|
|
313
|
+
browserOpened = true;
|
|
314
|
+
},
|
|
315
|
+
_projectRoot: "/tmp/test-project",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
await upCommand(["--no-open"], deps);
|
|
319
|
+
expect(browserOpened).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("opens browser when server starts (default)", async () => {
|
|
323
|
+
let openedUrl = "";
|
|
324
|
+
const deps: UpDeps = {
|
|
325
|
+
_runCommand: async (cmd) => {
|
|
326
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
327
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
328
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
329
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
330
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
331
|
+
return GATEWAY_START_OK;
|
|
332
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
333
|
+
},
|
|
334
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
335
|
+
_readPid: async () => null,
|
|
336
|
+
_isProcessRunning: () => false,
|
|
337
|
+
_openBrowser: (url) => {
|
|
338
|
+
openedUrl = url;
|
|
339
|
+
},
|
|
340
|
+
_projectRoot: "/tmp/test-project",
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
await upCommand([], deps);
|
|
344
|
+
expect(openedUrl).toBe("http://127.0.0.1:4173");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("throws ServerError when server start fails", async () => {
|
|
348
|
+
const deps: UpDeps = {
|
|
349
|
+
_runCommand: async (cmd) => {
|
|
350
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
351
|
+
if (cmd[0] === "legio" && cmd[1] === "server") {
|
|
352
|
+
return { stdout: "", stderr: "port already in use", exitCode: 1 };
|
|
353
|
+
}
|
|
354
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
355
|
+
},
|
|
356
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
357
|
+
_readPid: async () => null,
|
|
358
|
+
_isProcessRunning: () => false,
|
|
359
|
+
_openBrowser: () => {},
|
|
360
|
+
_projectRoot: "/tmp/test-project",
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
await expect(upCommand([], deps)).rejects.toThrow(ServerError);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("throws ValidationError when init fails", async () => {
|
|
367
|
+
const deps: UpDeps = {
|
|
368
|
+
_runCommand: async (cmd) => {
|
|
369
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
370
|
+
if (cmd[0] === "legio" && cmd[1] === "init") {
|
|
371
|
+
return { stdout: "", stderr: "init failed", exitCode: 1 };
|
|
372
|
+
}
|
|
373
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
374
|
+
},
|
|
375
|
+
_fileExists: async () => false, // not initialized
|
|
376
|
+
_readPid: async () => null,
|
|
377
|
+
_isProcessRunning: () => false,
|
|
378
|
+
_openBrowser: () => {},
|
|
379
|
+
_projectRoot: "/tmp/test-project",
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await expect(upCommand([], deps)).rejects.toThrow(ValidationError);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("outputs JSON when --json is passed", async () => {
|
|
386
|
+
const deps: UpDeps = {
|
|
387
|
+
_runCommand: async (cmd) => {
|
|
388
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
389
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
390
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
391
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
392
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
393
|
+
return GATEWAY_START_OK;
|
|
394
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
395
|
+
},
|
|
396
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
397
|
+
_readPid: async () => null,
|
|
398
|
+
_isProcessRunning: () => false,
|
|
399
|
+
_openBrowser: () => {},
|
|
400
|
+
_projectRoot: "/tmp/test-project",
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
await upCommand(["--json"], deps);
|
|
404
|
+
|
|
405
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
406
|
+
url: string;
|
|
407
|
+
initRan: boolean;
|
|
408
|
+
serverStarted: boolean;
|
|
409
|
+
serverAlreadyRunning: boolean;
|
|
410
|
+
gatewayStarted: boolean;
|
|
411
|
+
gatewayAlreadyRunning: boolean;
|
|
412
|
+
};
|
|
413
|
+
expect(parsed.url).toBe("http://127.0.0.1:4173");
|
|
414
|
+
expect(parsed.serverStarted).toBe(true);
|
|
415
|
+
expect(parsed.initRan).toBe(false);
|
|
416
|
+
expect(parsed.serverAlreadyRunning).toBe(false);
|
|
417
|
+
expect(parsed.gatewayStarted).toBe(true);
|
|
418
|
+
expect(parsed.gatewayAlreadyRunning).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("JSON output shows serverAlreadyRunning when server is up", async () => {
|
|
422
|
+
const deps: UpDeps = {
|
|
423
|
+
_runCommand: async (cmd) => {
|
|
424
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
425
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
426
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
427
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
428
|
+
return GATEWAY_START_OK;
|
|
429
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
430
|
+
},
|
|
431
|
+
_fileExists: async () => true,
|
|
432
|
+
_readPid: async () => 99,
|
|
433
|
+
_isProcessRunning: () => true,
|
|
434
|
+
_openBrowser: () => {},
|
|
435
|
+
_projectRoot: "/tmp/test-project",
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
await upCommand(["--json"], deps);
|
|
439
|
+
|
|
440
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
441
|
+
serverAlreadyRunning: boolean;
|
|
442
|
+
serverStarted: boolean;
|
|
443
|
+
};
|
|
444
|
+
expect(parsed.serverAlreadyRunning).toBe(true);
|
|
445
|
+
expect(parsed.serverStarted).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("prints URL summary when server starts", async () => {
|
|
449
|
+
const deps: UpDeps = {
|
|
450
|
+
_runCommand: async (cmd) => {
|
|
451
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
452
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
453
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
454
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
455
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
456
|
+
return GATEWAY_START_OK;
|
|
457
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
458
|
+
},
|
|
459
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
460
|
+
_readPid: async () => null,
|
|
461
|
+
_isProcessRunning: () => false,
|
|
462
|
+
_openBrowser: () => {},
|
|
463
|
+
_projectRoot: "/tmp/test-project",
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
await upCommand(["--port", "5000"], deps);
|
|
467
|
+
expect(capturedStdout).toContain("http://127.0.0.1:5000");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("starts gateway when not already running", async () => {
|
|
471
|
+
const commands: string[][] = [];
|
|
472
|
+
const deps: UpDeps = {
|
|
473
|
+
_runCommand: async (cmd) => {
|
|
474
|
+
commands.push(cmd);
|
|
475
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
476
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
477
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
478
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
479
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
480
|
+
return GATEWAY_START_OK;
|
|
481
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
482
|
+
},
|
|
483
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
484
|
+
_readPid: async () => null,
|
|
485
|
+
_isProcessRunning: () => false,
|
|
486
|
+
_openBrowser: () => {},
|
|
487
|
+
_projectRoot: "/tmp/test-project",
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
await upCommand(["--json"], deps);
|
|
491
|
+
|
|
492
|
+
const gatewayStartCalled = commands.some(
|
|
493
|
+
(c) =>
|
|
494
|
+
c[0] === "legio" && c[1] === "gateway" && c[2] === "start" && c.includes("--no-attach"),
|
|
495
|
+
);
|
|
496
|
+
expect(gatewayStartCalled).toBe(true);
|
|
497
|
+
|
|
498
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
499
|
+
gatewayStarted: boolean;
|
|
500
|
+
gatewayAlreadyRunning: boolean;
|
|
501
|
+
};
|
|
502
|
+
expect(parsed.gatewayStarted).toBe(true);
|
|
503
|
+
expect(parsed.gatewayAlreadyRunning).toBe(false);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("skips gateway start when already running", async () => {
|
|
507
|
+
const commands: string[][] = [];
|
|
508
|
+
const deps: UpDeps = {
|
|
509
|
+
_runCommand: async (cmd) => {
|
|
510
|
+
commands.push(cmd);
|
|
511
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
512
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
513
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status") {
|
|
514
|
+
return { stdout: JSON.stringify({ running: true }), stderr: "", exitCode: 0 };
|
|
515
|
+
}
|
|
516
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
517
|
+
},
|
|
518
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
519
|
+
_readPid: async () => null,
|
|
520
|
+
_isProcessRunning: () => false,
|
|
521
|
+
_openBrowser: () => {},
|
|
522
|
+
_projectRoot: "/tmp/test-project",
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
await upCommand(["--json"], deps);
|
|
526
|
+
|
|
527
|
+
const gatewayStartCalled = commands.some(
|
|
528
|
+
(c) => c[0] === "legio" && c[1] === "gateway" && c[2] === "start",
|
|
529
|
+
);
|
|
530
|
+
expect(gatewayStartCalled).toBe(false);
|
|
531
|
+
|
|
532
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
533
|
+
gatewayStarted: boolean;
|
|
534
|
+
gatewayAlreadyRunning: boolean;
|
|
535
|
+
};
|
|
536
|
+
expect(parsed.gatewayStarted).toBe(false);
|
|
537
|
+
expect(parsed.gatewayAlreadyRunning).toBe(true);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("continues when gateway start fails (non-fatal)", async () => {
|
|
541
|
+
const deps: UpDeps = {
|
|
542
|
+
_runCommand: async (cmd) => {
|
|
543
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
544
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
545
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status")
|
|
546
|
+
return GATEWAY_STATUS_NOT_RUNNING;
|
|
547
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start") {
|
|
548
|
+
return { stdout: "", stderr: "gateway failed", exitCode: 1 };
|
|
549
|
+
}
|
|
550
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
551
|
+
},
|
|
552
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
553
|
+
_readPid: async () => null,
|
|
554
|
+
_isProcessRunning: () => false,
|
|
555
|
+
_openBrowser: () => {},
|
|
556
|
+
_projectRoot: "/tmp/test-project",
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
await expect(upCommand(["--json"], deps)).resolves.toBeUndefined();
|
|
560
|
+
|
|
561
|
+
const parsed = JSON.parse(capturedStdout.trim()) as {
|
|
562
|
+
gatewayStarted: boolean;
|
|
563
|
+
gatewayAlreadyRunning: boolean;
|
|
564
|
+
};
|
|
565
|
+
expect(parsed.gatewayStarted).toBe(false);
|
|
566
|
+
expect(parsed.gatewayAlreadyRunning).toBe(false);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("continues when gateway status check fails", async () => {
|
|
570
|
+
const commands: string[][] = [];
|
|
571
|
+
const deps: UpDeps = {
|
|
572
|
+
_runCommand: async (cmd) => {
|
|
573
|
+
commands.push(cmd);
|
|
574
|
+
if (cmd[0] === "git") return GIT_OK;
|
|
575
|
+
if (cmd[0] === "legio" && cmd[1] === "server") return SERVER_START_OK;
|
|
576
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "status") {
|
|
577
|
+
return { stdout: "", stderr: "status failed", exitCode: 1 };
|
|
578
|
+
}
|
|
579
|
+
if (cmd[0] === "legio" && cmd[1] === "gateway" && cmd[2] === "start")
|
|
580
|
+
return GATEWAY_START_OK;
|
|
581
|
+
return { stdout: "", stderr: "unexpected", exitCode: 1 };
|
|
582
|
+
},
|
|
583
|
+
_fileExists: async (p) => p.endsWith("config.yaml"),
|
|
584
|
+
_readPid: async () => null,
|
|
585
|
+
_isProcessRunning: () => false,
|
|
586
|
+
_openBrowser: () => {},
|
|
587
|
+
_projectRoot: "/tmp/test-project",
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
await expect(upCommand([], deps)).resolves.toBeUndefined();
|
|
591
|
+
|
|
592
|
+
const gatewayStartCalled = commands.some(
|
|
593
|
+
(c) => c[0] === "legio" && c[1] === "gateway" && c[2] === "start",
|
|
594
|
+
);
|
|
595
|
+
expect(gatewayStartCalled).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
});
|