Files
OmniRoute/tests/unit/cli-runtime-locate-command-timeout-10710.test.ts
Diego Rodrigues de Sa e Souza 4d92dfe0a2 fix: distinguish CLI-probe timeouts from not_found, resolve Hermes Agent keyId server-side (#10710+10711) (#10746)
#10710: locateCommand() in cliRuntime.ts collapsed a genuine probe timeout
(runProcess's timedOut flag) into the same reason:"not_found" as a truly
absent binary, on both the where.exe and `command -v` branches. Give
timeouts a distinct "timeout" reason, keep trying remaining command
candidates in locateCommandCandidate instead of treating a timeout as
terminal, and extend the settings-file fallback (cliInstallFallback.ts) to
also cover the new "timeout" reason, matching the scenario it already
existed for.

#10711: the Hermes Agent dashboard "Apply" flow only ever sends `keyId`
(never a raw `apiKey`), but the hermes-agent-settings POST handler never
resolved it, so generateHermesAgentConfig() always fell through to the
literal placeholder "YOUR_OMNIROUTE_API_KEY_HERE" for
providers.omniroute.api_key, delegation.api_key, and every
auxiliary.*.api_key. Resolve keyId server-side via getApiKeyById(), the
same precedented pattern already used by claude-settings/route.ts and
codex-settings/route.ts.

Bug 2 from #10710 (hermes tool-detector configPath) was already fixed by
commit 0a74bfbdea -- confirmed still intact,
no action needed.

Co-authored-by: Markus Hartung <mail@hartmark.se>
2026-08-19 11:08:34 -03:00

122 lines
4.0 KiB
TypeScript

/**
* Regression test for #10710.
*
* locateCommand() (src/shared/services/cliRuntime.ts) used to collapse a
* genuine probe timeout (runProcess's internal 3s timer SIGKILLing the
* child) into the exact same reason:"not_found" as a truly-absent binary.
* That made an installed CLI whose where.exe/`command -v` probe was starved
* (e.g. under the all-statuses route's wide, undeduped ~30-tool fan-out)
* indistinguishable from one that was never installed at all.
*/
import test from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { pathToFileURL } from "node:url";
import { EventEmitter } from "node:events";
const require = createRequire(import.meta.url);
const childProcess = require("node:child_process");
const modulePath = path.join(process.cwd(), "src/shared/services/cliRuntime.ts");
const originalSpawn = childProcess.spawn;
async function importFresh(label: string) {
return import(`${pathToFileURL(modulePath).href}?case=${label}-${Date.now()}-${Math.random()}`);
}
type FakeChildProcess = EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
kill: (signal?: string) => boolean;
};
function installTimeoutSpawn() {
childProcess.spawn = () => {
const child = new EventEmitter() as FakeChildProcess;
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
// Model a real SIGKILL: the OS delivers 'close' shortly after the kill,
// not synchronously -- but not before the internal 3s timeout fires.
child.kill = () => {
setImmediate(() => child.emit("close", null));
return true;
};
return child;
};
syncBuiltinESMExports();
}
test.afterEach(() => {
childProcess.spawn = originalSpawn;
syncBuiltinESMExports();
});
test("#10710: locateCommand surfaces a distinct 'timeout' reason, not 'not_found'", async () => {
installTimeoutSpawn();
const cliRuntime = await importFresh("locate-timeout");
const start = Date.now();
const result = await cliRuntime.locateCommand("codex", { PATH: process.env.PATH });
const elapsedMs = Date.now() - start;
assert.ok(elapsedMs >= 2900, `expected the internal 3s timeout to fire, took ${elapsedMs}ms`);
assert.equal(result.installed, false);
assert.equal(
result.reason,
"timeout",
"a probe timeout must not be relabeled as not_found -- indistinguishable from a genuinely absent binary"
);
});
test("#10710: a timed-out first candidate does not hide a second candidate that resolves", async () => {
let call = 0;
childProcess.spawn = (_command: string) => {
call += 1;
const child = new EventEmitter() as FakeChildProcess;
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
if (call === 1) {
// First candidate (e.g. "codex-preview") times out.
child.kill = () => {
setImmediate(() => child.emit("close", null));
return true;
};
} else {
// Second candidate (e.g. "codex") resolves quickly and successfully.
child.kill = () => true;
setImmediate(() => {
child.stdout.emit("data", Buffer.from("/usr/local/bin/codex\n"));
child.emit("close", 0);
});
}
return child;
};
syncBuiltinESMExports();
const cliRuntime = await importFresh("locate-candidate-fallthrough");
const result = await cliRuntime.locateCommandCandidate(
["codex-preview", "codex"],
{ PATH: process.env.PATH },
undefined
);
assert.equal(result.installed, true, "the second, quickly-resolving candidate must still be found");
assert.equal(result.command, "codex");
});
test("#10710: when every candidate times out, the caller sees 'timeout' rather than 'not_found'", async () => {
installTimeoutSpawn();
const cliRuntime = await importFresh("locate-candidate-all-timeout");
const result = await cliRuntime.locateCommandCandidate(
["codex-preview", "codex"],
{ PATH: process.env.PATH },
undefined
);
assert.equal(result.installed, false);
assert.equal(result.reason, "timeout");
});