agentroam 0.2.0-preview.26 → 0.2.0-preview.27
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 +13 -12
- package/bin/desktop-download.mjs +4 -1
- package/bin/node-preflight.mjs +5 -4
- package/dist/device-pairing.js +46 -5
- package/dist/platform-packages.js +9 -2
- package/dist/tunnel/public-readiness.js +2 -1
- package/dist/update/update-command.js +6 -12
- package/dist/update/update-install.js +34 -0
- package/dist/update/update-worker.js +12 -3
- package/install/README.md +5 -5
- package/install/install-agentroam.ps1 +1 -1
- package/install/install-agentroam.sh +2 -2
- package/install/install-manifest.json +15 -0
- package/package.json +8 -8
- package/dist/args.test.js +0 -84
- package/dist/cloudflared/bundled-asset.test.js +0 -91
- package/dist/cloudflared/installer.test.js +0 -95
- package/dist/codex-runtime-manager.test.js +0 -275
- package/dist/desktop-download.test.js +0 -83
- package/dist/device-pairing.test.js +0 -33
- package/dist/install-script-contract.test.js +0 -176
- package/dist/instance-takeover.test.js +0 -121
- package/dist/native-runtime.test.js +0 -40
- package/dist/network.test.js +0 -20
- package/dist/node-preflight.test.js +0 -136
- package/dist/node-runtime-manager.test.js +0 -184
- package/dist/opencode-runtime-manager.test.js +0 -111
- package/dist/pairing.js +0 -2
- package/dist/platform-packages.test.js +0 -86
- package/dist/platform.test.js +0 -18
- package/dist/port.test.js +0 -3
- package/dist/power/sleep-inhibitor.test.js +0 -104
- package/dist/process-supervisor.test.js +0 -42
- package/dist/runtime-manager.test.js +0 -38
- package/dist/service/launcher-cleanup.test.js +0 -81
- package/dist/service/macos-launch-agent.test.js +0 -327
- package/dist/service/runtime-state.test.js +0 -46
- package/dist/service/service-command.test.js +0 -189
- package/dist/service/service-files.test.js +0 -48
- package/dist/service/windows-service-host.test.js +0 -52
- package/dist/service/windows-task-service.test.js +0 -230
- package/dist/tunnel/cloudflare-provider.test.js +0 -45
- package/dist/tunnel/pinggy-provider.test.js +0 -40
- package/dist/tunnel/proxy-settings.test.js +0 -111
- package/dist/tunnel/public-readiness.test.js +0 -104
- package/dist/tunnel/relay-orchestrator.test.js +0 -84
- package/dist/unlock-service.test.js +0 -37
- package/dist/update/update-command.test.js +0 -70
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { MANAGED_NODE_VERSION, NODE_RUNTIME_ASSETS } from "./node-runtime-manager.js";
|
|
6
|
-
import { MINIMUM_NODE_VERSION, isSupportedNodeVersion } from "../bin/runtime-policy.mjs";
|
|
7
|
-
const installRoot = resolve(import.meta.dirname, "../install");
|
|
8
|
-
describe("standalone installer contracts", () => {
|
|
9
|
-
it.skipIf(process.platform === "win32")("continues after failed diagnostics but stops when service registration fails", async () => {
|
|
10
|
-
const root = await mkdtemp(resolve(process.env.TMPDIR || "/tmp", "agentroam-install-diagnostics-"));
|
|
11
|
-
try {
|
|
12
|
-
const script = await readFile(resolve(installRoot, "install-agentroam.sh"), "utf8");
|
|
13
|
-
// Execute the actual post-install flow with a fake CLI, without touching launchd.
|
|
14
|
-
const start = script.indexOf('if ! "$NODE_BIN" "$entry" doctor');
|
|
15
|
-
expect(start).toBeGreaterThan(0);
|
|
16
|
-
const node = resolve(root, "node");
|
|
17
|
-
const calls = resolve(root, "calls");
|
|
18
|
-
await writeFile(node, `#!/bin/sh\nprintf '%s\\n' "$2" >> "$TEST_CALLS"\ncase "$2" in\n doctor) exit 1 ;;\n service) exit "$TEST_SERVICE_EXIT" ;;\nesac\n`);
|
|
19
|
-
await chmod(node, 0o755);
|
|
20
|
-
for (const serviceExit of [0, 1]) {
|
|
21
|
-
await writeFile(calls, "");
|
|
22
|
-
const result = spawnSync("/bin/sh", ["-c", `set -eu\n${script.slice(start)}`], {
|
|
23
|
-
encoding: "utf8",
|
|
24
|
-
env: { ...process.env, NODE_BIN: node, entry: "fake-cli", DATA_DIR: root, SERVICE_ROOT: root,
|
|
25
|
-
WRAPPER_PATH: "agentroam", WRAPPER_DIR: root, AGENTROAM_VERSION: "test",
|
|
26
|
-
AGENTROAM_INSTALL_SKIP_SERVICE: "0", AGENTROAM_INSTALL_DESKTOP: "no",
|
|
27
|
-
TEST_CALLS: calls, TEST_SERVICE_EXIT: String(serviceExit) },
|
|
28
|
-
});
|
|
29
|
-
expect(await readFile(calls, "utf8")).toBe("doctor\nservice\n");
|
|
30
|
-
expect(result.stderr).toContain("some component checks failed");
|
|
31
|
-
expect(result.status).toBe(serviceExit);
|
|
32
|
-
if (serviceExit === 0)
|
|
33
|
-
expect(result.stdout).toContain("installed:");
|
|
34
|
-
else
|
|
35
|
-
expect(result.stdout).not.toContain("installed:");
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
await rm(root, { recursive: true, force: true });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("allows installation from home and keeps a compatible active Node", async () => {
|
|
43
|
-
const root = await mkdtemp(resolve(process.env.TMPDIR || "/tmp", "agentroam-active-node-"));
|
|
44
|
-
try {
|
|
45
|
-
const homeDir = resolve(root, "home");
|
|
46
|
-
const workspace = resolve(root, "workspace");
|
|
47
|
-
const activeBin = resolve(root, "active bin");
|
|
48
|
-
const nvmRoot = resolve(root, "nvm");
|
|
49
|
-
await Promise.all([mkdir(homeDir), mkdir(workspace), mkdir(activeBin), mkdir(nvmRoot)]);
|
|
50
|
-
const expected = await fakeNode(resolve(activeBin, "node"), "24.13.0");
|
|
51
|
-
await fakeNode(resolve(nvmRoot, "versions/node/v26.0.0/bin/node"), "26.0.0");
|
|
52
|
-
const output = execFileSync("/bin/sh", [resolve(installRoot, "install-agentroam.sh")], {
|
|
53
|
-
cwd: homeDir, encoding: "utf8",
|
|
54
|
-
env: { ...process.env, HOME: homeDir, NVM_DIR: nvmRoot, PATH: `${activeBin}:/usr/bin:/bin`,
|
|
55
|
-
AGENTROAM_BOOTSTRAP_TEST: "1", AGENTROAM_BOOTSTRAP_NODE_DISCOVERY_ONLY: "1" },
|
|
56
|
-
});
|
|
57
|
-
expect(output.trim()).toBe(expected);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
await rm(root, { recursive: true, force: true });
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
it("keeps the macOS installer aligned with the runtime manifest", async () => {
|
|
64
|
-
const scriptPath = resolve(installRoot, "install-agentroam.sh");
|
|
65
|
-
const script = await readFile(scriptPath, "utf8");
|
|
66
|
-
expect(script).toContain(`NODE_VERSION="${MANAGED_NODE_VERSION}"`);
|
|
67
|
-
expect(script).toContain(`MINIMUM_NODE_VERSION="${MINIMUM_NODE_VERSION}"`);
|
|
68
|
-
expect(script).toContain('AGENTROAM_VERSION_REQUEST="${AGENTROAM_VERSION:-preview}"');
|
|
69
|
-
expect(script).toContain(NODE_RUNTIME_ASSETS["darwin-arm64"].archive);
|
|
70
|
-
expect(script).toContain(NODE_RUNTIME_ASSETS["darwin-arm64"].sha256);
|
|
71
|
-
expect(script).toContain("https://registry.npmjs.org");
|
|
72
|
-
expect(script).toContain("$HOME/.agentroam");
|
|
73
|
-
expect(script).toContain("$HOME/.local/bin");
|
|
74
|
-
expect(script).toContain('AGENTROAM_ROOT');
|
|
75
|
-
expect(script).toContain('AGENTROAM_INSTALL_SKIP_SERVICE');
|
|
76
|
-
expect(script).toContain('service install --root "$SERVICE_ROOT" --data-dir "$DATA_DIR"');
|
|
77
|
-
expect(script).toContain('SERVICE_ROOT="$PWD"');
|
|
78
|
-
expect(script).toContain("find_nvm_node");
|
|
79
|
-
expect(script).toContain('${NVM_DIR:-}');
|
|
80
|
-
expect(script).not.toMatch(/\b(?:brew|sudo|fnm|volta)\b/);
|
|
81
|
-
execFileSync("sh", ["-n", scriptPath]);
|
|
82
|
-
});
|
|
83
|
-
it("keeps the Windows installer aligned with the runtime manifest", async () => {
|
|
84
|
-
const script = await readFile(resolve(installRoot, "install-agentroam.ps1"), "utf8");
|
|
85
|
-
expect(script).toContain(`$NodeVersion = "${MANAGED_NODE_VERSION}"`);
|
|
86
|
-
expect(script).toContain(`$MinimumNodeVersion = "${MINIMUM_NODE_VERSION}"`);
|
|
87
|
-
expect(script).toContain('$AgentRoamVersionRequest = if ($env:AGENTROAM_VERSION) { $env:AGENTROAM_VERSION } else { "preview" }');
|
|
88
|
-
expect(script).toContain(NODE_RUNTIME_ASSETS["windows-amd64"].archive);
|
|
89
|
-
expect(script).toContain(NODE_RUNTIME_ASSETS["windows-amd64"].sha256);
|
|
90
|
-
expect(script).toContain("https://registry.npmjs.org");
|
|
91
|
-
expect(script).toContain(".agentroam\\bin");
|
|
92
|
-
expect(script).toContain('[Environment]::SetEnvironmentVariable("Path", $UpdatedPath, "User")');
|
|
93
|
-
expect(script).toContain("$env:AGENTROAM_ROOT");
|
|
94
|
-
expect(script).toContain("$env:AGENTROAM_INSTALL_SKIP_SERVICE");
|
|
95
|
-
expect(script).toContain("service install --root $ServiceRoot --data-dir $DataDir");
|
|
96
|
-
expect(script).toContain("(Get-Location).Path");
|
|
97
|
-
expect(script).toContain("Find-NvmNode");
|
|
98
|
-
expect(script).toContain("$env:NVM_HOME");
|
|
99
|
-
expect(script).not.toMatch(/\b(?:winget|choco|scoop|Start-Process\s+.*RunAs)\b/i);
|
|
100
|
-
});
|
|
101
|
-
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("selects the highest compatible NVM Node without changing the active version", async () => {
|
|
102
|
-
const root = await mkdtemp(resolve(process.env.TMPDIR || "/tmp", "agentroam-nvm-discovery-"));
|
|
103
|
-
try {
|
|
104
|
-
const home = resolve(root, "home");
|
|
105
|
-
const workspace = resolve(root, "workspace");
|
|
106
|
-
const activeBin = resolve(root, "active-bin");
|
|
107
|
-
const nvmRoot = resolve(root, "nvm root with spaces");
|
|
108
|
-
await Promise.all([mkdir(home), mkdir(workspace), mkdir(activeBin), mkdir(nvmRoot)]);
|
|
109
|
-
await fakeNode(resolve(activeBin, "node"), "20.19.0");
|
|
110
|
-
await fakeNode(resolve(nvmRoot, "versions/node/v22.3.0/bin/node"), "22.3.0");
|
|
111
|
-
await fakeNode(resolve(nvmRoot, "versions/node/v22.22.2/bin/node"), "22.22.2");
|
|
112
|
-
const expected = await fakeNode(resolve(nvmRoot, "versions/node/v25.8.0/bin/node"), "25.8.0");
|
|
113
|
-
await fakeNode(resolve(nvmRoot, "versions/node/v22.99.0/bin/node"), "21.99.0");
|
|
114
|
-
const output = execFileSync("/bin/sh", [resolve(installRoot, "install-agentroam.sh")], {
|
|
115
|
-
cwd: workspace,
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
env: {
|
|
118
|
-
...process.env,
|
|
119
|
-
HOME: home,
|
|
120
|
-
NVM_DIR: nvmRoot,
|
|
121
|
-
PATH: `${activeBin}:/usr/bin:/bin`,
|
|
122
|
-
AGENTROAM_BOOTSTRAP_TEST: "1",
|
|
123
|
-
AGENTROAM_BOOTSTRAP_NODE_DISCOVERY_ONLY: "1",
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
expect(output.trim().split("\n").at(-1)).toBe(expected);
|
|
127
|
-
expect(output).toContain("Using Node.js v25.8.0 from NVM");
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
await rm(root, { recursive: true, force: true });
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
it.skipIf(process.platform !== "win32" || !isSupportedNodeVersion(process.versions.node))("selects a compatible nvm-windows Node when the active PATH has no Node", async () => {
|
|
134
|
-
const root = await mkdtemp(resolve(process.env.TEMP || process.env.TMP || "C:\\Windows\\Temp", "agentroam-nvm-windows-"));
|
|
135
|
-
try {
|
|
136
|
-
const home = resolve(root, "home");
|
|
137
|
-
const workspace = resolve(root, "workspace");
|
|
138
|
-
const nvmRoot = resolve(root, "nvm root with spaces");
|
|
139
|
-
const expected = resolve(nvmRoot, `v${process.versions.node}`, "node.exe");
|
|
140
|
-
await Promise.all([
|
|
141
|
-
mkdir(home),
|
|
142
|
-
mkdir(workspace),
|
|
143
|
-
mkdir(resolve(expected, ".."), { recursive: true }),
|
|
144
|
-
]);
|
|
145
|
-
await copyFile(process.execPath, expected);
|
|
146
|
-
const output = execFileSync("powershell.exe", [
|
|
147
|
-
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
148
|
-
"-File", resolve(installRoot, "install-agentroam.ps1"),
|
|
149
|
-
], {
|
|
150
|
-
cwd: workspace,
|
|
151
|
-
encoding: "utf8",
|
|
152
|
-
env: {
|
|
153
|
-
...process.env,
|
|
154
|
-
HOME: home,
|
|
155
|
-
USERPROFILE: home,
|
|
156
|
-
APPDATA: resolve(root, "appdata"),
|
|
157
|
-
NVM_HOME: nvmRoot,
|
|
158
|
-
PATH: `${process.env.SystemRoot || "C:\\Windows"}\\System32`,
|
|
159
|
-
AGENTROAM_BOOTSTRAP_TEST: "1",
|
|
160
|
-
AGENTROAM_BOOTSTRAP_NODE_DISCOVERY_ONLY: "1",
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
expect(output.trim().split(/\r?\n/).at(-1)).toBe(expected);
|
|
164
|
-
expect(output).toContain(`Using Node.js v${process.versions.node} from NVM`);
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
await rm(root, { recursive: true, force: true });
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
async function fakeNode(path, version) {
|
|
172
|
-
await mkdir(resolve(path, ".."), { recursive: true });
|
|
173
|
-
await writeFile(path, `#!/bin/sh\ncase "$1" in\n -p) printf '%s\\n' '${version}' ;;\n --version) printf '%s\\n' 'v${version}' ;;\nesac\n`);
|
|
174
|
-
await chmod(path, 0o755);
|
|
175
|
-
return path;
|
|
176
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { once } from "node:events";
|
|
2
|
-
import { afterEach, expect, it, vi } from "vitest";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm, realpath } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { lockInstanceStartup, stopPreviousInstances } from "./instance-takeover.js";
|
|
7
|
-
const temporary = [];
|
|
8
|
-
afterEach(async () => { await Promise.all(temporary.splice(0).map(path => rm(path, { recursive: true, force: true }))); });
|
|
9
|
-
async function fixture() {
|
|
10
|
-
const root = await realpath(await mkdtemp(join(tmpdir(), "cli-takeover-")));
|
|
11
|
-
temporary.push(root);
|
|
12
|
-
const registry = join(root, "registry");
|
|
13
|
-
const dataDir = join(root, "data-dir");
|
|
14
|
-
await mkdir(registry);
|
|
15
|
-
await mkdir(join(dataDir, "data"), { recursive: true });
|
|
16
|
-
const descriptor = { protocol: 1, instanceId: "test-instance", pid: 200, url: "http://127.0.0.1:12345", dataDir: join(dataDir, "data"), token: "a".repeat(64) };
|
|
17
|
-
const save = () => writeFile(join(registry, "test.json"), JSON.stringify(descriptor));
|
|
18
|
-
await save();
|
|
19
|
-
const rows = [{ pid: 200, ppid: 100, command: "/node /runtime/ws-server.mjs" }, { pid: 100, ppid: 1, command: "/node /cache/node_modules/agentroam/bin/agentroam.mjs" }];
|
|
20
|
-
const stop = vi.fn(async () => { });
|
|
21
|
-
const deps = { registry, request: vi.fn(async () => Response.json(descriptor)), processes: vi.fn(async () => rows), stop, alive: () => false, timeoutMs: 1 };
|
|
22
|
-
return { descriptor, save, deps, stop, dataDir, rows };
|
|
23
|
-
}
|
|
24
|
-
it("authenticates the same data directory and stops the CLI parent before proceeding", async () => {
|
|
25
|
-
const f = await fixture();
|
|
26
|
-
await stopPreviousInstances(f.dataDir, () => { }, f.deps);
|
|
27
|
-
expect(f.stop).toHaveBeenCalledTimes(1);
|
|
28
|
-
expect(f.stop).toHaveBeenCalledWith(100);
|
|
29
|
-
expect(f.deps.processes).toHaveBeenCalledTimes(2);
|
|
30
|
-
});
|
|
31
|
-
it("leaves a different data directory untouched without making an authenticated request", async () => {
|
|
32
|
-
const f = await fixture();
|
|
33
|
-
f.descriptor.dataDir = tmpdir();
|
|
34
|
-
await f.save();
|
|
35
|
-
await stopPreviousInstances(f.dataDir, () => { }, f.deps);
|
|
36
|
-
expect(f.deps.request).not.toHaveBeenCalled();
|
|
37
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
38
|
-
});
|
|
39
|
-
it("does not send the private token to non-loopback addresses", async () => {
|
|
40
|
-
const f = await fixture();
|
|
41
|
-
f.descriptor.url = "https://example.com";
|
|
42
|
-
await f.save();
|
|
43
|
-
await stopPreviousInstances(f.dataDir, () => { }, f.deps);
|
|
44
|
-
expect(f.deps.request).not.toHaveBeenCalled();
|
|
45
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
46
|
-
});
|
|
47
|
-
it("rejects a matching live server with a non-CLI parent", async () => {
|
|
48
|
-
const f = await fixture();
|
|
49
|
-
f.rows[1].command = "/Applications/Other.app/server";
|
|
50
|
-
await expect(stopPreviousInstances(f.dataDir, () => { }, f.deps)).rejects.toThrow("无法确认");
|
|
51
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
52
|
-
});
|
|
53
|
-
it("does not signal a reused PID", async () => {
|
|
54
|
-
const f = await fixture();
|
|
55
|
-
f.deps.processes.mockResolvedValueOnce(f.rows).mockResolvedValueOnce([{ ...f.rows[0], ppid: 300 }, f.rows[1]]);
|
|
56
|
-
await expect(stopPreviousInstances(f.dataDir, () => { }, f.deps)).rejects.toThrow("身份已变化");
|
|
57
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
58
|
-
});
|
|
59
|
-
it("refuses to launch while the previous process still runs", async () => {
|
|
60
|
-
const f = await fixture();
|
|
61
|
-
f.deps.alive = () => true;
|
|
62
|
-
await expect(stopPreviousInstances(f.dataDir, () => { }, f.deps)).rejects.toThrow("尚未退出");
|
|
63
|
-
});
|
|
64
|
-
it("ignores stale dead descriptors but blocks unreachable live instances", async () => {
|
|
65
|
-
const f = await fixture();
|
|
66
|
-
f.deps.request = vi.fn(async () => { throw new Error("offline"); });
|
|
67
|
-
await stopPreviousInstances(f.dataDir, () => { }, f.deps);
|
|
68
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
69
|
-
f.deps.alive = () => true;
|
|
70
|
-
await expect(stopPreviousInstances(f.dataDir, () => { }, f.deps)).rejects.toThrow("无法核验");
|
|
71
|
-
});
|
|
72
|
-
it("serializes startup and releases the lock for the next launch", async () => {
|
|
73
|
-
const f = await fixture();
|
|
74
|
-
const release = await lockInstanceStartup(f.dataDir);
|
|
75
|
-
await expect(lockInstanceStartup(f.dataDir)).rejects.toThrow("正在启动");
|
|
76
|
-
await release();
|
|
77
|
-
await release();
|
|
78
|
-
await (await lockInstanceStartup(f.dataDir))();
|
|
79
|
-
});
|
|
80
|
-
it.skipIf(process.platform === "win32")("stops an authenticated real CLI parent and waits for its server child to exit", async () => {
|
|
81
|
-
const { spawn } = await import("node:child_process");
|
|
82
|
-
const f = await fixture();
|
|
83
|
-
const cli = join(f.dataDir, "node_modules/agentroam/bin/agentroam.mjs");
|
|
84
|
-
const server = join(f.dataDir, "runtime/ws-server.mjs");
|
|
85
|
-
await mkdir(join(f.dataDir, "node_modules/agentroam/bin"), { recursive: true });
|
|
86
|
-
await mkdir(join(f.dataDir, "runtime"));
|
|
87
|
-
await writeFile(server, `import http from 'node:http';\nimport {writeFileSync} from 'node:fs';\nconst d=JSON.parse(process.env.DESCRIPTOR);d.pid=process.pid;\nconst s=http.createServer((req,res)=>{if(req.headers['x-agentroam-desktop-token']!==d.token){res.writeHead(401).end();return;}res.setHeader('content-type','application/json');res.end(JSON.stringify(d));});\ns.listen(0,'127.0.0.1',()=>{d.url='http://127.0.0.1:'+s.address().port;writeFileSync(process.env.RECORD,JSON.stringify(d));console.log('ready');});`);
|
|
88
|
-
await writeFile(cli, `import {spawn} from 'node:child_process';\nimport {writeFileSync} from 'node:fs';\nconst child=spawn(process.execPath,[process.env.SERVER],{stdio:['ignore','inherit','inherit']});\nprocess.on('SIGTERM',()=>{writeFileSync(process.env.MARKER,'graceful');child.kill('SIGTERM');});\nchild.on('exit',()=>process.exit(0));`);
|
|
89
|
-
const marker = join(f.dataDir, "stopped");
|
|
90
|
-
const child = spawn(process.execPath, [cli], { env: { ...process.env, SERVER: server, RECORD: join(f.deps.registry, "test.json"), DESCRIPTOR: JSON.stringify(f.descriptor), MARKER: marker }, stdio: ["ignore", "pipe", "pipe"] });
|
|
91
|
-
const exited = once(child, "exit");
|
|
92
|
-
try {
|
|
93
|
-
await once(child.stdout, "data");
|
|
94
|
-
await stopPreviousInstances(f.dataDir, () => { }, { registry: f.deps.registry, stop: async (pid) => { expect(pid).toBe(child.pid); process.kill(pid, "SIGTERM"); } });
|
|
95
|
-
await exited;
|
|
96
|
-
const { readFile } = await import("node:fs/promises");
|
|
97
|
-
expect(await readFile(marker, "utf8")).toBe("graceful");
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
if (child.exitCode === null) {
|
|
101
|
-
child.kill("SIGTERM");
|
|
102
|
-
await exited;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
it("stops an authenticated orphan only when its executable belongs to a runtime package", async () => {
|
|
107
|
-
const f = await fixture();
|
|
108
|
-
const pkg = join(f.dataDir, "launcher/0.2.0-preview.18/node_modules/agentroam-runtime-darwin-arm64");
|
|
109
|
-
await mkdir(join(pkg, "runtime"), { recursive: true });
|
|
110
|
-
await writeFile(join(pkg, "package.json"), JSON.stringify({ name: "agentroam-runtime-darwin-arm64" }));
|
|
111
|
-
await writeFile(join(pkg, "runtime/ws-server.mjs"), "// fixture");
|
|
112
|
-
f.rows.splice(0, f.rows.length, { pid: 200, ppid: 1, command: `/node ${join(pkg, "runtime/ws-server.mjs")}` });
|
|
113
|
-
await stopPreviousInstances(f.dataDir, () => { }, f.deps);
|
|
114
|
-
expect(f.stop).toHaveBeenCalledWith(200);
|
|
115
|
-
});
|
|
116
|
-
it("does not stop an orphan merely because its script is named ws-server.mjs", async () => {
|
|
117
|
-
const f = await fixture();
|
|
118
|
-
f.rows.splice(0, f.rows.length, { pid: 200, ppid: 1, command: "/node /unrelated/ws-server.mjs" });
|
|
119
|
-
await expect(stopPreviousInstances(f.dataDir, () => { }, f.deps)).rejects.toThrow("无法确认");
|
|
120
|
-
expect(f.stop).not.toHaveBeenCalled();
|
|
121
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { probeSQLiteRuntime, probeTerminalRuntime, repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
6
|
-
describe("native runtime health", () => {
|
|
7
|
-
it("detects lazy SQLite binding failures after require succeeds", () => {
|
|
8
|
-
const requireRuntime = (() => class Database {
|
|
9
|
-
constructor() { throw new Error("native binding unavailable"); }
|
|
10
|
-
});
|
|
11
|
-
expect(() => probeSQLiteRuntime(requireRuntime)).toThrow("native binding unavailable");
|
|
12
|
-
});
|
|
13
|
-
it("closes SQLite when its query fails", () => {
|
|
14
|
-
const close = vi.fn();
|
|
15
|
-
const requireRuntime = (() => class Database {
|
|
16
|
-
exec() { throw new Error("database failed"); }
|
|
17
|
-
close = close;
|
|
18
|
-
});
|
|
19
|
-
expect(() => probeSQLiteRuntime(requireRuntime)).toThrow("database failed");
|
|
20
|
-
expect(close).toHaveBeenCalledOnce();
|
|
21
|
-
});
|
|
22
|
-
it("detects PTY spawn failures after require succeeds", async () => {
|
|
23
|
-
const requireRuntime = (() => ({ spawn() { throw new Error("spawn-helper unavailable"); } }));
|
|
24
|
-
await expect(probeTerminalRuntime(requireRuntime)).rejects.toThrow("spawn-helper unavailable");
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
describe("repairNativeRuntimePermissions", () => {
|
|
28
|
-
it("restores the node-pty helper execute bits stripped by npm pack", async () => {
|
|
29
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-native-"));
|
|
30
|
-
const helper = resolve(root, "node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper");
|
|
31
|
-
await mkdir(resolve(helper, ".."), { recursive: true });
|
|
32
|
-
await writeFile(helper, "helper", { mode: 0o644 });
|
|
33
|
-
await repairNativeRuntimePermissions(root, "darwin", "arm64");
|
|
34
|
-
expect((await stat(helper)).mode & 0o111).toBe(0o111);
|
|
35
|
-
});
|
|
36
|
-
it("fails before gateway startup when the helper is missing", async () => {
|
|
37
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-native-missing-"));
|
|
38
|
-
await expect(repairNativeRuntimePermissions(root, "darwin", "arm64")).rejects.toThrow("spawn-helper missing");
|
|
39
|
-
});
|
|
40
|
-
});
|
package/dist/network.test.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { findLanUrl } from "./network.js";
|
|
3
|
-
describe("findLanUrl", () => {
|
|
4
|
-
it("prefers an RFC1918 address over VPN and public interfaces", () => {
|
|
5
|
-
expect(findLanUrl(52032, {
|
|
6
|
-
utun7: [{ address: "100.121.10.115", netmask: "255.255.255.255", family: "IPv4", mac: "", internal: false, cidr: null }],
|
|
7
|
-
en0: [{ address: "10.22.34.143", netmask: "255.255.252.0", family: "IPv4", mac: "", internal: false, cidr: null }],
|
|
8
|
-
})).toBe("http://10.22.34.143:52032");
|
|
9
|
-
});
|
|
10
|
-
it("falls back to the first external IPv4 address", () => {
|
|
11
|
-
expect(findLanUrl(4100, {
|
|
12
|
-
utun7: [{ address: "100.121.10.115", netmask: "255.255.255.255", family: "IPv4", mac: "", internal: false, cidr: null }],
|
|
13
|
-
})).toBe("http://100.121.10.115:4100");
|
|
14
|
-
});
|
|
15
|
-
it("returns null when only loopback is available", () => {
|
|
16
|
-
expect(findLanUrl(4100, {
|
|
17
|
-
lo0: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "", internal: true, cidr: null }],
|
|
18
|
-
})).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
// @ts-expect-error The preflight intentionally remains plain ESM for Node 18 bootstrap.
|
|
7
|
-
import { findNvmNode, parsePreflightDataDir, runNodePreflight } from "../bin/node-preflight.mjs";
|
|
8
|
-
const temporaryDirectories = [];
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
11
|
-
});
|
|
12
|
-
describe("Node launcher preflight", () => {
|
|
13
|
-
it.each(["22.22.0", "22.23.0", "24.0.0", "25.8.0", "26.0.0", "100.0.0"])("passes through Node %s without installing or switching", async (nodeVersion) => {
|
|
14
|
-
const ensureNode = vi.fn();
|
|
15
|
-
const findSystemNode = vi.fn();
|
|
16
|
-
const spawnChild = vi.fn();
|
|
17
|
-
await expect(runNodePreflight({ nodeVersion, ensureNode, findSystemNode, spawnChild, environment: { AGENTROAM_MANAGED_NODE: "/older/node" } })).resolves.toEqual({ handled: false });
|
|
18
|
-
expect(ensureNode).not.toHaveBeenCalled();
|
|
19
|
-
expect(findSystemNode).not.toHaveBeenCalled();
|
|
20
|
-
expect(spawnChild).not.toHaveBeenCalled();
|
|
21
|
-
});
|
|
22
|
-
it.each(["18.20.0", "20.18.0", "22.1.0", "22.21.9"])("re-executes Node %s through the managed runtime", async (nodeVersion) => {
|
|
23
|
-
const child = fakeChild(0, null);
|
|
24
|
-
const ensureNode = vi.fn(async () => ({ executable: "/managed/node", version: "22.22.0" }));
|
|
25
|
-
const spawnChild = vi.fn((_executable, _args, _options) => child);
|
|
26
|
-
const result = await runNodePreflight({
|
|
27
|
-
argv: ["doctor", "--data-dir", "./custom"],
|
|
28
|
-
nodeVersion,
|
|
29
|
-
platform: "darwin",
|
|
30
|
-
arch: "arm64",
|
|
31
|
-
launcherPath: "/launcher/agentroam.mjs",
|
|
32
|
-
environment: { PATH: "" },
|
|
33
|
-
ensureNode,
|
|
34
|
-
findSystemNode: async () => null,
|
|
35
|
-
spawnChild,
|
|
36
|
-
processHost: fakeProcessHost(),
|
|
37
|
-
});
|
|
38
|
-
expect(result).toEqual({ handled: true, exitCode: 0, signal: null });
|
|
39
|
-
expect(ensureNode).toHaveBeenCalledWith(expect.objectContaining({
|
|
40
|
-
dataDir: resolve("./custom"),
|
|
41
|
-
target: "darwin-arm64",
|
|
42
|
-
}));
|
|
43
|
-
expect(spawnChild).toHaveBeenCalledWith("/managed/node", ["/launcher/agentroam.mjs", "doctor", "--data-dir", "./custom"], expect.objectContaining({
|
|
44
|
-
stdio: "inherit",
|
|
45
|
-
env: expect.objectContaining({ AGENTROAM_MANAGED_NODE: "/managed/node" }),
|
|
46
|
-
}));
|
|
47
|
-
});
|
|
48
|
-
it("reuses another visible compatible system Node", async () => {
|
|
49
|
-
const ensureNode = vi.fn();
|
|
50
|
-
const spawnChild = vi.fn((_executable, _args, _options) => fakeChild(7, null));
|
|
51
|
-
await expect(runNodePreflight({
|
|
52
|
-
argv: ["version"],
|
|
53
|
-
nodeVersion: "20.0.0",
|
|
54
|
-
platform: "win32",
|
|
55
|
-
arch: "x64",
|
|
56
|
-
environment: { PATH: "" },
|
|
57
|
-
findSystemNode: async () => "C:\\Node25\\node.exe",
|
|
58
|
-
ensureNode,
|
|
59
|
-
spawnChild,
|
|
60
|
-
processHost: fakeProcessHost(),
|
|
61
|
-
})).resolves.toMatchObject({ handled: true, exitCode: 7 });
|
|
62
|
-
expect(ensureNode).not.toHaveBeenCalled();
|
|
63
|
-
expect(spawnChild.mock.calls[0][0]).toBe("C:\\Node25\\node.exe");
|
|
64
|
-
});
|
|
65
|
-
it.skipIf(process.platform === "win32")("selects the highest compatible inactive NVM Node", async () => {
|
|
66
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-preflight-nvm-"));
|
|
67
|
-
temporaryDirectories.push(root);
|
|
68
|
-
await fakeNodeExecutable(resolve(root, "versions/node/v22.3.0/bin/node"), "22.3.0");
|
|
69
|
-
await fakeNodeExecutable(resolve(root, "versions/node/v22.22.2/bin/node"), "22.22.2");
|
|
70
|
-
await fakeNodeExecutable(resolve(root, "versions/node/v23.1.0/bin/node"), "23.1.0");
|
|
71
|
-
const expected = await fakeNodeExecutable(resolve(root, "versions/node/v25.8.0/bin/node"), "25.8.0");
|
|
72
|
-
await fakeNodeExecutable(resolve(root, "versions/node/v26.0.0-rc.1/bin/node"), "26.0.0-rc.1");
|
|
73
|
-
await expect(findNvmNode({ NVM_DIR: root, HOME: resolve(root, "home") }, "darwin"))
|
|
74
|
-
.resolves.toBe(expected);
|
|
75
|
-
});
|
|
76
|
-
it("rejects a failed managed re-entry instead of looping", async () => {
|
|
77
|
-
await expect(runNodePreflight({
|
|
78
|
-
nodeVersion: "22.21.9",
|
|
79
|
-
environment: { AGENTROAM_MANAGED_NODE: "22.22.0" },
|
|
80
|
-
})).rejects.toMatchObject({ message: expect.stringContaining("re-entry failed"), exitCode: 2 });
|
|
81
|
-
});
|
|
82
|
-
it("directs old Node versions to the standalone installer", async () => {
|
|
83
|
-
await expect(runNodePreflight({
|
|
84
|
-
nodeVersion: "16.20.0",
|
|
85
|
-
platform: "darwin",
|
|
86
|
-
environment: {},
|
|
87
|
-
})).rejects.toThrow("install-agentroam.sh");
|
|
88
|
-
await expect(runNodePreflight({
|
|
89
|
-
nodeVersion: "12.0.0",
|
|
90
|
-
platform: "win32",
|
|
91
|
-
environment: {},
|
|
92
|
-
})).rejects.toThrow("install-agentroam.ps1");
|
|
93
|
-
});
|
|
94
|
-
it("forwards termination signals and propagates a child signal", async () => {
|
|
95
|
-
const processHost = fakeProcessHost();
|
|
96
|
-
const child = new EventEmitter();
|
|
97
|
-
child.kill = vi.fn();
|
|
98
|
-
const running = runNodePreflight({
|
|
99
|
-
nodeVersion: "20.0.0",
|
|
100
|
-
platform: "darwin",
|
|
101
|
-
arch: "arm64",
|
|
102
|
-
environment: { PATH: "" },
|
|
103
|
-
findSystemNode: async () => "/compatible-node",
|
|
104
|
-
spawnChild: () => child,
|
|
105
|
-
processHost,
|
|
106
|
-
});
|
|
107
|
-
await new Promise((resolveTurn) => setImmediate(resolveTurn));
|
|
108
|
-
processHost.emit("SIGTERM");
|
|
109
|
-
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
110
|
-
child.emit("exit", null, "SIGTERM");
|
|
111
|
-
await expect(running).resolves.toEqual({ handled: true, exitCode: null, signal: "SIGTERM" });
|
|
112
|
-
});
|
|
113
|
-
it("parses data directory overrides before the CLI import", () => {
|
|
114
|
-
expect(parsePreflightDataDir(["start", "--data-dir", "./data"], {}, "/home/me")).toBe(resolve("./data"));
|
|
115
|
-
expect(parsePreflightDataDir([], { AGENTROAM_DATA_DIR: "/custom" }, "/home/me")).toBe("/custom");
|
|
116
|
-
expect(parsePreflightDataDir([], {}, "/home/me")).toBe(resolve("/home/me", ".agentroam"));
|
|
117
|
-
expect(() => parsePreflightDataDir(["--data-dir"], {}, "/home/me")).toThrow("missing value");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
function fakeChild(exitCode, signal) {
|
|
121
|
-
const child = new EventEmitter();
|
|
122
|
-
child.kill = vi.fn();
|
|
123
|
-
setImmediate(() => child.emit("exit", exitCode, signal));
|
|
124
|
-
return child;
|
|
125
|
-
}
|
|
126
|
-
function fakeProcessHost() {
|
|
127
|
-
const host = new EventEmitter();
|
|
128
|
-
host.stderr = { write: vi.fn() };
|
|
129
|
-
return host;
|
|
130
|
-
}
|
|
131
|
-
async function fakeNodeExecutable(path, version) {
|
|
132
|
-
await mkdir(resolve(path, ".."), { recursive: true });
|
|
133
|
-
await writeFile(path, `#!/bin/sh\nprintf '%s\\n' '${version}'\n`);
|
|
134
|
-
await chmod(path, 0o755);
|
|
135
|
-
return path;
|
|
136
|
-
}
|