agentroam 0.2.0-preview.25 → 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/cli.js +7 -0
- package/dist/device-pairing.js +46 -5
- package/dist/error-log.js +113 -0
- package/dist/platform-packages.js +9 -2
- package/dist/runtime-manager.js +3 -0
- 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
package/dist/args.test.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { parseArgs } from "./args.js";
|
|
4
|
-
describe("parseArgs", () => {
|
|
5
|
-
it("uses auto relay and the current directory by default", () => {
|
|
6
|
-
const value = parseArgs([]);
|
|
7
|
-
expect(value.command).toBe("start");
|
|
8
|
-
expect(value.serviceAction).toBeNull();
|
|
9
|
-
expect(value.unlockServiceAction).toBeNull();
|
|
10
|
-
expect(value.roots).toEqual([process.cwd()]);
|
|
11
|
-
expect(value.relay).toBe("auto");
|
|
12
|
-
});
|
|
13
|
-
it("parses Windows unlock service actions", () => {
|
|
14
|
-
for (const action of ["install", "uninstall", "status"]) {
|
|
15
|
-
expect(parseArgs(["unlock-service", action])).toMatchObject({ command: "unlock-service", unlockServiceAction: action });
|
|
16
|
-
}
|
|
17
|
-
expect(() => parseArgs(["unlock-service"])).toThrow("requires install");
|
|
18
|
-
expect(() => parseArgs(["unlock-service", "restart"])).toThrow("requires install");
|
|
19
|
-
expect(() => parseArgs(["unlock-service", "status", "--data-dir", "/tmp/x"])).toThrow("accepts no options");
|
|
20
|
-
});
|
|
21
|
-
it("parses every supported relay", () => {
|
|
22
|
-
expect(parseArgs(["--relay", "cloudflare"]).relay).toBe("cloudflare");
|
|
23
|
-
expect(parseArgs(["--relay", "pinggy"]).relay).toBe("pinggy");
|
|
24
|
-
expect(parseArgs(["--relay", "custom", "--tunnel-command", "relay {port}"]).relay).toBe("custom");
|
|
25
|
-
});
|
|
26
|
-
it("parses repeatable roots and custom relay options", () => {
|
|
27
|
-
const value = parseArgs([
|
|
28
|
-
"start", "--root", ".", "--root", "..", "--relay", "custom", "--tunnel-command", "relay {port}", "--no-qr",
|
|
29
|
-
]);
|
|
30
|
-
expect(value.roots).toHaveLength(2);
|
|
31
|
-
expect(value.tunnelCommand).toContain("{port}");
|
|
32
|
-
expect(value.qr).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
it("rejects invalid ports and relay names", () => {
|
|
35
|
-
expect(() => parseArgs(["--port", "70000"])).toThrow();
|
|
36
|
-
expect(() => parseArgs(["--relay", "unknown"])).toThrow("relay must be auto");
|
|
37
|
-
});
|
|
38
|
-
it("parses every service action", () => {
|
|
39
|
-
for (const action of ["install", "start", "stop", "status", "url", "logs", "restart", "uninstall"]) {
|
|
40
|
-
const value = parseArgs(["service", action]);
|
|
41
|
-
expect(value.command).toBe("service");
|
|
42
|
-
expect(value.serviceAction).toBe(action);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
it("parses service install using the existing start options", () => {
|
|
46
|
-
const value = parseArgs([
|
|
47
|
-
"service", "install", "--root", ".", "--root", "..", "--port", "3210", "--relay", "cloudflare",
|
|
48
|
-
"--data-dir", "./service-data", "--no-qr",
|
|
49
|
-
]);
|
|
50
|
-
expect(value.roots).toHaveLength(2);
|
|
51
|
-
expect(value.port).toBe(3210);
|
|
52
|
-
expect(value.relay).toBe("cloudflare");
|
|
53
|
-
expect(value.dataDir).toBe(resolve(process.cwd(), "service-data"));
|
|
54
|
-
expect(value.qr).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
it("rejects missing service actions and options on non-install actions", () => {
|
|
57
|
-
expect(() => parseArgs(["service"])).toThrow("service requires");
|
|
58
|
-
expect(() => parseArgs(["service", "unknown"])).toThrow("service requires");
|
|
59
|
-
expect(() => parseArgs(["service", "status", "--root", "."])).toThrow("does not accept options");
|
|
60
|
-
expect(() => parseArgs(["service", "start", "--port", "3000"])).toThrow("does not accept options");
|
|
61
|
-
expect(() => parseArgs(["service", "stop", "--local-only"])).toThrow("does not accept options");
|
|
62
|
-
});
|
|
63
|
-
it("parses update with an optional exact stable version", () => {
|
|
64
|
-
expect(parseArgs(["update"]).updateVersion).toBeNull();
|
|
65
|
-
expect(parseArgs(["update", "1.2.3", "--data-dir", "./data"])).toMatchObject({ command: "update", updateVersion: "1.2.3" });
|
|
66
|
-
expect(() => parseArgs(["update", "1.2.3-preview.1"])).toThrow("exact stable");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe("device authorization commands", () => {
|
|
70
|
-
it("parses pairing, listing and individual/all revocation", () => {
|
|
71
|
-
expect(parseArgs(["pair"]).command).toBe("pair");
|
|
72
|
-
expect(parseArgs(["devices"]).command).toBe("devices");
|
|
73
|
-
expect(parseArgs(["revoke", "--all"]).revokeAll).toBe(true);
|
|
74
|
-
expect(parseArgs(["revoke", "12345678-1234-1234-1234-123456789abc"]).revokeDeviceId).toBe("12345678-1234-1234-1234-123456789abc");
|
|
75
|
-
expect(() => parseArgs(["revoke"])).toThrow();
|
|
76
|
-
expect(parseArgs(["pair", "--data-dir", "/tmp/device-test"]).dataDir).toBe("/tmp/device-test");
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
it("restricts no-pairing test mode to explicit local foreground starts", () => {
|
|
80
|
-
expect(parseArgs(["start", "--local-only", "--test-no-pairing"]).testNoPairing).toBe(true);
|
|
81
|
-
expect(parseArgs([]).testNoPairing).toBeUndefined();
|
|
82
|
-
for (const args of [["--test-no-pairing"], ["service", "install", "--local-only", "--test-no-pairing"], ["start", "--local-only", "--test-no-pairing", "--relay", "pinggy"]])
|
|
83
|
-
expect(() => parseArgs(args)).toThrow();
|
|
84
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { PLATFORM_DEPENDENCY_VERSIONS } from "../platform-packages.js";
|
|
6
|
-
import { resolveBundledCloudflared } from "./bundled-asset.js";
|
|
7
|
-
describe("resolveBundledCloudflared", () => {
|
|
8
|
-
it("resolves a valid platform package manifest and archive", async () => {
|
|
9
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-bundled-resolver-"));
|
|
10
|
-
const manifestPath = resolve(root, "manifest.json");
|
|
11
|
-
const archivePath = resolve(root, "cloudflared.tgz");
|
|
12
|
-
await writeFile(archivePath, "archive");
|
|
13
|
-
await writeFile(manifestPath, JSON.stringify({
|
|
14
|
-
packageVersion: PLATFORM_DEPENDENCY_VERSIONS["agentroam-cloudflared-darwin-arm64"],
|
|
15
|
-
upstreamVersion: "2026.8.2",
|
|
16
|
-
target: "darwin-arm64",
|
|
17
|
-
assetFormat: "tgz",
|
|
18
|
-
fileName: "cloudflared",
|
|
19
|
-
size: 7,
|
|
20
|
-
sha256: "a".repeat(64),
|
|
21
|
-
}));
|
|
22
|
-
const asset = await resolveBundledCloudflared("darwin-arm64", fakeRequire(manifestPath, archivePath));
|
|
23
|
-
expect(asset).toEqual({
|
|
24
|
-
assetPath: archivePath,
|
|
25
|
-
assetFormat: "tgz",
|
|
26
|
-
version: "2026.8.2",
|
|
27
|
-
fileName: "cloudflared",
|
|
28
|
-
size: 7,
|
|
29
|
-
sha256: "a".repeat(64),
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
it("rejects a mismatched platform package version", async () => {
|
|
33
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-bundled-version-"));
|
|
34
|
-
const manifestPath = resolve(root, "manifest.json");
|
|
35
|
-
const archivePath = resolve(root, "cloudflared.tgz");
|
|
36
|
-
await writeFile(archivePath, "archive");
|
|
37
|
-
await writeFile(manifestPath, JSON.stringify({
|
|
38
|
-
packageVersion: "0.2.0-preview.2",
|
|
39
|
-
upstreamVersion: "2026.8.2",
|
|
40
|
-
target: "darwin-arm64",
|
|
41
|
-
assetFormat: "tgz",
|
|
42
|
-
fileName: "cloudflared",
|
|
43
|
-
size: 7,
|
|
44
|
-
sha256: "a".repeat(64),
|
|
45
|
-
}));
|
|
46
|
-
await expect(resolveBundledCloudflared("darwin-arm64", fakeRequire(manifestPath, archivePath)))
|
|
47
|
-
.rejects.toThrow("invalid cloudflared platform manifest");
|
|
48
|
-
});
|
|
49
|
-
it("checks Windows against its own pin when platform versions differ", async () => {
|
|
50
|
-
const key = "agentroam-cloudflared-win32-x64";
|
|
51
|
-
const previous = PLATFORM_DEPENDENCY_VERSIONS[key];
|
|
52
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-mixed-cloudflared-"));
|
|
53
|
-
const manifestPath = resolve(root, "manifest.json");
|
|
54
|
-
try {
|
|
55
|
-
PLATFORM_DEPENDENCY_VERSIONS[key] = "0.2.0-preview.1";
|
|
56
|
-
await writeFile(manifestPath, JSON.stringify({ packageVersion: "0.2.0-preview.1", upstreamVersion: "2026.8.2", target: "windows-amd64", assetFormat: "executable", fileName: "cloudflared.exe", size: 6, sha256: "b".repeat(64) }));
|
|
57
|
-
await expect(resolveBundledCloudflared("windows-amd64", fakeRequire(manifestPath, "binary"))).resolves.toMatchObject({ assetPath: "binary" });
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
PLATFORM_DEPENDENCY_VERSIONS[key] = previous;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
it("resolves a Windows executable asset", async () => {
|
|
64
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-bundled-windows-"));
|
|
65
|
-
const manifestPath = resolve(root, "manifest.json");
|
|
66
|
-
const assetPath = resolve(root, "cloudflared.exe");
|
|
67
|
-
await writeFile(assetPath, "binary");
|
|
68
|
-
await writeFile(manifestPath, JSON.stringify({
|
|
69
|
-
packageVersion: PLATFORM_DEPENDENCY_VERSIONS["agentroam-cloudflared-win32-x64"],
|
|
70
|
-
upstreamVersion: "2026.8.2",
|
|
71
|
-
target: "windows-amd64",
|
|
72
|
-
assetFormat: "executable",
|
|
73
|
-
fileName: "cloudflared.exe",
|
|
74
|
-
size: 6,
|
|
75
|
-
sha256: "b".repeat(64),
|
|
76
|
-
}));
|
|
77
|
-
await expect(resolveBundledCloudflared("windows-amd64", fakeRequire(manifestPath, assetPath))).resolves.toEqual({
|
|
78
|
-
assetPath,
|
|
79
|
-
assetFormat: "executable",
|
|
80
|
-
version: "2026.8.2",
|
|
81
|
-
fileName: "cloudflared.exe",
|
|
82
|
-
size: 6,
|
|
83
|
-
sha256: "b".repeat(64),
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
function fakeRequire(manifestPath, archivePath) {
|
|
88
|
-
return {
|
|
89
|
-
resolve: (id) => id.endsWith("/manifest.json") ? manifestPath : archivePath,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
@@ -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
|
-
});
|