Files
OmniRoute/tests/unit/cursor-agent-models.test.ts
Diego Rodrigues de Sa e Souza 3d4f3e4960 test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) (#11968)
* 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.
2026-08-29 01:17:40 -03:00

301 lines
12 KiB
TypeScript

import test, { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
humanizeCursorModelId,
parseCursorAgentModels,
resolveCursorAgentBinary,
runCursorAgent,
} from "../../src/lib/providerModels/cursorAgent";
test("parseCursorAgentModels returns every reported id including auto and composer-*", () => {
const text =
"Cannot use this model: --help. Available models: auto, composer-2, composer-2-fast, gpt-5.3-codex-low, claude-opus-4-7-thinking-high, kimi-k2.5";
assert.deepEqual(parseCursorAgentModels(text), [
"auto",
"composer-2",
"composer-2-fast",
"gpt-5.3-codex-low",
"claude-opus-4-7-thinking-high",
"kimi-k2.5",
]);
});
test("parseCursorAgentModels deduplicates and trims", () => {
assert.deepEqual(parseCursorAgentModels("Available models: a, a , b"), ["a", "b"]);
});
test("parseCursorAgentModels parses the multiline output from the models command", () => {
const text = `Available models
auto - Auto (default)
gpt-5.3-codex - Codex 5.3
claude-opus-4-8-thinking-high-fast - Opus 4.8 1M Thinking Fast
Tip: use --model <id> to switch.`;
assert.deepEqual(parseCursorAgentModels(text), [
"auto",
"gpt-5.3-codex",
"claude-opus-4-8-thinking-high-fast",
]);
});
test("parseCursorAgentModels returns [] when the marker is missing", () => {
assert.deepEqual(parseCursorAgentModels("nothing here"), []);
});
test("humanizeCursorModelId pretty-prints common patterns", () => {
assert.equal(humanizeCursorModelId("auto"), "Auto (Server Picks)");
assert.equal(humanizeCursorModelId("composer-2-fast"), "Composer 2 Fast");
assert.equal(humanizeCursorModelId("gpt-5.3-codex-low"), "GPT 5.3 Codex Low");
assert.equal(humanizeCursorModelId("gpt-5.5-extra-high-fast"), "GPT 5.5 Extra High Fast");
// Collapses claude-opus-4-7-* version pattern into 4.7
assert.equal(
humanizeCursorModelId("claude-opus-4-7-thinking-high"),
"Claude Opus 4.7 Thinking High"
);
assert.equal(
humanizeCursorModelId("claude-opus-4-8-thinking-high-fast"),
"Claude Opus 4.8 Thinking High Fast"
);
assert.equal(
humanizeCursorModelId("claude-fable-5-thinking-xhigh"),
"Claude Fable 5 Thinking XHigh"
);
assert.equal(humanizeCursorModelId("claude-sonnet-5-max"), "Claude Sonnet 5 Max");
assert.equal(humanizeCursorModelId("kimi-k2.5"), "Kimi K2.5");
assert.equal(humanizeCursorModelId("gemini-3.1-pro"), "Gemini 3.1 Pro");
assert.equal(humanizeCursorModelId("claude-4-sonnet-thinking"), "Claude 4 Sonnet Thinking");
// Grok 4.5 uses infix -fast- (unlike GPT's trailing -fast)
assert.equal(humanizeCursorModelId("grok-4.5-medium"), "Grok 4.5 Medium");
assert.equal(humanizeCursorModelId("grok-4.5-fast-medium"), "Grok 4.5 Fast Medium");
assert.equal(humanizeCursorModelId("grok-4.5-xhigh"), "Grok 4.5 XHigh");
assert.equal(humanizeCursorModelId("grok-4.5-fast-xhigh"), "Grok 4.5 Fast XHigh");
});
// --- Cursor renewal plan, Task 2 Step 1: resolveCursorAgentBinary()/runCursorAgent() ---
//
// resolveCursorAgentBinary() checks a fixed list of absolute candidate paths
// (one of which is HOME-relative: `~/.local/bin/cursor-agent`) before an
// optional PATH scan. It has no injectable candidate list, so the 4 other
// hardcoded absolute paths (/root/.local/bin, /usr/local/bin, /usr/bin,
// /opt/homebrew/bin) are outside test control. On a host that genuinely has
// cursor-agent installed at one of those paths (true on at least one dev
// machine, via Homebrew Cask `cursor-cli`), the "nothing found" branches
// cannot be made hermetic without either touching real system files (out of
// scope — that's someone's actual local install) or a DI seam in production
// code (out of this test-writer's scope). Those specific cases are guarded
// with a runtime-computed `skip` reason instead of being silently omitted.
function ambientFixedCursorAgentPath(): string | null {
const candidates = [
"/root/.local/bin/cursor-agent",
"/usr/local/bin/cursor-agent",
"/usr/bin/cursor-agent",
"/opt/homebrew/bin/cursor-agent",
];
return candidates.find((c) => fs.existsSync(c)) ?? null;
}
function writeFakeBinary(destPath: string, script: string): void {
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, script, { mode: 0o755 });
fs.chmodSync(destPath, 0o755);
}
const NOOP_SCRIPT = "#!/usr/bin/env node\n";
describe("resolveCursorAgentBinary", () => {
const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_USERPROFILE = process.env.USERPROFILE;
const ORIGINAL_PATH = process.env.PATH;
let tmpHome: string;
beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-resolve-cursor-agent-"));
process.env.HOME = tmpHome;
process.env.USERPROFILE = tmpHome;
});
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
if (ORIGINAL_USERPROFILE !== undefined) process.env.USERPROFILE = ORIGINAL_USERPROFILE;
else delete process.env.USERPROFILE;
process.env.PATH = ORIGINAL_PATH;
fs.rmSync(tmpHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
it("finds the HOME-relative fixed candidate (~/.local/bin/cursor-agent) with allowPathFallback:false", () => {
const binary = path.join(tmpHome, ".local", "bin", "cursor-agent");
writeFakeBinary(binary, NOOP_SCRIPT);
assert.equal(resolveCursorAgentBinary({ allowPathFallback: false }), binary);
});
it("finds the HOME-relative fixed candidate with allowPathFallback:true (byte-identical default caller behavior)", () => {
const binary = path.join(tmpHome, ".local", "bin", "cursor-agent");
writeFakeBinary(binary, NOOP_SCRIPT);
assert.equal(resolveCursorAgentBinary(), binary);
assert.equal(resolveCursorAgentBinary({ allowPathFallback: true }), binary);
});
it("a fixed-path match wins over a PATH-only decoy, regardless of allowPathFallback", () => {
const fixedBinary = path.join(tmpHome, ".local", "bin", "cursor-agent");
writeFakeBinary(fixedBinary, NOOP_SCRIPT);
const pathDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-decoy-path-"));
const decoyBinary = path.join(pathDir, "cursor-agent");
writeFakeBinary(decoyBinary, NOOP_SCRIPT);
process.env.PATH = `${pathDir}${path.delimiter}${ORIGINAL_PATH ?? ""}`;
try {
assert.equal(resolveCursorAgentBinary({ allowPathFallback: false }), fixedBinary);
assert.equal(resolveCursorAgentBinary({ allowPathFallback: true }), fixedBinary);
} finally {
fs.rmSync(pathDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});
it("allowPathFallback:true finds a PATH-only binary when no fixed candidate matches", () => {
const ambient = ambientFixedCursorAgentPath();
if (ambient) {
// The HOME-relative candidate is clean (fresh tmp dir), but an ambient
// real install at one of the OTHER 4 hardcoded absolute paths would be
// found first regardless of PATH — not hermetically testable here.
console.log(
`SKIP: ambient cursor-agent install detected at ${ambient} (see file-level note above)`
);
return;
}
const pathDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-real-path-only-"));
const pathOnlyBinary = path.join(pathDir, "cursor-agent");
writeFakeBinary(pathOnlyBinary, NOOP_SCRIPT);
process.env.PATH = `${pathDir}${path.delimiter}${ORIGINAL_PATH ?? ""}`;
try {
assert.equal(resolveCursorAgentBinary({ allowPathFallback: true }), pathOnlyBinary);
assert.equal(resolveCursorAgentBinary(), pathOnlyBinary);
} finally {
fs.rmSync(pathDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});
const ambient = ambientFixedCursorAgentPath();
it(
"allowPathFallback:false returns null when only a PATH-resolvable binary exists (no fixed-path match)",
{
skip: ambient
? `ambient cursor-agent install detected at ${ambient} — resolveCursorAgentBinary has no ` +
"DI seam for its other hardcoded absolute candidates, so this host can never observe a " +
'true "nothing fixed matches" state; passes in a clean CI container without cursor-agent installed'
: false,
},
() => {
const pathDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-path-only-no-fallback-"));
const pathOnlyBinary = path.join(pathDir, "cursor-agent");
writeFakeBinary(pathOnlyBinary, NOOP_SCRIPT);
process.env.PATH = `${pathDir}${path.delimiter}${ORIGINAL_PATH ?? ""}`;
try {
assert.equal(resolveCursorAgentBinary({ allowPathFallback: false }), null);
} finally {
fs.rmSync(pathDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
}
);
it(
"returns null when nothing matches and allowPathFallback is false",
{
skip: ambient
? `ambient cursor-agent install detected at ${ambient} — see note above`
: false,
},
() => {
assert.equal(resolveCursorAgentBinary({ allowPathFallback: false }), null);
}
);
it(
"finds the real /opt/homebrew/bin/cursor-agent candidate on hosts that have it installed there, without a PATH scan",
{
skip:
fs.existsSync("/opt/homebrew/bin/cursor-agent") &&
!fs.existsSync("/root/.local/bin/cursor-agent") &&
!fs.existsSync("/usr/local/bin/cursor-agent") &&
!fs.existsSync("/usr/bin/cursor-agent")
? false
: "no ambient /opt/homebrew/bin/cursor-agent (or an earlier fixed candidate shadows it) on this host",
},
() => {
// Read-only, non-destructive: asserts against whatever is already
// installed on this host (e.g. via `brew install --cask cursor-cli`) —
// never creates/modifies anything at this real system path.
process.env.PATH = "";
assert.equal(
resolveCursorAgentBinary({ allowPathFallback: false }),
"/opt/homebrew/bin/cursor-agent"
);
}
);
});
describe("runCursorAgent — sigkillFollowupMs (Task 2 Step 1/2 unattended hardening)", () => {
let tmpDir: string;
let binary: string;
const HANG_IGNORE_SIGTERM_SCRIPT = `#!/usr/bin/env node
process.on("SIGTERM", () => {});
const selfExitMs = process.env.FAKE_BIN_SELF_EXIT_MS;
if (selfExitMs) {
setTimeout(() => process.exit(0), Number(selfExitMs));
} else {
setInterval(() => {}, 60000);
}
`;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-run-cursor-agent-"));
binary = path.join(tmpDir, "fake-cursor-agent");
writeFakeBinary(binary, HANG_IGNORE_SIGTERM_SCRIPT);
});
afterEach(() => {
delete process.env.FAKE_BIN_SELF_EXIT_MS;
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
it("sends SIGKILL after the follow-up window when the process ignores SIGTERM", async () => {
// A freshly spawned node process (via the #!/usr/bin/env node shebang) needs
// real wall-clock time to start up and register its SIGTERM handler before
// the handler can take effect — empirically, 150-200ms was flaky in this
// sandboxed environment (SIGTERM won the race and killed the process before
// the handler was installed). 600ms/200ms gives a comfortable margin.
const start = Date.now();
const result = await runCursorAgent(binary, [], 600, { sigkillFollowupMs: 200 });
const elapsed = Date.now() - start;
assert.equal(result.signal, "SIGKILL");
assert.ok(
elapsed >= 600,
`SIGKILL cannot fire before the SIGTERM timeout (600ms), got ${elapsed}ms`
);
assert.ok(elapsed < 10_000, `expected the SIGKILL follow-up well under 10s, got ${elapsed}ms`);
});
it("does NOT force-kill when sigkillFollowupMs is omitted (byte-identical to the existing fetchCursorAgentModels caller)", async () => {
process.env.FAKE_BIN_SELF_EXIT_MS = "1000";
const start = Date.now();
const result = await runCursorAgent(binary, [], 600);
const elapsed = Date.now() - start;
// No sigkillFollowupMs -> SIGTERM at 600ms is ignored, process exits on
// its own at ~1000ms via process.exit(0) — proves no automatic SIGKILL
// follow-up fired (that would have resolved near 600ms with signal
// "SIGKILL" instead).
assert.equal(result.signal, null);
assert.equal(result.code, 0);
assert.ok(
elapsed >= 950,
`expected the process to exit on its own near 1000ms, got ${elapsed}ms`
);
});
});