mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) Two shards on release/v3.8.51 went red in one day with the same signature — "ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only .github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass alone and on re-run: the cleanup races something still writing into the directory (SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner the window opens. 1154 test files do their own cleanup with fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries. One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record): every rm / rmSync / rmdirSync option object with `recursive: true` and no `maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292 files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included. Only the option object changes: no call site, assertion or import is touched. Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292 files; a random 20-file sample runs green (quota-redis-store hangs identically on the untouched tree — it needs a Redis on localhost, an environment matter). The four unit shards on this PR are the full run. * fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff The gate shells out to `git diff` through execFileSync with Node's default 1 MB maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing anything. 64 MB is far above any real PR and costs nothing when unused.
280 lines
11 KiB
TypeScript
280 lines
11 KiB
TypeScript
/**
|
|
* Unit tests: parameterized DNS helpers (addDNSEntries / removeDNSEntries)
|
|
*
|
|
* All execFileWithPassword / runElevatedPowerShell calls are mocked so the
|
|
* test does not touch /etc/hosts or require sudo.
|
|
*
|
|
* Hard Rule #13 assertion: commands use argv array form, no interpolation.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inline mock for systemCommands — must happen before importing dnsConfig.
|
|
// We use module-level state to capture calls.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Track calls made to the fake execFileWithPassword.
|
|
interface ExecCall {
|
|
command: string;
|
|
args: string[];
|
|
stdin: string;
|
|
}
|
|
const execCalls: ExecCall[] = [];
|
|
let execShouldFail = false;
|
|
const fakeCommands = {
|
|
execFileWithPassword: async (command: string, args: string[], _password: string, stdin = "") => {
|
|
execCalls.push({ command, args, stdin });
|
|
if (execShouldFail) throw new Error("fake command failed");
|
|
return "";
|
|
},
|
|
};
|
|
|
|
// We cannot use Node's built-in mock.module in ESM without experimental flags,
|
|
// so we write the hosts file to a temp file and point HOSTS_FILE at it via a
|
|
// thin environment trick: we override the module path at import time.
|
|
// Instead we test via a real /tmp hosts file + a custom execFileWithPassword shim.
|
|
|
|
// Strategy: we write a fresh temp hosts file, then call the *exported* functions
|
|
// directly. For the actual OS-level writes we replace them by monkey-patching
|
|
// the module's internal `execFileWithPassword` dependency through a test-only
|
|
// re-export. But dnsConfig.ts does not expose that. So the cleanest approach
|
|
// for a non-interactive unit test is:
|
|
//
|
|
// 1. Pre-populate the temp hosts file so "already exists" paths are tested.
|
|
// 2. For "write" paths (entries not present), we accept that execFile will
|
|
// fail (no sudo in CI) and assert the correct error is thrown.
|
|
//
|
|
// This gives us coverage for:
|
|
// - checkDNSEntry / hasHostEntry logic (read path)
|
|
// - addDNSEntries idempotency (skips existing)
|
|
// - removeDNSEntries idempotency (skips missing)
|
|
// - removeDNSEntries throws on exec failure
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Set up a temp hosts file and redirect dnsConfig to use it.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dns-test-"));
|
|
const tmpHostsFile = path.join(tmpDir, "hosts");
|
|
|
|
// We need to intercept the HOSTS_FILE constant. Since dnsConfig.ts derives it
|
|
// at module load time from process.platform, we control it by setting an env var
|
|
// THAT IS READ BY the module. dnsConfig uses IS_WIN = process.platform === "win32"
|
|
// and then picks "/etc/hosts" on non-Windows. We cannot change that at runtime.
|
|
//
|
|
// Practical workaround: since the add/remove paths call execFileWithPassword
|
|
// and the test has no sudo, we verify:
|
|
// (a) When entries ALREADY exist → no exec is called (idempotency).
|
|
// (b) When entries are MISSING → exec is attempted (we catch the expected error).
|
|
// (c) checkDNSEntry reads real /etc/hosts but we only assert it returns boolean.
|
|
|
|
// Import the module under test AFTER all setup.
|
|
const dnsModule = await import("../../src/mitm/dns/dnsConfig.ts");
|
|
const {
|
|
addDNSEntries,
|
|
removeDNSEntries,
|
|
addDNSEntry,
|
|
removeDNSEntry,
|
|
checkDNSEntry,
|
|
resolveHostsForAgent,
|
|
} = dnsModule;
|
|
const { ALL_TARGETS } = await import("../../src/mitm/targets/index.ts");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("checkDNSEntry returns a boolean", () => {
|
|
const result = checkDNSEntry();
|
|
assert.equal(typeof result, "boolean");
|
|
});
|
|
|
|
test("addDNSEntries: exported function exists and accepts string[] + password", () => {
|
|
assert.equal(typeof addDNSEntries, "function");
|
|
// Calling with empty list must resolve (no-op)
|
|
return assert.doesNotReject(addDNSEntries([], "any-password"));
|
|
});
|
|
|
|
test("removeDNSEntries: exported function exists and accepts string[] + password", () => {
|
|
assert.equal(typeof removeDNSEntries, "function");
|
|
// Calling with empty list must resolve (no-op)
|
|
return assert.doesNotReject(removeDNSEntries([], "any-password"));
|
|
});
|
|
|
|
test("addDNSEntry (legacy) is a function that delegates for Antigravity hosts", () => {
|
|
assert.equal(typeof addDNSEntry, "function");
|
|
});
|
|
|
|
test("addDNSEntry with agentId resolves agent-specific hosts from ALL_TARGETS", async () => {
|
|
// Cursor target has hosts: ["api2.cursor.sh"]
|
|
// Verify that calling addDNSEntry with agentId="cursor" passes the right hosts.
|
|
// We call with [] to skip actual exec — just verifying the function signature accepts agentId.
|
|
await assert.doesNotReject(
|
|
addDNSEntry("fake-sudo", "cursor", fakeCommands),
|
|
"addDNSEntry must accept optional agentId parameter"
|
|
);
|
|
// Verify host selection without invoking the privileged /etc/hosts writer.
|
|
assert.deepEqual(
|
|
resolveHostsForAgent("cursor"),
|
|
["api2.cursor.sh"],
|
|
"addDNSEntry must resolve hosts for the optional agentId"
|
|
);
|
|
});
|
|
|
|
test("addDNSEntry without agentId falls back to Antigravity hosts (backward compat)", async () => {
|
|
await assert.doesNotReject(
|
|
addDNSEntry("fake-sudo", undefined, fakeCommands),
|
|
"addDNSEntry without agentId must still work for backward compat"
|
|
);
|
|
assert.deepEqual(
|
|
resolveHostsForAgent(),
|
|
[
|
|
"daily-cloudcode-pa.googleapis.com",
|
|
"cloudcode-pa.googleapis.com",
|
|
"daily-cloudcode-pa.sandbox.googleapis.com",
|
|
"autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
],
|
|
"addDNSEntry without agentId must preserve backward-compatible hosts"
|
|
);
|
|
});
|
|
|
|
test("addDNSEntry with unknown agentId falls back to Antigravity hosts", async () => {
|
|
await assert.doesNotReject(
|
|
addDNSEntry("fake-sudo", "__nonexistent_agent__", fakeCommands),
|
|
"addDNSEntry with unknown agentId must fall back to Antigravity hosts"
|
|
);
|
|
assert.deepEqual(
|
|
resolveHostsForAgent("__nonexistent_agent__"),
|
|
resolveHostsForAgent(),
|
|
"unknown agentId must fall back to Antigravity hosts"
|
|
);
|
|
});
|
|
|
|
test("removeDNSEntry (legacy) is a function that delegates for Antigravity hosts", () => {
|
|
assert.equal(typeof removeDNSEntry, "function");
|
|
});
|
|
|
|
test("removeDNSEntry with agentId resolves agent-specific hosts from ALL_TARGETS", async () => {
|
|
await assert.doesNotReject(
|
|
removeDNSEntry("fake-sudo", "copilot"),
|
|
"removeDNSEntry must accept optional agentId parameter"
|
|
);
|
|
});
|
|
|
|
test("resolveHostsForAgent returns Antigravity hosts when agentId is undefined", () => {
|
|
// Verify ALL_TARGETS exists and cursor target has expected hosts
|
|
const cursorTarget = ALL_TARGETS.find((t) => t.id === "cursor");
|
|
assert.ok(cursorTarget, "cursor target must exist in ALL_TARGETS");
|
|
assert.ok(
|
|
cursorTarget.hosts.includes("api2.cursor.sh"),
|
|
"cursor target must include api2.cursor.sh"
|
|
);
|
|
// Codex target
|
|
const codexTarget = ALL_TARGETS.find((t) => t.id === "codex");
|
|
assert.ok(codexTarget, "codex target must exist in ALL_TARGETS");
|
|
assert.ok(codexTarget.hosts.includes("chatgpt.com"), "codex target must include chatgpt.com");
|
|
});
|
|
|
|
test("addDNSEntries batches missing entries with no-op on empty list", async () => {
|
|
// Empty list must resolve immediately (no exec, no error)
|
|
await assert.doesNotReject(addDNSEntries([], "fake-sudo"));
|
|
});
|
|
|
|
test("addDNSEntries: skips hosts already in /etc/hosts (idempotency)", async () => {
|
|
// Read live /etc/hosts and pick the first entry that already exists.
|
|
// If localhost is in /etc/hosts we use it; otherwise skip this sub-assertion.
|
|
let hostsContent = "";
|
|
try {
|
|
hostsContent = fs.readFileSync("/etc/hosts", "utf8");
|
|
} catch {
|
|
// No readable /etc/hosts — skip idempotency check.
|
|
return;
|
|
}
|
|
|
|
// Find a host already present (127.0.0.1 localhost is universal).
|
|
if (!hostsContent.includes("localhost")) return;
|
|
|
|
// addDNSEntries with a host that already has both 127.0.0.1 + ::1 lines
|
|
// should not call execFile. We cannot assert "no exec called" without a
|
|
// module mock, but we can assert no error is thrown and the call resolves.
|
|
await assert.doesNotReject(
|
|
// "localhost" is already in /etc/hosts; trying to add it again should be a no-op.
|
|
addDNSEntries(["localhost"], "fake-sudo-password"),
|
|
"addDNSEntries must not throw when entries already exist"
|
|
);
|
|
});
|
|
|
|
test("removeDNSEntries: skips hosts NOT in /etc/hosts (idempotency)", async () => {
|
|
// A host that almost certainly does not exist in /etc/hosts.
|
|
const fakeHost = `omniroute-test-nonexistent-${Date.now()}.invalid`;
|
|
await assert.doesNotReject(
|
|
removeDNSEntries([fakeHost], "fake-sudo-password"),
|
|
"removeDNSEntries must not throw when host is not present"
|
|
);
|
|
});
|
|
|
|
test("addDNSEntries: calls exec with array-form args (Hard Rule #13 pattern)", async () => {
|
|
// We cannot fully mock execFile in ESM without experimental flags, so we
|
|
// verify structural compliance by inspecting the source file directly.
|
|
const srcPath = new URL("../../src/mitm/dns/dnsConfig.ts", import.meta.url).pathname;
|
|
const src = fs.readFileSync(srcPath, "utf8");
|
|
|
|
// The tee invocation must use array form: args array contains HOSTS_FILE as
|
|
// a string argument, never template-interpolated into a shell string.
|
|
assert.ok(
|
|
src.includes('"-S", "tee", "-a", hostsFilePath()'),
|
|
"addDNSEntries must pass hostsFilePath() as an argv element, not interpolated"
|
|
);
|
|
|
|
// The remove invocation must pass the hosts path and hostname as process.argv,
|
|
// not string-interpolated.
|
|
assert.ok(
|
|
src.includes("REMOVE_HOSTS_ENTRY_SCRIPT, hostsFilePath(), hostname"),
|
|
"removeDNSEntries must pass hostsFilePath() and hostname as argv, not interpolated"
|
|
);
|
|
});
|
|
|
|
test("addDNSEntries: entry passed as stdin data, not shell-interpolated", () => {
|
|
const srcPath = new URL("../../src/mitm/dns/dnsConfig.ts", import.meta.url).pathname;
|
|
const src = fs.readFileSync(srcPath, "utf8");
|
|
|
|
// The stdin `data` is built from the batched entries and sent to tee via pipe —
|
|
// not part of the command array. Verify the pattern appears in the source and
|
|
// that it's passed as the 4th positional arg to execFileWithPassword (stdin),
|
|
// not interpolated into the argv array.
|
|
assert.ok(
|
|
src.includes('missingEntries.map((e) => `${e}\\n`).join("")'),
|
|
"entry content must be built from missingEntries for stdin, not interpolated in args"
|
|
);
|
|
assert.ok(
|
|
src.includes(
|
|
'commands.execFileWithPassword(\n "sudo",\n ["-S", "tee", "-a", hostsFilePath()],\n sudoPassword,\n data\n )'
|
|
),
|
|
"entry data must be passed as stdin to tee, not interpolated in args"
|
|
);
|
|
});
|
|
|
|
test("addDNSEntries: generates both IPv4 and IPv6 lines per host", () => {
|
|
// Validate by reading source — the dnsLines helper must produce both.
|
|
const srcPath = new URL("../../src/mitm/dns/dnsConfig.ts", import.meta.url).pathname;
|
|
const src = fs.readFileSync(srcPath, "utf8");
|
|
assert.ok(src.includes("127.0.0.1 ${hostname}"), "must produce 127.0.0.1 entry");
|
|
assert.ok(src.includes("::1 ${hostname}"), "must produce ::1 entry");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cleanup
|
|
// ---------------------------------------------------------------------------
|
|
test.after(() => {
|
|
try {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
} catch {
|
|
// best effort
|
|
}
|
|
});
|