mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
* fix(cli): stabilize setup-claude.test.ts flake — inject dry-run log sink (#5959) Root cause (isolated empirically, 5/10 fail on the pristine base): the dry-run path of syncClaudeProfilesFromModels console.log's a multi-byte box-drawing heading ("── [dry-run] … ──"). Under the node:test runner that write lands on the test child's stdout and corrupts the runner's V8-serialized event stream ~50% of the time ("Unable to deserialize cloned data due to invalid or unsupported version"), killing the file at the first logging test. ASCII-only logging never reproduced it (0/20); the unicode heading alone reproduced it (10/20). Fix: syncClaudeProfilesFromModels accepts an injectable log sink (opts.log, CLI default unchanged: console.log). The dry-run test injects a collector — keeping unicode off the child's stdout — and gains assertions on the dry-run report (path + parsed settings content), which FAIL on the old code (log ignored) and PASS on the new one. Validation: 0/30 failures post-fix vs 5/10 pre-fix on the same tree. Baselines: complexity 2003->2006 and cognitive 859->860 are inherited post-3a3d618fe release drift — measured identical on the pristine base with and without this change (notes added in both files). * test(ci): collect the orphaned tests/unit/executors/ directory (base-red unblock) #5800 created tests/unit/executors/ outside every unit-runner brace glob, so its 2 test files (firecrawl-fetch, xai-executor) never ran anywhere and check:test-discovery flags them as NEW orphans on the pristine base, red-flagging every PR into release/v3.8.44. Added 'executors' to the runner globs in package.json (7 scripts), ci.yml unit shards, quality.yml TIA glob, build-test-impact-map.mjs, and the test-discovery gate's COLLECTORS (the gate enforces those stay in sync). Both files pass when actually collected (10/10); cli+executors under suite flags: 99/99. * chore(quality): complexity baseline 2006 -> 2007 (CI-observed value) The GitHub fast-gates runner measures 2007 where local measures 2006 — the same local-vs-CI off-by-one documented in the 2026-06-26 note. Pin the CI-observed value so the gate is deterministic where it runs. * fix(i18n): add the 6 missing en.json keys flagged by settings-i18n-keys (base-red unblock) providers.iconUrlLabel/iconUrlHint (referenced by AddCompatibleProviderModal and EditCompatibleNodeModal) and settings.authz.cors.wildcard.title/desc (the #5602 CORS_ALLOW_ALL banner in AuthzSection) shipped without their en.json messages — 'direct translation calls have English messages' fails on the pristine release tip, red-flagging every PR. git log -S proves the keys never existed (not a merge-eat). Scanner test: 10/10 green.
156 lines
7.1 KiB
TypeScript
156 lines
7.1 KiB
TypeScript
import { test, before, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
buildProfileSettings,
|
|
syncClaudeProfilesFromModels,
|
|
} from "../../../bin/cli/commands/setup-claude.mjs";
|
|
import { buildClaudeEnv, resolveLaunchTarget } from "../../../bin/cli/commands/launch.mjs";
|
|
import { categoriseModel } from "../../../bin/cli/commands/setup-codex.mjs";
|
|
|
|
// #5959 — deflake: `node --test` runs each file in a child process that streams
|
|
// its report back to the parent as V8-serialized frames on fd 1 (stdout). The CLI
|
|
// helpers under test (`syncClaudeProfilesFromModels`) print progress via
|
|
// `console.log`, and that stdout output interleaves with the serialized frames,
|
|
// corrupting the stream — the parent then throws
|
|
// "Unable to deserialize cloned data due to invalid or unsupported version" at
|
|
// file teardown ~50% of runs (all subtests pass; only the file errors). No test
|
|
// here asserts on stdout, so silence the stdout-writing console methods for the
|
|
// duration of this file. Restored in `after` for good hygiene.
|
|
const _console = { log: console.log, info: console.info, warn: console.warn };
|
|
before(() => {
|
|
console.log = () => {};
|
|
console.info = () => {};
|
|
console.warn = () => {};
|
|
});
|
|
after(() => {
|
|
console.log = _console.log;
|
|
console.info = _console.info;
|
|
console.warn = _console.warn;
|
|
});
|
|
|
|
// ── setup-claude profile generation ──────────────────────────────────────────
|
|
|
|
test("buildProfileSettings pins the model + base URL + gateway discovery", () => {
|
|
const cfg = categoriseModel("glm/glm-5.2"); // thinking → effort xhigh
|
|
const json = JSON.parse(buildProfileSettings("glm/glm-5.2", "http://vps:20128", cfg));
|
|
assert.equal(json.model, "glm/glm-5.2");
|
|
assert.equal(json.env.ANTHROPIC_BASE_URL, "http://vps:20128");
|
|
assert.equal(json.env.ANTHROPIC_MODEL, "glm/glm-5.2");
|
|
assert.equal(json.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY, "1");
|
|
assert.equal(json.effortLevel, "xhigh");
|
|
});
|
|
|
|
test("buildProfileSettings NEVER writes the auth token to disk", () => {
|
|
const cfg = categoriseModel("kmc/kimi-k2.7");
|
|
const raw = buildProfileSettings("kmc/kimi-k2.7", "http://vps:20128", cfg);
|
|
assert.equal(raw.includes("ANTHROPIC_AUTH_TOKEN"), false);
|
|
assert.equal(raw.includes("ANTHROPIC_API_KEY"), false);
|
|
});
|
|
|
|
test("buildProfileSettings omits effortLevel for the simple tier", () => {
|
|
const cfg = categoriseModel("ollamacloud/gemma4:31b"); // simple → no effort
|
|
const json = JSON.parse(buildProfileSettings("ollamacloud/gemma4:31b", "http://x:20128", cfg));
|
|
assert.equal("effortLevel" in json, false);
|
|
});
|
|
|
|
test("profile names match setup-codex (cross-CLI consistency)", () => {
|
|
assert.equal(categoriseModel("glm/glm-5.2").name, "glm52");
|
|
assert.equal(categoriseModel("kmc/kimi-k2.7").name, "kimi-k27");
|
|
});
|
|
|
|
test("syncClaudeProfilesFromModels writes directory-per-profile settings + threads baseUrl, skips non-ids", async () => {
|
|
const claudeHome = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-claude-profiles-"));
|
|
try {
|
|
const result = await syncClaudeProfilesFromModels([{ id: "glm/glm-5.2" }, { id: "" }], {
|
|
claudeHome,
|
|
baseUrl: "http://vps:20128",
|
|
});
|
|
|
|
assert.equal(result.written, 1);
|
|
assert.equal(result.skipped, 1);
|
|
assert.deepEqual(
|
|
result.profiles.map((p) => p.name),
|
|
["glm52"]
|
|
);
|
|
|
|
// Directory-per-profile: <claudeHome>/profiles/<name>/settings.json
|
|
const settingsPath = path.join(claudeHome, "profiles", "glm52", "settings.json");
|
|
const json = JSON.parse(await fs.readFile(settingsPath, "utf8"));
|
|
assert.equal(json.model, "glm/glm-5.2");
|
|
assert.equal(json.env.ANTHROPIC_BASE_URL, "http://vps:20128");
|
|
assert.equal(json.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY, "1");
|
|
// The auth token must never be written to disk.
|
|
assert.equal(JSON.stringify(json).includes("ANTHROPIC_AUTH_TOKEN"), false);
|
|
} finally {
|
|
await fs.rm(claudeHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("syncClaudeProfilesFromModels dry-run writes nothing and reports via the injected log (#5959)", async () => {
|
|
const claudeHome = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-claude-dry-"));
|
|
// The collector keeps the dry-run's multi-byte "──" heading OFF this child
|
|
// process's stdout: under the node:test runner that write corrupts the V8
|
|
// serialization stream ~50% of runs (#5959, "Unable to deserialize cloned
|
|
// data due to invalid or unsupported version").
|
|
const lines: string[] = [];
|
|
try {
|
|
const result = await syncClaudeProfilesFromModels([{ id: "glm/glm-5.2" }], {
|
|
claudeHome,
|
|
baseUrl: "http://vps:20128",
|
|
dryRun: true,
|
|
log: (line: string) => lines.push(line),
|
|
});
|
|
assert.equal(result.written, 1);
|
|
// Dry-run reports the would-be file + its content through the log sink…
|
|
assert.equal(lines.length, 2);
|
|
const settingsPath = path.join(claudeHome, "profiles", "glm52", "settings.json");
|
|
assert.ok(lines[0].includes(settingsPath));
|
|
const printed = JSON.parse(lines[1]);
|
|
assert.equal(printed.model, "glm/glm-5.2");
|
|
assert.equal(printed.env.ANTHROPIC_BASE_URL, "http://vps:20128");
|
|
// …and writes nothing to disk.
|
|
await assert.rejects(fs.stat(settingsPath), /ENOENT/);
|
|
} finally {
|
|
await fs.rm(claudeHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ── launch env (Claude Code) ─────────────────────────────────────────────────
|
|
|
|
test("buildClaudeEnv still accepts a bare port (backward compatible)", () => {
|
|
const env = buildClaudeEnv({ PATH: "/bin" }, 20128, "secret");
|
|
assert.equal(env.ANTHROPIC_BASE_URL, "http://localhost:20128");
|
|
assert.equal(env.ANTHROPIC_AUTH_TOKEN, "secret");
|
|
assert.equal(env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY, "1");
|
|
});
|
|
|
|
test("buildClaudeEnv accepts a full base URL and strips /v1", () => {
|
|
const env = buildClaudeEnv({}, "https://vps.example.com:20128/v1", "t");
|
|
assert.equal(env.ANTHROPIC_BASE_URL, "https://vps.example.com:20128");
|
|
});
|
|
|
|
test("buildClaudeEnv sets CLAUDE_CONFIG_DIR for a profile", () => {
|
|
const env = buildClaudeEnv({}, 20128, "t", { configDir: "/home/u/.claude/profiles/glm52" });
|
|
assert.equal(env.CLAUDE_CONFIG_DIR, "/home/u/.claude/profiles/glm52");
|
|
});
|
|
|
|
test("buildClaudeEnv strips inherited ANTHROPIC_* and does not mutate input", () => {
|
|
const input = { ANTHROPIC_API_KEY: "leak", PATH: "/bin" };
|
|
const env = buildClaudeEnv(input, 20128, "x");
|
|
assert.equal(env.ANTHROPIC_API_KEY, undefined);
|
|
assert.equal(input.ANTHROPIC_API_KEY, "leak");
|
|
});
|
|
|
|
test("resolveLaunchTarget: explicit --remote wins, strips /v1", () => {
|
|
const { baseUrl } = resolveLaunchTarget({ remote: "https://vps:20128/v1" });
|
|
assert.equal(baseUrl, "https://vps:20128");
|
|
});
|
|
|
|
test("resolveLaunchTarget: explicit token wins over everything", () => {
|
|
const { authToken } = resolveLaunchTarget({ remote: "http://x:20128", token: "tok-explicit" });
|
|
assert.equal(authToken, "tok-explicit");
|
|
});
|