Files
OmniRoute/tests/unit/cli-expanded-commands.test.ts
Diego Rodrigues de Sa e Souza 93265eede3 test(infra): retry recursive temp-dir removal on main (main twin of #11968) (#12246)
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)

`main` has been red since b342c1a361 on the vitest and integration gates:

  ✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
  ✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos

Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).

release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.

This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.

The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.

* chore(scripts): carry the rm-maxretries codemod onto main alongside its output

The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
2026-09-01 01:48:00 -03:00

359 lines
15 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
test("logs.mjs pode ser importado com novas flags", async () => {
const mod = await import("../../bin/cli/commands/logs.mjs");
assert.equal(typeof mod.registerLogs, "function");
assert.equal(typeof mod.runLogsCommand, "function");
});
test("health.mjs exporta runHealthComponentsCommand", async () => {
const mod = await import("../../bin/cli/commands/health.mjs");
assert.equal(typeof mod.registerHealth, "function");
assert.equal(typeof mod.runHealthCommand, "function");
assert.equal(typeof mod.runHealthComponentsCommand, "function");
});
test("update.mjs exporta runUpdateCommand", async () => {
const mod = await import("../../bin/cli/commands/update.mjs");
assert.equal(typeof mod.registerUpdate, "function");
assert.equal(typeof mod.runUpdateCommand, "function");
});
test("keys.mjs exporta novos comandos", async () => {
const mod = await import("../../bin/cli/commands/keys.mjs");
assert.equal(typeof mod.registerKeys, "function");
assert.equal(typeof mod.runKeysRegenerateCommand, "function");
assert.equal(typeof mod.runKeysRevokeCommand, "function");
assert.equal(typeof mod.runKeysRevealCommand, "function");
assert.equal(typeof mod.runKeysUsageCommand, "function");
assert.equal(typeof mod.runKeysPolicyShowCommand, "function");
assert.equal(typeof mod.runKeysPolicySetCommand, "function");
assert.equal(typeof mod.runKeysExpirationListCommand, "function");
assert.equal(typeof mod.runKeysRotateCommand, "function");
assert.equal(typeof mod.runKeysListCommand, "function");
assert.equal(typeof mod.runKeysRemoveCommand, "function");
});
test("keys.mjs — runKeysListCommand sem server retorna 0 ou 1", async () => {
const { runKeysListCommand } = await import("../../bin/cli/commands/keys.mjs");
let code = 0;
try {
code = await runKeysListCommand({ json: true });
} catch {
code = 1;
}
assert.ok(code === 0 || code === 1);
});
test("provider-store.mjs exporta removeProviderConnectionByProvider", async () => {
const mod = await import("../../bin/cli/provider-store.mjs");
assert.equal(typeof mod.removeProviderConnectionByProvider, "function");
});
test("tunnel.mjs exporta subcomandos status/logs/info/rotate", async () => {
const mod = await import("../../bin/cli/commands/tunnel.mjs");
assert.equal(typeof mod.registerTunnel, "function");
assert.equal(typeof mod.runTunnelStatusCommand, "function");
assert.equal(typeof mod.runTunnelLogsCommand, "function");
assert.equal(typeof mod.runTunnelInfoCommand, "function");
assert.equal(typeof mod.runTunnelRotateCommand, "function");
});
test("backup.mjs exporta runBackupAutoEnableCommand/Disable/Status", async () => {
const mod = await import("../../bin/cli/commands/backup.mjs");
assert.equal(typeof mod.registerBackup, "function");
assert.equal(typeof mod.runBackupCommand, "function");
assert.equal(typeof mod.runBackupAutoEnableCommand, "function");
assert.equal(typeof mod.runBackupAutoDisableCommand, "function");
assert.equal(typeof mod.runBackupAutoStatusCommand, "function");
});
test("backup auto status sem arquivo retorna 0", async () => {
const { runBackupAutoStatusCommand } = await import("../../bin/cli/commands/backup.mjs");
const { tmpdir } = await import("node:os");
const orig = process.env.HOME;
process.env.HOME = tmpdir();
try {
const code = await runBackupAutoStatusCommand();
assert.ok(code === 0 || code === 1);
} finally {
process.env.HOME = orig;
}
});
// Regressão #8512: `backup` re-declara os mesmos nomes de opção que `create`/
// `auto enable` no fallback legacy ("omniroute backup" sem subcomando) — o
// Commander resolve a opção no ancestral mais próximo que a declara, então o
// valor do subcomando é descartado silenciosamente e substituído pelo default
// da própria opção do subcomando (null/false/[]).
test("backup auto enable — nenhuma opção é sombreada pelo parent backup", async () => {
const { registerBackup } = await import("../../bin/cli/commands/backup.mjs");
const { Command } = await import("commander");
const { mkdtempSync, readFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const dataDir = mkdtempSync(join(tmpdir(), "omniroute-backup-test-"));
const origDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = dataDir;
try {
const prog = new Command().exitOverride();
registerBackup(prog);
await prog.parseAsync(
[
"node",
"x",
"backup",
"auto",
"enable",
"--cron",
"0 4 * * *",
"--cloud",
"--encrypt",
"--retention",
"7",
],
{ from: "node" }
);
const schedule = JSON.parse(readFileSync(join(dataDir, "backup-schedule.json"), "utf8"));
assert.equal(schedule.cron, "0 4 * * *");
assert.equal(schedule.cloud, true, "--cloud não deve ser sombreado pelo parent backup");
assert.equal(schedule.encrypt, true, "--encrypt não deve ser sombreado pelo parent backup");
assert.equal(schedule.retention, 7, "--retention não deve ser sombreado pelo parent backup");
} finally {
if (origDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = origDataDir;
rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});
test("backup create — nenhuma opção é sombreada pelo parent backup", async () => {
const { registerBackup } = await import("../../bin/cli/commands/backup.mjs");
const { Command } = await import("commander");
const prog = new Command().exitOverride();
registerBackup(prog);
const backupCmd = prog.commands.find((c) => c.name() === "backup");
const createCmd = backupCmd.commands.find((c) => c.name() === "create");
let capturedOpts = null;
createCmd.action((opts) => {
capturedOpts = opts;
});
await prog.parseAsync(
[
"node",
"x",
"backup",
"create",
"--name",
"foo",
"--cloud",
"--encrypt",
"--retention",
"7",
"--exclude",
"*.log",
],
{ from: "node" }
);
assert.ok(capturedOpts, "action deve ter sido chamada");
assert.equal(capturedOpts.name, "foo", "--name não deve ser sombreado");
assert.equal(capturedOpts.cloud, true, "--cloud não deve ser sombreado");
assert.equal(capturedOpts.encrypt, true, "--encrypt não deve ser sombreado");
assert.equal(capturedOpts.retention, 7, "--retention não deve ser sombreado");
assert.deepEqual(capturedOpts.exclude, ["*.log"], "--exclude não deve ser sombreado");
});
// Regressão de documentação: `omniroute backup` sem subcomando é o uso
// canônico documentado em USER_GUIDE.md / CLI-TOOLS.md / AGENT-SKILLS.md
// ("omniroute backup # Snapshot config + DB"). Remover só as opções
// duplicadas do parent (causa real do #8512) não pode remover essa ação.
test("backup — sem subcomando ainda cria um backup (uso legado documentado)", async () => {
const { registerBackup } = await import("../../bin/cli/commands/backup.mjs");
const { Command } = await import("commander");
const { mkdtempSync, existsSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const dataDir = mkdtempSync(join(tmpdir(), "omniroute-backup-bare-test-"));
const origDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = dataDir;
try {
const prog = new Command().exitOverride();
registerBackup(prog);
await prog.parseAsync(["node", "x", "backup"], { from: "node" });
assert.ok(
existsSync(join(dataDir, "backups")),
"omniroute backup deve criar o diretório de backups"
);
} finally {
if (origDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = origDataDir;
rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});
test("tunnel — registerTunnel registra list/create/stop/status/logs/info/rotate", async () => {
const { registerTunnel } = await import("../../bin/cli/commands/tunnel.mjs");
const { Command } = await import("commander");
const prog = new Command().exitOverride();
registerTunnel(prog);
const tunnelCmd = prog.commands.find((c) => c.name() === "tunnel");
assert.ok(tunnelCmd, "tunnel command deve existir");
const names = tunnelCmd.commands.map((c) => c.name());
for (const sub of ["list", "create", "stop", "status", "logs", "info", "rotate"]) {
assert.ok(names.includes(sub), `tunnel ${sub} deve existir`);
}
});
test("health components com alertsOnly=true não lança", async () => {
// Server não está rodando — função deve retornar 1 sem throw.
const { runHealthComponentsCommand } = await import("../../bin/cli/commands/health.mjs");
const code = await runHealthComponentsCommand({ alertsOnly: true });
assert.ok(code === 0 || code === 1, "should return 0 or 1");
});
test("update --check com versão atual não lança", async () => {
const { runUpdateCommand } = await import("../../bin/cli/commands/update.mjs");
// Sem servidor npm disponível pode retornar erro — só garante que não throw.
let code = 0;
try {
code = await runUpdateCommand({ check: true, yes: true });
} catch {
code = 1;
}
assert.ok(code === 0 || code === 1);
});
test("test-provider.mjs exporta runTestProviderCommand com novas flags", async () => {
const mod = await import("../../bin/cli/commands/test-provider.mjs");
assert.equal(typeof mod.registerTestProvider, "function");
assert.equal(typeof mod.runTestProviderCommand, "function");
});
test("test-provider — registerTestProvider registra flags latency/repeat/compare/save", async () => {
const { registerTestProvider } = await import("../../bin/cli/commands/test-provider.mjs");
const { Command } = await import("commander");
const prog = new Command().exitOverride();
registerTestProvider(prog);
const testCmd = prog.commands.find((c) => c.name() === "test");
assert.ok(testCmd, "test command deve existir");
const optNames = testCmd.options.map((o: any) => o.long);
assert.ok(optNames.includes("--latency"), "--latency deve existir");
assert.ok(optNames.includes("--repeat"), "--repeat deve existir");
assert.ok(optNames.includes("--compare"), "--compare deve existir");
assert.ok(optNames.includes("--save"), "--save deve existir");
});
test("OAuthFlow.jsx — arquivo existe e exporta startOAuthTui", async () => {
const { readFileSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
const path = new URL("../../bin/cli/tui/OAuthFlow.jsx", import.meta.url);
const src = readFileSync(fileURLToPath(path), "utf8");
assert.ok(
src.includes("export async function startOAuthTui"),
"startOAuthTui deve ser exportada"
);
});
test("EvalWatch.jsx — arquivo existe e exporta startEvalWatchTui", async () => {
const { readFileSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
const path = new URL("../../bin/cli/tui/EvalWatch.jsx", import.meta.url);
const src = readFileSync(fileURLToPath(path), "utf8");
assert.ok(
src.includes("export async function startEvalWatchTui"),
"startEvalWatchTui deve ser exportada"
);
});
test("ProvidersTestAll.jsx — arquivo existe e exporta startProvidersTestTui", async () => {
const { readFileSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
const path = new URL("../../bin/cli/tui/ProvidersTestAll.jsx", import.meta.url);
const src = readFileSync(fileURLToPath(path), "utf8");
assert.ok(
src.includes("export async function startProvidersTestTui"),
"startProvidersTestTui deve ser exportada"
);
});
test("backup matchesGlob — comportamento correto", async () => {
// Teste via leitura de arquivo para evitar efeitos colaterais de importação
const { readFileSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
const src = readFileSync(
fileURLToPath(new URL("../../bin/cli/commands/backup.mjs", import.meta.url)),
"utf8"
);
// Garante que a função usa lógica correta (sem startsWith na branch sem *)
assert.ok(src.includes("return fileName === pattern;"), "non-glob deve usar igualdade exata");
assert.ok(!src.includes("fileName.startsWith(pattern)"), "não deve usar startsWith no non-glob");
// Garante que suporta multi-wildcard (sem early return parts.length !== 2)
assert.ok(!src.includes("parts.length !== 2"), "não deve bloquear multi-wildcard");
});
test("test-provider — compare requer pelo menos dois modelos sem server retorna 0 ou 1", async () => {
const { runTestProviderCommand } = await import("../../bin/cli/commands/test-provider.mjs");
let code = 0;
try {
code = await runTestProviderCommand("anthropic", undefined, {
compare: "gpt-4o,claude-3-5-sonnet",
});
} catch {
code = 1;
}
assert.ok(code === 0 || code === 1);
});
test("test-provider --all-providers consumes the connections envelope", async () => {
const origFetch = globalThis.fetch;
const connections = [
{ id: "conn1", provider: "anthropic", defaultModel: "claude", authType: "apikey" },
{ id: "conn2", provider: "gemini", defaultModel: "gemini", authType: "oauth" },
];
const requests: string[] = [];
globalThis.fetch = ((url: string) => {
requests.push(url);
if (url.includes("/api/health")) return Promise.resolve(new Response("{}", { status: 200 }));
if (url.includes("/api/providers?limit=200")) {
return Promise.resolve(new Response(JSON.stringify({ connections }), { status: 200 }));
}
if (url.includes("/api/providers/") && url.includes("/test")) {
return Promise.resolve(new Response(JSON.stringify({ valid: true }), { status: 200 }));
}
throw new Error(`unexpected URL: ${url}`);
}) as typeof fetch;
try {
const { runTestProviderCommand } = await import("../../bin/cli/commands/test-provider.mjs");
const output: string[] = [];
const origWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string | Uint8Array) => {
if (typeof chunk === "string") output.push(chunk);
return true;
}) as typeof process.stdout.write;
try {
const code = await runTestProviderCommand(undefined, undefined, {
allProviders: true,
json: true,
});
assert.equal(code, 0);
} finally {
process.stdout.write = origWrite;
}
assert.ok(requests.some((url) => url.includes("/api/providers?limit=200")));
const parsed = JSON.parse(output.join(""));
assert.deepEqual(
parsed.map(({ provider, model }: { provider: string; model: string }) => ({ provider, model })),
[
{ provider: "anthropic", model: "claude" },
{ provider: "gemini", model: "gemini" },
],
);
assert.ok(parsed.every(({ success }: { success: boolean }) => success));
} finally {
globalThis.fetch = origFetch;
}
});