@saadnvd1/agent-os 0.3.810 → 0.3.815
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/README.md +3 -2
- package/app/api/reach/probe/route.test.ts +22 -0
- package/app/api/reach/probe/route.ts +12 -0
- package/app/api/reach/route.ts +30 -0
- package/app/api/sessions/list-size.test.ts +77 -0
- package/app/api/sessions/route.ts +21 -10
- package/app/api/tasks/list-size.test.ts +50 -0
- package/app/api/tasks/route.ts +20 -3
- package/components/Chat/ViewSwitch.tsx +2 -1
- package/components/Devices/NetworkSection.tsx +2 -0
- package/components/Pane/index.tsx +4 -2
- package/components/Reach/ReachRow.tsx +137 -0
- package/components/Settings/SetupSection.tsx +2 -0
- package/components/Welcome/ConnectBlock.tsx +2 -0
- package/content/tips.json +10 -0
- package/content/whats-new.json +32 -0
- package/data/devices/queries.ts +6 -1
- package/data/reach/index.ts +33 -0
- package/lib/connect/serve.ts +22 -3
- package/lib/connect/supervise.ts +33 -9
- package/lib/db/queries.ts +31 -4
- package/lib/db/types.ts +3 -1
- package/lib/guides.ts +6 -0
- package/lib/hosts/remote-menu.ts +4 -2
- package/lib/reach/check-cli.ts +117 -0
- package/lib/reach/checkReach.ts +62 -0
- package/lib/reach/classify.ts +283 -0
- package/lib/reach/cli.ts +38 -0
- package/lib/reach/gatherFacts.ts +167 -0
- package/lib/reach/index.ts +4 -0
- package/lib/reach/probeConnect.ts +122 -0
- package/lib/reach/types.ts +96 -0
- package/lib/security/auth.ts +2 -0
- package/lib/security/demo.ts +2 -0
- package/lib/sessions/has-prompt.ts +7 -0
- package/lib/sessions/omit-null.ts +8 -0
- package/lib/tasks/contract/views.ts +12 -0
- package/lib/tasks/index.ts +9 -5
- package/lib/tasks/session.ts +15 -1
- package/lib/tips/view.ts +2 -4
- package/package.json +1 -1
- package/scripts/agent-os +3 -0
- package/scripts/install.sh +12 -1
- package/scripts/lib/commands.sh +59 -65
- package/scripts/lib/service.sh +230 -0
- package/scripts/lib/setup.sh +303 -0
- package/scripts/lib/setup.test.ts +389 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
import { tempDir } from "@/lib/test-temp";
|
|
14
|
+
|
|
15
|
+
// `agent-os setup` and the service commands against a fake $HOME. launchctl,
|
|
16
|
+
// systemctl, curl and the TypeScript entry points are fakes that record what
|
|
17
|
+
// they were asked; the server is "up" once a file exists.
|
|
18
|
+
const LIB = join(import.meta.dirname);
|
|
19
|
+
const TOOLS = [
|
|
20
|
+
"tr",
|
|
21
|
+
"cat",
|
|
22
|
+
"sed",
|
|
23
|
+
"head",
|
|
24
|
+
"grep",
|
|
25
|
+
"mkdir",
|
|
26
|
+
"id",
|
|
27
|
+
"mktemp",
|
|
28
|
+
"cp",
|
|
29
|
+
"rm",
|
|
30
|
+
"dirname",
|
|
31
|
+
"realpath",
|
|
32
|
+
"ps",
|
|
33
|
+
"stat",
|
|
34
|
+
"mv",
|
|
35
|
+
"touch",
|
|
36
|
+
"nohup",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
let dir: string;
|
|
40
|
+
|
|
41
|
+
function fake(name: string, body: string) {
|
|
42
|
+
const p = join(dir, "bin", name);
|
|
43
|
+
writeFileSync(p, `#!/bin/sh\n${body}\n`);
|
|
44
|
+
chmodSync(p, 0o755);
|
|
45
|
+
}
|
|
46
|
+
const calls = () =>
|
|
47
|
+
existsSync(join(dir, "calls.log"))
|
|
48
|
+
? readFileSync(join(dir, "calls.log"), "utf8")
|
|
49
|
+
: "";
|
|
50
|
+
const plist = () => join(dir, "Library/LaunchAgents/com.agent-os.plist");
|
|
51
|
+
const unit = () => join(dir, ".config/systemd/user/agent-os.service");
|
|
52
|
+
const script = () => join(dir, "agent-os");
|
|
53
|
+
|
|
54
|
+
function run(command: string, os = "macos") {
|
|
55
|
+
return spawnSync(
|
|
56
|
+
"/bin/bash",
|
|
57
|
+
[
|
|
58
|
+
"-euo",
|
|
59
|
+
"pipefail",
|
|
60
|
+
"-c",
|
|
61
|
+
[
|
|
62
|
+
`source "${LIB}/common.sh"`,
|
|
63
|
+
`source "${LIB}/prerequisites.sh"`,
|
|
64
|
+
`source "${LIB}/commands.sh"`,
|
|
65
|
+
`source "${LIB}/setup.sh"`,
|
|
66
|
+
`BOLD=""; OS=${os} WSL=0 PORT=3011 AGENT_OS_HOME="${dir}" REPO_DIR="${dir}/repo"`,
|
|
67
|
+
`PID_FILE="${dir}/agent-os.pid" LOG_FILE="${dir}/agent-os.log" SCRIPT_DIR="${LIB}"`,
|
|
68
|
+
`has_systemd() { [[ "$OS" != macos ]]; }`,
|
|
69
|
+
`enable_linger() { echo "linger" >> "${dir}/calls.log"; }`,
|
|
70
|
+
`aos_tsx() { echo "tsx $*" >> "${dir}/calls.log"; return "$(cat "${dir}/check-exit" 2>/dev/null || echo 0)"; }`,
|
|
71
|
+
command,
|
|
72
|
+
].join("; "),
|
|
73
|
+
script(),
|
|
74
|
+
],
|
|
75
|
+
{
|
|
76
|
+
cwd: dir,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
env: {
|
|
79
|
+
PATH: join(dir, "bin"),
|
|
80
|
+
HOME: dir,
|
|
81
|
+
USER: "tester",
|
|
82
|
+
NODE_ENV: "test",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
dir = tempDir("aos-setup-");
|
|
90
|
+
mkdirSync(join(dir, "bin"));
|
|
91
|
+
mkdirSync(join(dir, "repo/.next"), { recursive: true });
|
|
92
|
+
writeFileSync(join(dir, "repo/.next/BUILD_ID"), "x");
|
|
93
|
+
writeFileSync(script(), "");
|
|
94
|
+
for (const tool of [...TOOLS, "node"]) {
|
|
95
|
+
const real =
|
|
96
|
+
tool === "node"
|
|
97
|
+
? process.execPath
|
|
98
|
+
: ["/usr/bin", "/bin"].map((d) => join(d, tool)).find(existsSync);
|
|
99
|
+
if (real) symlinkSync(real, join(dir, "bin", tool));
|
|
100
|
+
}
|
|
101
|
+
fake("sleep", "true");
|
|
102
|
+
// launchctl: print succeeds once bootstrapped; bootstrap starts the server.
|
|
103
|
+
fake(
|
|
104
|
+
"launchctl",
|
|
105
|
+
`echo "launchctl $*" >> "${dir}/calls.log"
|
|
106
|
+
case "$1" in
|
|
107
|
+
print) [ -f "${dir}/loaded" ];;
|
|
108
|
+
bootstrap) [ -f "${dir}/no-gui" ] && exit 5; touch "${dir}/loaded" "${dir}/up";;
|
|
109
|
+
bootout) rm -f "${dir}/loaded" "${dir}/up";;
|
|
110
|
+
kickstart) touch "${dir}/up";;
|
|
111
|
+
esac`
|
|
112
|
+
);
|
|
113
|
+
fake(
|
|
114
|
+
"systemctl",
|
|
115
|
+
`echo "systemctl $*" >> "${dir}/calls.log"
|
|
116
|
+
case "$*" in *is-enabled*|*is-active*) exit 1;; esac`
|
|
117
|
+
);
|
|
118
|
+
fake(
|
|
119
|
+
"curl",
|
|
120
|
+
`for a in "$@"; do last="$a"; done
|
|
121
|
+
case "$*" in
|
|
122
|
+
*"-X PUT"*) echo "curl PUT $last" >> "${dir}/calls.log"; exit 0;;
|
|
123
|
+
esac
|
|
124
|
+
[ -f "${dir}/up" ] || exit 7
|
|
125
|
+
case "$last" in
|
|
126
|
+
*/api/health) echo '{"ok":true,"startedAt":"2026-10-31T12:00:00Z"}';;
|
|
127
|
+
*/api/devices/network) cat "${dir}/net.json" 2>/dev/null || echo '{}';;
|
|
128
|
+
esac`
|
|
129
|
+
);
|
|
130
|
+
// Stays up a moment, as a server would, for cmd_start's liveness check.
|
|
131
|
+
fake(
|
|
132
|
+
"npm",
|
|
133
|
+
`echo "npm $*" >> "${dir}/calls.log"; touch "${dir}/up"; exec /bin/sleep 1`
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
137
|
+
|
|
138
|
+
const PLIST = (
|
|
139
|
+
s: string,
|
|
140
|
+
log: string
|
|
141
|
+
) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
142
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
143
|
+
<plist version="1.0">
|
|
144
|
+
<dict>
|
|
145
|
+
<key>Label</key>
|
|
146
|
+
<string>com.agent-os</string>
|
|
147
|
+
<key>ProgramArguments</key>
|
|
148
|
+
<array>
|
|
149
|
+
<string>${s}</string>
|
|
150
|
+
<string>start-foreground</string>
|
|
151
|
+
</array>
|
|
152
|
+
<key>RunAtLoad</key>
|
|
153
|
+
<true/>
|
|
154
|
+
<key>KeepAlive</key>
|
|
155
|
+
<dict>
|
|
156
|
+
<key>SuccessfulExit</key>
|
|
157
|
+
<false/>
|
|
158
|
+
</dict>
|
|
159
|
+
<key>ThrottleInterval</key>
|
|
160
|
+
<integer>10</integer>
|
|
161
|
+
<key>AbandonProcessGroup</key>
|
|
162
|
+
<true/>
|
|
163
|
+
<key>StandardOutPath</key>
|
|
164
|
+
<string>${log}</string>
|
|
165
|
+
<key>StandardErrorPath</key>
|
|
166
|
+
<string>${log}</string>
|
|
167
|
+
<key>EnvironmentVariables</key>
|
|
168
|
+
<dict>
|
|
169
|
+
<key>PATH</key>
|
|
170
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
171
|
+
</dict>
|
|
172
|
+
</dict>
|
|
173
|
+
</plist>
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
// What `agent-os enable` wrote before this change, byte for byte.
|
|
177
|
+
const UNIT = (s: string) => `[Unit]
|
|
178
|
+
Description=AgentOS - AI Coding Session Manager
|
|
179
|
+
After=network.target
|
|
180
|
+
|
|
181
|
+
[Service]
|
|
182
|
+
Type=simple
|
|
183
|
+
ExecStart=${s} start-foreground
|
|
184
|
+
Restart=on-failure
|
|
185
|
+
RestartSec=10
|
|
186
|
+
# Only the server is stopped on a restart: terminals (tmux), chat workers and
|
|
187
|
+
# an update in progress outlive it, as they do under launchd.
|
|
188
|
+
KillMode=process
|
|
189
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
190
|
+
|
|
191
|
+
[Install]
|
|
192
|
+
WantedBy=default.target
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const setup = (args = "--yes --via lan --no-pair") => run(`cmd_setup ${args}`);
|
|
196
|
+
const realScript = () => run(`realpath "$0"`).stdout.trim();
|
|
197
|
+
|
|
198
|
+
describe("agent-os setup", () => {
|
|
199
|
+
it("installs the service, turns on Wi-Fi access and runs the check", () => {
|
|
200
|
+
const r = setup();
|
|
201
|
+
expect(r.status, r.stderr).toBe(0);
|
|
202
|
+
expect(readFileSync(plist(), "utf8")).toBe(
|
|
203
|
+
PLIST(realScript(), join(dir, "agent-os.log"))
|
|
204
|
+
);
|
|
205
|
+
const log = calls();
|
|
206
|
+
expect(log).toMatch(
|
|
207
|
+
/launchctl bootout gui\/\d+\/com\.agent-os\nlaunchctl bootstrap gui\/\d+ /
|
|
208
|
+
);
|
|
209
|
+
expect(log).not.toMatch(/launchctl (load|unload)/);
|
|
210
|
+
expect(log).toContain("curl PUT http://127.0.0.1:3011/api/devices/network");
|
|
211
|
+
expect(log).toContain("tsx lib/reach/cli.ts 3011 --via lan");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("a second run changes nothing", () => {
|
|
215
|
+
setup();
|
|
216
|
+
writeFileSync(join(dir, "net.json"), '{"lan":{"on":true}}');
|
|
217
|
+
rmSync(join(dir, "calls.log"));
|
|
218
|
+
const r = setup();
|
|
219
|
+
expect(r.status, r.stderr).toBe(0);
|
|
220
|
+
expect(r.stdout).toContain(
|
|
221
|
+
"Starts at login and restarts after a crash (already done)"
|
|
222
|
+
);
|
|
223
|
+
expect(r.stdout).toContain(
|
|
224
|
+
"Devices on this Wi-Fi can connect (already done)"
|
|
225
|
+
);
|
|
226
|
+
expect(calls()).not.toMatch(/bootstrap|bootout|PUT/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("never overwrites a service file you changed under --yes", () => {
|
|
230
|
+
mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
|
|
231
|
+
writeFileSync(plist(), "<plist>mine</plist>\n");
|
|
232
|
+
const r = setup();
|
|
233
|
+
expect(r.stdout).toContain("Kept yours");
|
|
234
|
+
expect(readFileSync(plist(), "utf8")).toBe("<plist>mine</plist>\n");
|
|
235
|
+
expect(calls()).not.toContain("bootstrap");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("leaves a server started some other way alone", () => {
|
|
239
|
+
writeFileSync(join(dir, "up"), "");
|
|
240
|
+
const r = setup();
|
|
241
|
+
expect(r.status, r.stderr).toBe(0);
|
|
242
|
+
expect(r.stdout).toContain("started some other way");
|
|
243
|
+
expect(existsSync(plist())).toBe(false);
|
|
244
|
+
expect(calls()).not.toContain("launchctl bootstrap");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("stops at the check's failure, and says to run it again", () => {
|
|
248
|
+
writeFileSync(join(dir, "check-exit"), "2");
|
|
249
|
+
const r = setup();
|
|
250
|
+
expect(r.status).toBe(1);
|
|
251
|
+
expect(r.stdout).toContain("Run 'agent-os setup' again");
|
|
252
|
+
expect(r.stdout).not.toContain("5. Your phone");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("carries on without launchd when there's no desktop session (ssh)", () => {
|
|
256
|
+
writeFileSync(join(dir, "no-gui"), "");
|
|
257
|
+
const r = setup();
|
|
258
|
+
expect(r.status, r.stdout + r.stderr).toBe(0);
|
|
259
|
+
expect(r.stdout).toContain("no-gui-session");
|
|
260
|
+
expect(calls()).toContain("npm start");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("refuses --via with nothing after it", () => {
|
|
264
|
+
const r = run("cmd_setup --via");
|
|
265
|
+
expect(r.status).toBe(2);
|
|
266
|
+
expect(r.stdout + r.stderr).toContain(
|
|
267
|
+
"--via takes connect, tailscale or lan"
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("refuses an unknown route", () => {
|
|
272
|
+
const r = setup("--via carrier-pigeon");
|
|
273
|
+
expect(r.status).toBe(2);
|
|
274
|
+
expect(r.stdout + r.stderr).toContain(
|
|
275
|
+
"--via takes connect, tailscale or lan"
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("the service file", () => {
|
|
281
|
+
it("systemd: the same unit as before, byte for byte", () => {
|
|
282
|
+
expect(run(`render_systemd_unit "$(realpath "$0")"`, "debian").stdout).toBe(
|
|
283
|
+
UNIT(realScript())
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("reconcile: installed, then unchanged", () => {
|
|
288
|
+
expect(run("service_reconcile").stdout.trim()).toBe("installed");
|
|
289
|
+
expect(run("service_reconcile").stdout.trim()).toBe("unchanged");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("reconcile: the old plist is ours and gets updated", () => {
|
|
293
|
+
mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
|
|
294
|
+
writeFileSync(
|
|
295
|
+
plist(),
|
|
296
|
+
run(`render_launchd_plist_v1 "$(realpath "$0")"`).stdout
|
|
297
|
+
);
|
|
298
|
+
expect(run("service_file_state").stdout.trim()).toBe("older");
|
|
299
|
+
expect(run("service_reconcile keep").stdout.trim()).toBe("updated");
|
|
300
|
+
expect(readFileSync(plist(), "utf8")).toContain(
|
|
301
|
+
"<string>start-foreground</string>"
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("reconcile: an edited file is kept under keep", () => {
|
|
306
|
+
mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
|
|
307
|
+
writeFileSync(plist(), "edited\n");
|
|
308
|
+
expect(run("service_reconcile keep").stdout.trim()).toBe("kept-yours");
|
|
309
|
+
expect(readFileSync(plist(), "utf8")).toBe("edited\n");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("reconcile: an edited file that is replaced is kept as .bak", () => {
|
|
313
|
+
mkdirSync(join(dir, "Library/LaunchAgents"), { recursive: true });
|
|
314
|
+
writeFileSync(plist(), "edited\n");
|
|
315
|
+
const r = run("service_reconcile");
|
|
316
|
+
expect(r.stdout.trim()).toBe("updated");
|
|
317
|
+
expect(readFileSync(`${plist()}.bak`, "utf8")).toBe("edited\n");
|
|
318
|
+
expect(readFileSync(plist(), "utf8")).toContain("start-foreground");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("enable on an edited systemd unit keeps a copy of it", () => {
|
|
322
|
+
mkdirSync(join(dir, ".config/systemd/user"), { recursive: true });
|
|
323
|
+
writeFileSync(unit(), "[Service]\nExecStart=/mine\n");
|
|
324
|
+
const r = run("cmd_enable", "debian");
|
|
325
|
+
expect(r.status, r.stderr).toBe(0);
|
|
326
|
+
expect(readFileSync(`${unit()}.bak`, "utf8")).toBe(
|
|
327
|
+
"[Service]\nExecStart=/mine\n"
|
|
328
|
+
);
|
|
329
|
+
expect(readFileSync(unit(), "utf8")).toBe(UNIT(realScript()));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("paths with odd characters stay one value in both files", () => {
|
|
333
|
+
const odd = "/Users/a b/R&D <x>/100%/agent-os";
|
|
334
|
+
const xml = run(`render_launchd_plist "${odd}"`).stdout;
|
|
335
|
+
expect(xml).toContain(
|
|
336
|
+
"<string>/Users/a b/R&D <x>/100%/agent-os</string>"
|
|
337
|
+
);
|
|
338
|
+
const systemd = run(`render_systemd_unit '${odd}'`, "debian").stdout;
|
|
339
|
+
expect(systemd).toContain(
|
|
340
|
+
'ExecStart="/Users/a b/R&D <x>/100%%/agent-os" start-foreground'
|
|
341
|
+
);
|
|
342
|
+
expect(run(`render_systemd_unit '/a/"q"\\b c'`, "debian").stdout).toContain(
|
|
343
|
+
'ExecStart="/a/\\"q\\"\\\\b c" start-foreground'
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("systemd: enable writes the unit and enables it", () => {
|
|
348
|
+
const r = run("cmd_enable", "debian");
|
|
349
|
+
expect(r.status, r.stderr).toBe(0);
|
|
350
|
+
expect(readFileSync(unit(), "utf8")).toBe(UNIT(realScript()));
|
|
351
|
+
expect(calls()).toContain("systemctl --user enable agent-os");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("launchd commands", () => {
|
|
356
|
+
it("stop boots it out, restart kickstarts it, disable boots out and removes it", () => {
|
|
357
|
+
run("cmd_enable");
|
|
358
|
+
rmSync(join(dir, "calls.log"));
|
|
359
|
+
run("cmd_restart");
|
|
360
|
+
expect(calls()).toMatch(/launchctl kickstart -k gui\/\d+\/com\.agent-os/);
|
|
361
|
+
run("cmd_stop");
|
|
362
|
+
expect(calls()).toMatch(/launchctl bootout gui\/\d+\/com\.agent-os/);
|
|
363
|
+
run("cmd_start");
|
|
364
|
+
expect(calls()).toMatch(
|
|
365
|
+
/launchctl bootstrap gui\/\d+ .*com\.agent-os\.plist/
|
|
366
|
+
);
|
|
367
|
+
run("cmd_disable");
|
|
368
|
+
expect(existsSync(plist())).toBe(false);
|
|
369
|
+
expect(calls()).not.toMatch(/launchctl (load|unload)/);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("start with the agent loaded but the server gone never kills one that's running", () => {
|
|
373
|
+
run("cmd_enable");
|
|
374
|
+
rmSync(join(dir, "up"));
|
|
375
|
+
rmSync(join(dir, "calls.log"));
|
|
376
|
+
const r = run("cmd_start");
|
|
377
|
+
expect(r.status, r.stderr).toBe(0);
|
|
378
|
+
expect(calls()).toMatch(/launchctl kickstart gui\/\d+\/com\.agent-os/);
|
|
379
|
+
expect(calls()).not.toContain("kickstart -k");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("enable twice: the second is a no-op", () => {
|
|
383
|
+
run("cmd_enable");
|
|
384
|
+
rmSync(join(dir, "calls.log"));
|
|
385
|
+
const r = run("cmd_enable");
|
|
386
|
+
expect(r.stdout).toContain("already enabled");
|
|
387
|
+
expect(calls()).not.toMatch(/bootstrap|bootout/);
|
|
388
|
+
});
|
|
389
|
+
});
|