315 lines
12 KiB
JavaScript
315 lines
12 KiB
JavaScript
// Tests for the pure helpers in src/omnigent_cli.js, run with `node --test`
|
|
// (no extra deps). The spawning functions need a real binary and are covered by
|
|
// the manual verification flow; here we test path resolution order, server-URL
|
|
// matching, and status parsing — the logic that decides "is this machine
|
|
// connected to server X?" and "which omnigent binary do we run?".
|
|
|
|
const { describe, it, mock, afterEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("fs");
|
|
|
|
const {
|
|
normalizeServerUrl,
|
|
isLoopbackServer,
|
|
sameLoopbackServer,
|
|
parseLocalServerPidfile,
|
|
candidatePaths,
|
|
resolveCliPath,
|
|
parseJsonLoose,
|
|
matchesServer,
|
|
parseDaemonRecord,
|
|
daemonServerUrl,
|
|
getHostConnectionFast,
|
|
} = require("../src/omnigent_cli");
|
|
|
|
describe("normalizeServerUrl", () => {
|
|
it("strips trailing slashes and trims", () => {
|
|
assert.equal(normalizeServerUrl("https://x.com/"), "https://x.com");
|
|
assert.equal(normalizeServerUrl(" http://localhost:6767// "), "http://localhost:6767");
|
|
assert.equal(normalizeServerUrl("https://x.com/ml/omnigents"), "https://x.com/ml/omnigents");
|
|
});
|
|
|
|
it("returns empty string for non-strings", () => {
|
|
assert.equal(normalizeServerUrl(undefined), "");
|
|
assert.equal(normalizeServerUrl(null), "");
|
|
assert.equal(normalizeServerUrl(42), "");
|
|
});
|
|
});
|
|
|
|
describe("isLoopbackServer", () => {
|
|
it("is true for loopback hosts", () => {
|
|
assert.equal(isLoopbackServer("http://localhost:6767"), true);
|
|
assert.equal(isLoopbackServer("http://127.0.0.1:6767"), true);
|
|
assert.equal(isLoopbackServer("http://[::1]:6767"), true);
|
|
});
|
|
|
|
it("is false for remote hosts and junk", () => {
|
|
assert.equal(isLoopbackServer("https://example.databricksapps.com"), false);
|
|
assert.equal(isLoopbackServer("not a url"), false);
|
|
});
|
|
});
|
|
|
|
describe("sameLoopbackServer", () => {
|
|
it("matches loopback hosts on the same port (localhost == 127.0.0.1)", () => {
|
|
assert.equal(sameLoopbackServer("http://127.0.0.1:6767", "http://localhost:6767/"), true);
|
|
assert.equal(sameLoopbackServer("http://localhost:6767", "http://[::1]:6767"), true);
|
|
});
|
|
|
|
it("does not match different ports", () => {
|
|
assert.equal(sameLoopbackServer("http://127.0.0.1:6767", "http://localhost:8000"), false);
|
|
});
|
|
|
|
it("does not match when either side is remote, or on junk", () => {
|
|
assert.equal(sameLoopbackServer("http://localhost:6767", "https://example.com:6767"), false);
|
|
assert.equal(sameLoopbackServer("not a url", "http://localhost:6767"), false);
|
|
});
|
|
});
|
|
|
|
describe("parseLocalServerPidfile", () => {
|
|
it("parses pid then port", () => {
|
|
assert.deepEqual(parseLocalServerPidfile("12345\n6767\n"), { pid: 12345, port: 6767 });
|
|
assert.deepEqual(parseLocalServerPidfile("42\n8000"), { pid: 42, port: 8000 });
|
|
});
|
|
|
|
it("returns null for malformed contents", () => {
|
|
assert.equal(parseLocalServerPidfile("12345"), null); // only one line
|
|
assert.equal(parseLocalServerPidfile("abc\ndef"), null); // non-numeric
|
|
assert.equal(parseLocalServerPidfile(""), null);
|
|
assert.equal(parseLocalServerPidfile(null), null);
|
|
});
|
|
});
|
|
|
|
describe("candidatePaths", () => {
|
|
it("probes both the omnigent name and the omni alias in each location", () => {
|
|
const paths = candidatePaths();
|
|
// Every well-known dir contributes an `omnigent` and an `omni` entry.
|
|
assert.ok(paths.some((p) => p.endsWith("/.local/bin/omnigent")));
|
|
assert.ok(paths.some((p) => p.endsWith("/.local/bin/omni")));
|
|
assert.ok(paths.includes("/opt/homebrew/bin/omnigent"));
|
|
assert.ok(paths.includes("/opt/homebrew/bin/omni"));
|
|
assert.ok(paths.includes("/usr/local/bin/omni"));
|
|
});
|
|
|
|
it("lists the canonical omnigent name before the omni alias within a dir", () => {
|
|
const paths = candidatePaths();
|
|
const og = paths.indexOf("/opt/homebrew/bin/omnigent");
|
|
const omni = paths.indexOf("/opt/homebrew/bin/omni");
|
|
assert.ok(og !== -1 && omni !== -1 && og < omni);
|
|
});
|
|
});
|
|
|
|
describe("resolveCliPath", () => {
|
|
it("resolves the omni alias when only it is executable", () => {
|
|
const got = resolveCliPath(null, {
|
|
isExecutableFile: (p) => p === "/home/me/.local/bin/omni",
|
|
whichOmnigent: () => null,
|
|
candidatePaths: () => ["/home/me/.local/bin/omnigent", "/home/me/.local/bin/omni"],
|
|
});
|
|
assert.deepEqual(got, { path: "/home/me/.local/bin/omni", source: "candidate" });
|
|
});
|
|
|
|
it("prefers a usable configured path", () => {
|
|
const got = resolveCliPath("/custom/omnigent", {
|
|
isExecutableFile: (p) => p === "/custom/omnigent",
|
|
whichOmnigent: () => "/usr/bin/omnigent",
|
|
candidatePaths: () => ["/home/me/.local/bin/omnigent"],
|
|
});
|
|
assert.deepEqual(got, { path: "/custom/omnigent", source: "configured" });
|
|
});
|
|
|
|
it("falls back to PATH when the configured path is unusable", () => {
|
|
const got = resolveCliPath("/bad/path", {
|
|
isExecutableFile: (p) => p === "/usr/bin/omnigent",
|
|
whichOmnigent: () => "/usr/bin/omnigent",
|
|
candidatePaths: () => ["/home/me/.local/bin/omnigent"],
|
|
});
|
|
assert.deepEqual(got, { path: "/usr/bin/omnigent", source: "path" });
|
|
});
|
|
|
|
it("falls back to a candidate when PATH misses (GUI minimal PATH)", () => {
|
|
const got = resolveCliPath(null, {
|
|
isExecutableFile: (p) => p === "/home/me/.local/bin/omnigent",
|
|
whichOmnigent: () => null,
|
|
candidatePaths: () => ["/home/me/.local/bin/omnigent", "/opt/homebrew/bin/omnigent"],
|
|
});
|
|
assert.deepEqual(got, { path: "/home/me/.local/bin/omnigent", source: "candidate" });
|
|
});
|
|
|
|
it("returns null when nothing is usable", () => {
|
|
const got = resolveCliPath(null, {
|
|
isExecutableFile: () => false,
|
|
whichOmnigent: () => null,
|
|
candidatePaths: () => ["/a", "/b"],
|
|
});
|
|
assert.equal(got, null);
|
|
});
|
|
});
|
|
|
|
describe("parseJsonLoose", () => {
|
|
it("parses clean JSON", () => {
|
|
assert.deepEqual(parseJsonLoose('{"running": true}'), { running: true });
|
|
});
|
|
|
|
it("recovers JSON after a stray warning line", () => {
|
|
assert.deepEqual(parseJsonLoose('WARN: something\n{"running": false}\n'), {
|
|
running: false,
|
|
});
|
|
});
|
|
|
|
it("returns null for empty or unparseable output", () => {
|
|
assert.equal(parseJsonLoose(""), null);
|
|
assert.equal(parseJsonLoose("not json"), null);
|
|
});
|
|
});
|
|
|
|
describe("matchesServer", () => {
|
|
it("matches on server_url or target, ignoring trailing slashes", () => {
|
|
assert.equal(matchesServer({ server_url: "https://x.com/" }, "https://x.com"), true);
|
|
assert.equal(matchesServer({ target: "https://x.com" }, "https://x.com/"), true);
|
|
});
|
|
|
|
it("matches a local-mode daemon by its resolved_server_url", () => {
|
|
// target "local", server_url null — only resolved_server_url has the URL.
|
|
assert.equal(
|
|
matchesServer(
|
|
{ target: "local", server_url: null, resolved_server_url: "http://127.0.0.1:6767" },
|
|
"http://127.0.0.1:6767/",
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("does not match a different server", () => {
|
|
assert.equal(matchesServer({ server_url: "https://y.com" }, "https://x.com"), false);
|
|
});
|
|
|
|
it("is false for junk daemons or empty target", () => {
|
|
assert.equal(matchesServer(null, "https://x.com"), false);
|
|
assert.equal(matchesServer({ server_url: "https://x.com" }, ""), false);
|
|
});
|
|
});
|
|
|
|
describe("parseDaemonRecord", () => {
|
|
it("parses a server-mode record, keeping pid/target/urls", () => {
|
|
assert.deepEqual(
|
|
parseDaemonRecord({
|
|
pid: 4242,
|
|
target: "https://x.com",
|
|
mode: "server",
|
|
server_url: "https://x.com",
|
|
host_id: "host_abc",
|
|
log_path: "/tmp/x.log",
|
|
}),
|
|
{
|
|
pid: 4242,
|
|
target: "https://x.com",
|
|
mode: "server",
|
|
server_url: "https://x.com",
|
|
resolved_server_url: null,
|
|
host_id: "host_abc",
|
|
log_path: "/tmp/x.log",
|
|
},
|
|
);
|
|
});
|
|
|
|
it("coerces a string pid (registry writes it either way)", () => {
|
|
assert.equal(parseDaemonRecord({ pid: "99", target: "local", mode: "local" }).pid, 99);
|
|
});
|
|
|
|
it("rejects malformed records", () => {
|
|
assert.equal(parseDaemonRecord(null), null);
|
|
assert.equal(parseDaemonRecord({ target: "local", mode: "local" }), null); // no pid
|
|
assert.equal(parseDaemonRecord({ pid: 0, target: "local", mode: "local" }), null); // bad pid
|
|
assert.equal(parseDaemonRecord({ pid: 5, target: "", mode: "local" }), null); // empty target
|
|
assert.equal(parseDaemonRecord({ pid: 5, target: "x", mode: "weird" }), null); // bad mode
|
|
});
|
|
});
|
|
|
|
describe("daemonServerUrl", () => {
|
|
it("uses resolved_server_url for a local-mode daemon, stripping trailing slash", () => {
|
|
assert.equal(
|
|
daemonServerUrl({ mode: "local", resolved_server_url: "http://127.0.0.1:6767/" }),
|
|
"http://127.0.0.1:6767",
|
|
);
|
|
});
|
|
|
|
it("uses server_url (then target) for a server-mode daemon", () => {
|
|
assert.equal(
|
|
daemonServerUrl({ mode: "server", server_url: "https://x.com/" }),
|
|
"https://x.com",
|
|
);
|
|
assert.equal(
|
|
daemonServerUrl({ mode: "server", server_url: null, target: "https://y.com" }),
|
|
"https://y.com",
|
|
);
|
|
});
|
|
|
|
it("is null for a falsy record", () => {
|
|
assert.equal(daemonServerUrl(null), null);
|
|
});
|
|
});
|
|
|
|
describe("getHostConnectionFast — probe destination & token handling (S1)", () => {
|
|
afterEach(() => {
|
|
mock.restoreAll();
|
|
delete process.env.OMNIGENT_REMOTE_AUTH_TOKEN;
|
|
});
|
|
|
|
it("probes the window's serverUrl, never a server_url re-derived from the daemon record", async () => {
|
|
// Planted record: `target` matches the loopback server the window is on (so
|
|
// matchesServer hits), but `server_url` points at an attacker host. The
|
|
// probe must go to the window's serverUrl, not to that disk-record URL.
|
|
const record = {
|
|
pid: process.pid, // a live pid → process is "online", so we reach the probe
|
|
target: "http://localhost:6767",
|
|
mode: "server",
|
|
server_url: "https://evil.com",
|
|
host_id: "host_abc",
|
|
};
|
|
mock.method(fs, "readdirSync", () => ["x.json"]);
|
|
mock.method(fs, "readFileSync", () => JSON.stringify(record));
|
|
|
|
const calls = [];
|
|
mock.method(globalThis, "fetch", async (target, init) => {
|
|
calls.push({ target, headers: init?.headers ?? {} });
|
|
return { ok: true, json: async () => ({ status: "online" }) };
|
|
});
|
|
|
|
const res = await getHostConnectionFast("http://localhost:6767", { timeoutMs: 100 });
|
|
|
|
assert.equal(calls.length, 1);
|
|
// Exact-match the full probe URL: it pins the host to the window's
|
|
// serverUrl, proving the probe did NOT go to the record's `server_url`
|
|
// (https://evil.com).
|
|
assert.equal(calls[0].target, "http://localhost:6767/v1/hosts/host_abc");
|
|
assert.equal(res.connected, true);
|
|
});
|
|
|
|
it("never attaches OMNIGENT_REMOTE_AUTH_TOKEN to a probe", async () => {
|
|
// The env token is destination-independent, so the desktop no longer reads
|
|
// it. Even on a loopback probe (which would attach any available bearer),
|
|
// the Authorization header stays absent when only the env var is set.
|
|
process.env.OMNIGENT_REMOTE_AUTH_TOKEN = "secret-token";
|
|
const record = {
|
|
pid: process.pid,
|
|
target: "http://localhost:6767",
|
|
mode: "server",
|
|
server_url: "http://localhost:6767",
|
|
host_id: "host_abc",
|
|
};
|
|
mock.method(fs, "readdirSync", () => ["x.json"]);
|
|
mock.method(fs, "readFileSync", () => JSON.stringify(record));
|
|
|
|
const calls = [];
|
|
mock.method(globalThis, "fetch", async (target, init) => {
|
|
calls.push({ target, headers: init?.headers ?? {} });
|
|
return { ok: true, json: async () => ({ status: "online" }) };
|
|
});
|
|
|
|
await getHostConnectionFast("http://localhost:6767", { timeoutMs: 100 });
|
|
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].headers.Authorization, undefined);
|
|
});
|
|
});
|