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,95 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { access, chmod, mkdtemp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
|
-
import { describe, expect, it, vi } from "vitest";
|
|
8
|
-
import { ensureCloudflared, findSystemCloudflared } from "./installer.js";
|
|
9
|
-
describe("ensureCloudflared", () => {
|
|
10
|
-
it("extracts a verified bundled archive and repairs executable permissions", async () => {
|
|
11
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-bundle-"));
|
|
12
|
-
const asset = await createArchive(root, "#!/bin/sh\necho bundled\n");
|
|
13
|
-
const executable = await ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
14
|
-
resolveBundled: async () => asset,
|
|
15
|
-
findSystem: async () => null,
|
|
16
|
-
});
|
|
17
|
-
expect(await readFile(executable, "utf8")).toContain("bundled");
|
|
18
|
-
await access(executable, constants.X_OK);
|
|
19
|
-
expect(await readFile(`${executable}.source.sha256`, "utf8")).toBe(`${asset.sha256}\n`);
|
|
20
|
-
});
|
|
21
|
-
it("reuses the verified cache", async () => {
|
|
22
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-cache-"));
|
|
23
|
-
const asset = await createArchive(root, "#!/bin/sh\n");
|
|
24
|
-
const resolver = vi.fn(async () => asset);
|
|
25
|
-
const options = { resolveBundled: resolver, findSystem: async () => null };
|
|
26
|
-
const first = await ensureCloudflared("darwin-arm64", resolve(root, "data"), options);
|
|
27
|
-
const second = await ensureCloudflared("darwin-arm64", resolve(root, "data"), options);
|
|
28
|
-
expect(second).toBe(first);
|
|
29
|
-
expect(resolver).toHaveBeenCalledTimes(2);
|
|
30
|
-
});
|
|
31
|
-
it("copies a verified Windows executable without tar", async () => {
|
|
32
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-windows-"));
|
|
33
|
-
const assetPath = resolve(root, "source.exe");
|
|
34
|
-
const bytes = Buffer.from("MZ-test-windows-binary");
|
|
35
|
-
await writeFile(assetPath, bytes);
|
|
36
|
-
const asset = {
|
|
37
|
-
assetPath,
|
|
38
|
-
assetFormat: "executable",
|
|
39
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
40
|
-
size: bytes.length,
|
|
41
|
-
fileName: "cloudflared.exe",
|
|
42
|
-
version: "test",
|
|
43
|
-
};
|
|
44
|
-
const executable = await ensureCloudflared("windows-amd64", resolve(root, "data"), {
|
|
45
|
-
resolveBundled: async () => asset,
|
|
46
|
-
findSystem: async () => null,
|
|
47
|
-
});
|
|
48
|
-
expect(executable).toBe(resolve(root, "data/bin/cloudflared.exe"));
|
|
49
|
-
expect(await readFile(executable)).toEqual(bytes);
|
|
50
|
-
});
|
|
51
|
-
it("falls back to PATH when the bundled checksum is invalid", async () => {
|
|
52
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-fallback-"));
|
|
53
|
-
const asset = await createArchive(root, "#!/bin/sh\n");
|
|
54
|
-
const system = resolve(root, "system-cloudflared");
|
|
55
|
-
await writeFile(system, "#!/bin/sh\n", { mode: 0o755 });
|
|
56
|
-
const result = await ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
57
|
-
resolveBundled: async () => ({ ...asset, sha256: "0".repeat(64) }),
|
|
58
|
-
findSystem: async () => system,
|
|
59
|
-
});
|
|
60
|
-
expect(result).toBe(system);
|
|
61
|
-
});
|
|
62
|
-
it("reports package and PATH failures without downloading", async () => {
|
|
63
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-missing-"));
|
|
64
|
-
await expect(ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
65
|
-
resolveBundled: async () => null,
|
|
66
|
-
findSystem: async () => null,
|
|
67
|
-
})).rejects.toThrow("platform package missing for darwin-arm64; no executable on PATH");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
describe("findSystemCloudflared", () => {
|
|
71
|
-
it("reuses an executable cloudflared already installed on PATH", async () => {
|
|
72
|
-
const directory = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-path-"));
|
|
73
|
-
const executable = resolve(directory, "cloudflared");
|
|
74
|
-
await writeFile(executable, "#!/bin/sh\n", { mode: 0o644 });
|
|
75
|
-
expect(await findSystemCloudflared(directory, "darwin")).toBeNull();
|
|
76
|
-
await chmod(executable, 0o755);
|
|
77
|
-
expect(await findSystemCloudflared(directory, "darwin")).toBe(executable);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
async function createArchive(root, content) {
|
|
81
|
-
const source = resolve(root, "source");
|
|
82
|
-
await mkdir(source, { recursive: true });
|
|
83
|
-
await writeFile(resolve(source, "cloudflared"), content, { mode: 0o644 });
|
|
84
|
-
const archivePath = resolve(root, "cloudflared.tgz");
|
|
85
|
-
execFileSync("tar", ["-czf", archivePath, "-C", source, "cloudflared"]);
|
|
86
|
-
const bytes = await readFile(archivePath);
|
|
87
|
-
return {
|
|
88
|
-
assetPath: archivePath,
|
|
89
|
-
assetFormat: "tgz",
|
|
90
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
91
|
-
size: (await stat(archivePath)).size,
|
|
92
|
-
fileName: "cloudflared",
|
|
93
|
-
version: "test",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { chmod, mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { dirname, resolve, win32 } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { CODEX_INSTALL_STALE_LOCK_MS, CODEX_INSTALL_TIMEOUT_MS, CODEX_MINIMUM_VERSION, CODEX_RUNTIME_VERSION, isCodexVersionAtLeast, managedCodexBinaryCandidates, parseCodexVersion, resolveCodexRuntime, resolveNpmExecutor, } from "./codex-runtime-manager.js";
|
|
6
|
-
const temporaryDirectories = [];
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
9
|
-
});
|
|
10
|
-
describe("managed Codex runtime", () => {
|
|
11
|
-
it("parses the official version output", () => {
|
|
12
|
-
expect(parseCodexVersion("codex-cli 0.153.5")).toBe("0.153.5");
|
|
13
|
-
expect(parseCodexVersion("codex-cli next")).toBeNull();
|
|
14
|
-
});
|
|
15
|
-
it("compares versions numerically instead of lexicographically", () => {
|
|
16
|
-
// Samples stay below the current pin: pins only move forward, so these
|
|
17
|
-
// literals can never collide with the upgrade script's version replacement.
|
|
18
|
-
expect(isCodexVersionAtLeast("0.140.12", "0.140.3")).toBe(true);
|
|
19
|
-
expect(isCodexVersionAtLeast("0.140.3", "0.140.12")).toBe(false);
|
|
20
|
-
expect(isCodexVersionAtLeast(CODEX_MINIMUM_VERSION, CODEX_MINIMUM_VERSION)).toBe(true);
|
|
21
|
-
expect(isCodexVersionAtLeast("0.139.9", CODEX_MINIMUM_VERSION)).toBe(false);
|
|
22
|
-
expect(isCodexVersionAtLeast("0.152.0", "0.140.0")).toBe(true);
|
|
23
|
-
expect(isCodexVersionAtLeast("1.0.0", "0.140.0")).toBe(true);
|
|
24
|
-
});
|
|
25
|
-
it("uses a compatible explicit absolute executable first", async () => {
|
|
26
|
-
const root = await temporaryRoot();
|
|
27
|
-
const executable = await fakeExecutable(resolve(root, "codex"));
|
|
28
|
-
const run = vi.fn(async (_command, args) => ({
|
|
29
|
-
stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "app-server help\n",
|
|
30
|
-
stderr: "",
|
|
31
|
-
}));
|
|
32
|
-
await expect(resolveCodexRuntime({
|
|
33
|
-
dataDir: resolve(root, "data"),
|
|
34
|
-
target: "darwin-arm64",
|
|
35
|
-
environment: { AGENT_CODEX_BIN: executable, PATH: "" },
|
|
36
|
-
platform: "darwin",
|
|
37
|
-
dependencies: { run },
|
|
38
|
-
})).resolves.toEqual({
|
|
39
|
-
executable,
|
|
40
|
-
version: CODEX_RUNTIME_VERSION,
|
|
41
|
-
source: "explicit",
|
|
42
|
-
});
|
|
43
|
-
expect(run.mock.calls.map((call) => call[1])).toEqual([["--version"], ["app-server", "--help"]]);
|
|
44
|
-
});
|
|
45
|
-
it("accepts explicit executables at or above the minimum stable version", async () => {
|
|
46
|
-
const outputs = [
|
|
47
|
-
`codex-cli ${CODEX_MINIMUM_VERSION}\n`,
|
|
48
|
-
`codex-cli ${CODEX_RUNTIME_VERSION}\n`,
|
|
49
|
-
];
|
|
50
|
-
for (const output of outputs) {
|
|
51
|
-
const root = await temporaryRoot();
|
|
52
|
-
const executable = await fakeExecutable(resolve(root, "codex"));
|
|
53
|
-
await expect(resolveCodexRuntime({
|
|
54
|
-
dataDir: resolve(root, "data"),
|
|
55
|
-
target: "darwin-arm64",
|
|
56
|
-
environment: { AGENT_CODEX_BIN: executable, PATH: "" },
|
|
57
|
-
platform: "darwin",
|
|
58
|
-
dependencies: {
|
|
59
|
-
run: async (_command, args) => ({
|
|
60
|
-
stdout: args[0] === "--version" ? output : "app-server help\n",
|
|
61
|
-
stderr: "",
|
|
62
|
-
}),
|
|
63
|
-
},
|
|
64
|
-
})).resolves.toMatchObject({ executable, source: "explicit" });
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
it("rejects an explicit executable below the minimum instead of silently replacing it", async () => {
|
|
68
|
-
const root = await temporaryRoot();
|
|
69
|
-
const executable = await fakeExecutable(resolve(root, "codex"));
|
|
70
|
-
await expect(resolveCodexRuntime({
|
|
71
|
-
dataDir: resolve(root, "data"),
|
|
72
|
-
target: "darwin-arm64",
|
|
73
|
-
environment: { AGENT_CODEX_BIN: executable, PATH: "" },
|
|
74
|
-
platform: "darwin",
|
|
75
|
-
dependencies: {
|
|
76
|
-
run: async () => ({ stdout: "codex-cli 0.152.9\n", stderr: "" }),
|
|
77
|
-
},
|
|
78
|
-
})).rejects.toThrow(`requires >=${CODEX_MINIMUM_VERSION}`);
|
|
79
|
-
});
|
|
80
|
-
it("rejects prerelease or otherwise unparseable version output", async () => {
|
|
81
|
-
const root = await temporaryRoot();
|
|
82
|
-
const executable = await fakeExecutable(resolve(root, "codex"));
|
|
83
|
-
await expect(resolveCodexRuntime({
|
|
84
|
-
dataDir: resolve(root, "data"),
|
|
85
|
-
target: "darwin-arm64",
|
|
86
|
-
environment: { AGENT_CODEX_BIN: executable, PATH: "" },
|
|
87
|
-
platform: "darwin",
|
|
88
|
-
dependencies: {
|
|
89
|
-
run: async () => ({ stdout: "codex-cli 0.150.0-alpha.1\n", stderr: "" }),
|
|
90
|
-
},
|
|
91
|
-
})).rejects.toThrow("Unable to parse a supported stable Codex version");
|
|
92
|
-
});
|
|
93
|
-
it("skips an incompatible PATH executable and reuses the managed runtime", async () => {
|
|
94
|
-
const root = await temporaryRoot();
|
|
95
|
-
const global = await fakeExecutable(resolve(root, "bin", "codex"));
|
|
96
|
-
const managedRoot = resolve(root, "data", "runtimes", "codex", CODEX_RUNTIME_VERSION);
|
|
97
|
-
const managed = await fakeExecutable(managedCodexBinaryCandidates(managedRoot, "darwin-arm64")[0]);
|
|
98
|
-
const run = vi.fn(async (command, args) => ({
|
|
99
|
-
stdout: args[0] === "--version"
|
|
100
|
-
? command === global ? "codex-cli 0.152.0\n" : `codex-cli ${CODEX_RUNTIME_VERSION}\n`
|
|
101
|
-
: "help\n",
|
|
102
|
-
stderr: "",
|
|
103
|
-
}));
|
|
104
|
-
await expect(resolveCodexRuntime({
|
|
105
|
-
dataDir: resolve(root, "data"),
|
|
106
|
-
target: "darwin-arm64",
|
|
107
|
-
environment: { PATH: resolve(root, "bin") },
|
|
108
|
-
platform: "darwin",
|
|
109
|
-
dependencies: { run },
|
|
110
|
-
})).resolves.toEqual({ executable: managed, version: CODEX_RUNTIME_VERSION, source: "managed" });
|
|
111
|
-
expect(run.mock.calls.some((call) => call[1].includes("install"))).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
it("reuses a qualified PATH executable without installing", async () => {
|
|
114
|
-
const root = await temporaryRoot();
|
|
115
|
-
const global = await fakeExecutable(resolve(root, "bin", "codex"));
|
|
116
|
-
const run = vi.fn(async (_command, args) => ({
|
|
117
|
-
stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
|
|
118
|
-
stderr: "",
|
|
119
|
-
}));
|
|
120
|
-
await expect(resolveCodexRuntime({
|
|
121
|
-
dataDir: resolve(root, "data"),
|
|
122
|
-
target: "darwin-arm64",
|
|
123
|
-
environment: { PATH: resolve(root, "bin") },
|
|
124
|
-
platform: "darwin",
|
|
125
|
-
dependencies: { run },
|
|
126
|
-
})).resolves.toEqual({ executable: global, version: CODEX_RUNTIME_VERSION, source: "global" });
|
|
127
|
-
expect(run.mock.calls.some((call) => call[1].includes("install"))).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
it("installs the locked official package into a versioned runtime directory", async () => {
|
|
130
|
-
const root = await temporaryRoot();
|
|
131
|
-
const npmExecPath = await fakeExecutable(resolve(root, "npm-cli.js"));
|
|
132
|
-
let installs = 0;
|
|
133
|
-
const run = vi.fn(async (command, args, timeoutMs) => {
|
|
134
|
-
if (command === "/test/node" && args.includes("install")) {
|
|
135
|
-
installs += 1;
|
|
136
|
-
const prefix = args[args.indexOf("--prefix") + 1];
|
|
137
|
-
await fakeExecutable(managedCodexBinaryCandidates(prefix, "darwin-arm64")[0]);
|
|
138
|
-
expect(args).toContain("@openai/codex@0.153.0");
|
|
139
|
-
expect(args).toContain("--registry=https://registry.npmjs.org");
|
|
140
|
-
expect(timeoutMs).toBe(60 * 60_000);
|
|
141
|
-
return { stdout: "installed", stderr: "" };
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
|
|
145
|
-
stderr: "",
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
const resolution = await resolveCodexRuntime({
|
|
149
|
-
dataDir: resolve(root, "data"),
|
|
150
|
-
target: "darwin-arm64",
|
|
151
|
-
environment: { PATH: "", npm_execpath: npmExecPath },
|
|
152
|
-
platform: "darwin",
|
|
153
|
-
nodeExecutable: "/test/node",
|
|
154
|
-
dependencies: { run },
|
|
155
|
-
});
|
|
156
|
-
expect(installs).toBe(1);
|
|
157
|
-
expect(CODEX_INSTALL_STALE_LOCK_MS).toBeGreaterThan(CODEX_INSTALL_TIMEOUT_MS);
|
|
158
|
-
expect(resolution.executable).toContain(`/runtimes/codex/${CODEX_RUNTIME_VERSION}/`);
|
|
159
|
-
expect(resolution.source).toBe("managed");
|
|
160
|
-
});
|
|
161
|
-
it("serializes concurrent first-start installs", async () => {
|
|
162
|
-
const root = await temporaryRoot();
|
|
163
|
-
const npmExecPath = await fakeExecutable(resolve(root, "npm-cli.js"));
|
|
164
|
-
let installs = 0;
|
|
165
|
-
const run = async (command, args) => {
|
|
166
|
-
if (command === "/test/node" && args.includes("install")) {
|
|
167
|
-
installs += 1;
|
|
168
|
-
await new Promise((resolveDelay) => setTimeout(resolveDelay, 30));
|
|
169
|
-
const prefix = args[args.indexOf("--prefix") + 1];
|
|
170
|
-
await fakeExecutable(managedCodexBinaryCandidates(prefix, "darwin-arm64")[0]);
|
|
171
|
-
return { stdout: "installed", stderr: "" };
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
|
|
175
|
-
stderr: "",
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
const options = {
|
|
179
|
-
dataDir: resolve(root, "data"),
|
|
180
|
-
target: "darwin-arm64",
|
|
181
|
-
environment: { PATH: "", npm_execpath: npmExecPath },
|
|
182
|
-
platform: "darwin",
|
|
183
|
-
nodeExecutable: "/test/node",
|
|
184
|
-
dependencies: { run },
|
|
185
|
-
};
|
|
186
|
-
const [first, second] = await Promise.all([
|
|
187
|
-
resolveCodexRuntime(options),
|
|
188
|
-
resolveCodexRuntime(options),
|
|
189
|
-
]);
|
|
190
|
-
expect(installs).toBe(1);
|
|
191
|
-
expect(second.executable).toBe(first.executable);
|
|
192
|
-
});
|
|
193
|
-
it("cleans temporary installs and the lock after npm failure", async () => {
|
|
194
|
-
const root = await temporaryRoot();
|
|
195
|
-
const npmExecPath = await fakeExecutable(resolve(root, "npm-cli.js"));
|
|
196
|
-
await expect(resolveCodexRuntime({
|
|
197
|
-
dataDir: resolve(root, "data"),
|
|
198
|
-
target: "darwin-arm64",
|
|
199
|
-
environment: { PATH: "", npm_execpath: npmExecPath },
|
|
200
|
-
platform: "darwin",
|
|
201
|
-
nodeExecutable: "/test/node",
|
|
202
|
-
dependencies: {
|
|
203
|
-
run: async () => { throw new Error("network unavailable"); },
|
|
204
|
-
},
|
|
205
|
-
})).rejects.toThrow("network unavailable");
|
|
206
|
-
expect(await readdir(resolve(root, "data", "runtimes", "codex"))).toEqual([]);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
describe("Codex npm executor", () => {
|
|
210
|
-
it("runs npm_execpath through the current Node executable", async () => {
|
|
211
|
-
await expect(resolveNpmExecutor({
|
|
212
|
-
environment: { npm_execpath: "/tools/npm-cli.js", PATH: "" },
|
|
213
|
-
nodeExecutable: "/runtime/node",
|
|
214
|
-
canAccess: async (path) => path === "/tools/npm-cli.js",
|
|
215
|
-
})).resolves.toEqual({ command: "/runtime/node", argsPrefix: ["/tools/npm-cli.js"] });
|
|
216
|
-
});
|
|
217
|
-
it("finds a directly executable npm.exe on Windows PATH", async () => {
|
|
218
|
-
const expected = win32.resolve("C:\\tools", "npm.exe");
|
|
219
|
-
await expect(resolveNpmExecutor({
|
|
220
|
-
environment: { PATH: ["C:\\missing", "C:\\tools"].join(";") },
|
|
221
|
-
platform: "win32",
|
|
222
|
-
canAccess: async (path) => path === expected,
|
|
223
|
-
})).resolves.toEqual({ command: expected, argsPrefix: [] });
|
|
224
|
-
});
|
|
225
|
-
it("finds npm-cli.js beside an NVM Node without relying on PATH", async () => {
|
|
226
|
-
const nodeExecutable = "/Users/test/.nvm/versions/node/v22.22.2/bin/node";
|
|
227
|
-
const npmCli = "/Users/test/.nvm/versions/node/v22.22.2/lib/node_modules/npm/bin/npm-cli.js";
|
|
228
|
-
await expect(resolveNpmExecutor({
|
|
229
|
-
environment: { PATH: "" },
|
|
230
|
-
platform: "darwin",
|
|
231
|
-
nodeExecutable,
|
|
232
|
-
canAccess: async (path) => path === npmCli,
|
|
233
|
-
})).resolves.toEqual({ command: nodeExecutable, argsPrefix: [npmCli] });
|
|
234
|
-
});
|
|
235
|
-
it("finds npm-cli.js beside an nvm-windows Node without relying on PATH", async () => {
|
|
236
|
-
const nodeExecutable = "C:\\Users\\test\\AppData\\Roaming\\nvm\\v22.22.2\\node.exe";
|
|
237
|
-
const npmCli = "C:\\Users\\test\\AppData\\Roaming\\nvm\\v22.22.2\\node_modules\\npm\\bin\\npm-cli.js";
|
|
238
|
-
await expect(resolveNpmExecutor({
|
|
239
|
-
environment: { PATH: "" },
|
|
240
|
-
platform: "win32",
|
|
241
|
-
nodeExecutable,
|
|
242
|
-
canAccess: async (path) => path === npmCli,
|
|
243
|
-
})).resolves.toEqual({ command: nodeExecutable, argsPrefix: [npmCli] });
|
|
244
|
-
});
|
|
245
|
-
it("runs a standard Windows npm.cmd installation through npm-cli.js", async () => {
|
|
246
|
-
const npmCmd = win32.resolve("C:\\Program Files\\nodejs", "npm.cmd");
|
|
247
|
-
const npmCli = win32.resolve("C:\\Program Files\\nodejs", "node_modules", "npm", "bin", "npm-cli.js");
|
|
248
|
-
await expect(resolveNpmExecutor({
|
|
249
|
-
environment: { PATH: "C:\\Program Files\\nodejs" },
|
|
250
|
-
platform: "win32",
|
|
251
|
-
nodeExecutable: "C:\\Program Files\\nodejs\\node.exe",
|
|
252
|
-
canAccess: async (path) => path === npmCmd || path === npmCli,
|
|
253
|
-
})).resolves.toEqual({
|
|
254
|
-
command: "C:\\Program Files\\nodejs\\node.exe",
|
|
255
|
-
argsPrefix: [npmCli],
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
it("maps official macOS and Windows optional-package binaries", () => {
|
|
259
|
-
expect(managedCodexBinaryCandidates("/runtime", "darwin-arm64")[0])
|
|
260
|
-
.toContain("codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex");
|
|
261
|
-
expect(managedCodexBinaryCandidates("/runtime", "windows-amd64")[0])
|
|
262
|
-
.toContain("codex-win32-x64/vendor/x86_64-pc-windows-msvc/bin/codex.exe");
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
async function temporaryRoot() {
|
|
266
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-codex-runtime-"));
|
|
267
|
-
temporaryDirectories.push(root);
|
|
268
|
-
return root;
|
|
269
|
-
}
|
|
270
|
-
async function fakeExecutable(path) {
|
|
271
|
-
await mkdir(dirname(path), { recursive: true });
|
|
272
|
-
await writeFile(path, "fake\n");
|
|
273
|
-
await chmod(path, 0o755);
|
|
274
|
-
return path;
|
|
275
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
-
// @ts-expect-error Standalone bootstrap helper ships as JavaScript.
|
|
7
|
-
import { desktopAsset, downloadDesktop } from '../bin/desktop-download.mjs';
|
|
8
|
-
const version = '0.2.0-preview.17';
|
|
9
|
-
const body = Buffer.from('desktop test artifact');
|
|
10
|
-
function manifest() {
|
|
11
|
-
return { schemaVersion: 2, version, channel: 'preview', installers: { desktop: {
|
|
12
|
-
'darwin-arm64': { fileName: `AgentRoam-${version}-arm64.dmg`, sha256: createHash('sha256').update(body).digest('hex'), size: body.length, signed: false },
|
|
13
|
-
'windows-amd64': { fileName: `AgentRoam-Setup-${version}-x64.exe`, sha256: createHash('sha256').update(body).digest('hex'), signed: false },
|
|
14
|
-
} } };
|
|
15
|
-
}
|
|
16
|
-
describe('optional desktop installer download', () => {
|
|
17
|
-
it('selects the versioned Windows asset and rejects wrong versions and filenames', () => {
|
|
18
|
-
expect(desktopAsset(manifest(), version, 'win32', 'x64').fileName).toMatch(/\.exe$/);
|
|
19
|
-
expect(() => desktopAsset(manifest(), '0.2.0-preview.18', 'darwin', 'arm64')).toThrow();
|
|
20
|
-
const invalid = manifest();
|
|
21
|
-
invalid.installers.desktop['darwin-arm64'].fileName = '../unsafe.dmg';
|
|
22
|
-
expect(() => desktopAsset(invalid, version, 'darwin', 'arm64')).toThrow();
|
|
23
|
-
});
|
|
24
|
-
it('downloads and verifies into unique directories without overwriting previous installs', async () => {
|
|
25
|
-
const directory = await mkdtemp(join(tmpdir(), 'desktop-download-'));
|
|
26
|
-
const fetchImpl = vi.fn(async (url) => url.endsWith('.json') ? Response.json(manifest()) : new Response(body));
|
|
27
|
-
try {
|
|
28
|
-
const first = await downloadDesktop(version, { directory, platform: 'darwin', arch: 'arm64', fetchImpl });
|
|
29
|
-
const second = await downloadDesktop(version, { directory, platform: 'darwin', arch: 'arm64', fetchImpl });
|
|
30
|
-
expect(await readFile(first)).toEqual(body);
|
|
31
|
-
expect(second).not.toBe(first);
|
|
32
|
-
expect(fetchImpl.mock.calls[1][0]).toContain(`/v${version}/AgentRoam-${version}-arm64.dmg`);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
await rm(directory, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
it('cleans up partial files on checksum mismatch', async () => {
|
|
39
|
-
const directory = await mkdtemp(join(tmpdir(), 'desktop-download-'));
|
|
40
|
-
const fetchImpl = vi.fn(async (url) => url.endsWith('.json') ? Response.json(manifest()) : new Response('corrupted'));
|
|
41
|
-
try {
|
|
42
|
-
await expect(downloadDesktop(version, { directory, platform: 'darwin', arch: 'arm64', fetchImpl })).rejects.toThrow('checksum');
|
|
43
|
-
expect(await readdir(directory)).toEqual([]);
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
await rm(directory, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
it('rejects missing platform artifacts before downloading binaries', async () => {
|
|
50
|
-
const fetchImpl = vi.fn(async () => Response.json({ ...manifest(), installers: {} }));
|
|
51
|
-
await expect(downloadDesktop(version, { platform: 'darwin', arch: 'arm64', fetchImpl })).rejects.toThrow('no valid');
|
|
52
|
-
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
// Exercise the actual installer tail with an isolated launcher. Nothing installs
|
|
56
|
-
// on the machine; the helper records whether the optional branch ran.
|
|
57
|
-
describe.skipIf(process.platform === 'win32')('Shell desktop choice after CLI installation', () => {
|
|
58
|
-
it.each(['no', 'yes', undefined])('handles choice %s without reading piped script input', async (choice) => {
|
|
59
|
-
const { execFileSync } = await import('node:child_process');
|
|
60
|
-
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
61
|
-
const root = await mkdtemp(join(tmpdir(), 'desktop-choice-'));
|
|
62
|
-
try {
|
|
63
|
-
const bin = join(root, 'node_modules/agentroam/bin');
|
|
64
|
-
await mkdir(bin, { recursive: true });
|
|
65
|
-
const marker = join(root, 'selected');
|
|
66
|
-
await writeFile(join(bin, 'desktop-download.mjs'), `import { writeFileSync } from 'node:fs'; writeFileSync(process.env.CHOICE_MARKER, process.argv[2]);`);
|
|
67
|
-
const installer = await readFile(new URL('../install/install-agentroam.sh', import.meta.url), 'utf8');
|
|
68
|
-
const section = installer.slice(installer.indexOf('# stdin is the script itself'));
|
|
69
|
-
const env = { ...process.env, LAUNCHER_ROOT: root, NODE_BIN: process.execPath, AGENTROAM_VERSION: version, CHOICE_MARKER: marker };
|
|
70
|
-
delete env.AGENTROAM_INSTALL_DESKTOP;
|
|
71
|
-
if (choice)
|
|
72
|
-
Object.assign(env, { AGENTROAM_INSTALL_DESKTOP: choice });
|
|
73
|
-
execFileSync('/bin/sh', ['-c', section], { env, input: 'yes\n' });
|
|
74
|
-
if (choice === 'yes')
|
|
75
|
-
expect(await readFile(marker, 'utf8')).toBe(version);
|
|
76
|
-
else
|
|
77
|
-
expect(await readdir(root)).not.toContain('selected');
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
await rm(root, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { parseArgs } from "./args.js";
|
|
3
|
-
import { pairingQrPayload, watchPairingApproval } from "./device-pairing.js";
|
|
4
|
-
describe("local approval commands", () => {
|
|
5
|
-
it("encodes a separate QR invitation and validates the server URL", () => {
|
|
6
|
-
expect(parseArgs(["pair", "--url", "https://phone.example/web"]).pairingUrl).toBe("https://phone.example/web");
|
|
7
|
-
const link = new URL(pairingQrPayload("https://phone.example/web", "a".repeat(43)));
|
|
8
|
-
expect(link.origin + link.pathname).toBe("https://phone.example/pair");
|
|
9
|
-
expect(link.search).toBe("");
|
|
10
|
-
expect(link.hash).toBe(`#pair=${"a".repeat(43)}`);
|
|
11
|
-
expect(() => pairingQrPayload("https://user:password@phone.example", "a".repeat(43))).toThrow();
|
|
12
|
-
});
|
|
13
|
-
const id = "12345678-1234-1234-1234-123456789abc";
|
|
14
|
-
it("requires a request ID and matching phrase for explicit approval", () => {
|
|
15
|
-
expect(() => parseArgs(["approve", id])).toThrow(/phrase/);
|
|
16
|
-
expect(parseArgs(["approve", id, "--phrase", "松林·月光·1234"])).toMatchObject({ approvalRequestId: id, approvalPhrase: "松林·月光·1234" });
|
|
17
|
-
for (const command of ["lock", "unlock", "audit", "approvals"])
|
|
18
|
-
expect(parseArgs([command]).command).toBe(command);
|
|
19
|
-
expect(parseArgs(["deny", id]).approvalRequestId).toBe(id);
|
|
20
|
-
});
|
|
21
|
-
it.each([true, false])("displays the phrase and sends only the local user's %s decision", async (approve) => {
|
|
22
|
-
const now = Date.now();
|
|
23
|
-
const request = { id, name: "Phone\u001b[31m", phrase: "松林·月光·1234", created: now, expires: now + 300_000, status: "waiting" };
|
|
24
|
-
const admin = vi.fn().mockResolvedValueOnce({ requests: [request], locked: false }).mockResolvedValueOnce({ status: approve ? "approved" : "denied" });
|
|
25
|
-
const log = vi.fn();
|
|
26
|
-
const confirm = vi.fn(async () => approve);
|
|
27
|
-
await watchPairingApproval("/tmp/unused", now + 300_000, { admin: admin, log, confirm });
|
|
28
|
-
expect(log).toHaveBeenCalledWith(`核对短语:${request.phrase}`);
|
|
29
|
-
expect(confirm).toHaveBeenCalledWith(request, undefined);
|
|
30
|
-
expect(admin).toHaveBeenLastCalledWith("/tmp/unused", approve ? "approve" : "deny", { id, ...(approve ? { phrase: request.phrase } : {}) });
|
|
31
|
-
expect(log.mock.calls.flat().join("\n")).not.toContain("\u001b");
|
|
32
|
-
});
|
|
33
|
-
});
|