mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
#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>
122 lines
4.0 KiB
TypeScript
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");
|
|
});
|