Files
OmniRoute/tests/unit/cursor-agent-models.test.ts
Will Gordon 6ff2eceb9e feat(cursor): adds cursor-agent-backed token renewal orchestrator
Builds the renewal orchestrator in src/lib/cursor/renewal.ts: a
bounded, unattended-safe --list-models nudge, a side-effect-free
status availability check, an in-flight spawn lock keyed by
command, and renewCursorConnection() which nudges cursor-agent
then independently re-scrapes the IDE and cursor-agent credential
sources to detect whichever refreshed. Extends cursorAgent.ts's
binary resolution and spawn helper with fixed-paths-only mode and
a SIGKILL follow-up for background use. Adds a generic keyed-mutex
utility (src/shared/utils/keyedMutex.ts) for serializing a
connection's renew-then-persist cycle, and forwards a busy-timeout
through driverFactory's node:sqlite fallback path.
2026-08-04 11:43:51 -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 });
});
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 });
}
});
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 });
}
});
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 });
}
}
);
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 });
});
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`
);
});
});