mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-05-23 04:28:06 +00:00
- bin/cli/CONVENTIONS.md: fonte normativa de flags, exit codes, output,
retry/backoff, i18n, secrets, auditoria de ações destrutivas
- bin/cli/api.mjs: apiFetch() com retry/backoff, Retry-After, ApiError,
statusToExitCode, isServerUp; computeBackoff/shouldRetryStatus exportados
- bin/cli/runtime.mjs: withRuntime/withHttp/withDb — server-first / DB-fallback;
ServerOfflineError com exitCode 3
- bin/cli/i18n.mjs: t() com Map achatado (sem bracket em prototype), interpolação
{vars}, setLocale/detectLocale/resetForTests; hardened contra __proto__ traversal
- bin/cli/output.mjs: emit() (table/json/jsonl/csv), EXIT_CODES, maskSecret,
printSuccess/printError/printWarning/exitWith; output → stdout, diagnóstico → stderr
- bin/cli/locales/en.json + pt-BR.json: strings base (setup/doctor/providers/
keys/combo/serve/backup/update/health/mcp/tunnel)
- bin/cli/README.md: mapa da estrutura e guia de uso dos helpers
- tests/unit/cli-exit-codes.test.ts: 10 casos — EXIT_CODES, statusToExitCode,
backoff exponencial, jitter ±25%, t() i18n com pt-BR e anti-__proto__
- .env.example + docs/reference/ENVIRONMENT.md: documentar 4 novas env vars CLI
(OMNIROUTE_LANG, OMNIROUTE_CLI_TOKEN, OMNIROUTE_HTTP_TIMEOUT_MS, OMNIROUTE_VERBOSE)
- scripts/check/check-env-doc-sync.mjs: adicionar LC_MESSAGES ao allowlist de sistema
110 lines
4.8 KiB
TypeScript
110 lines
4.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { EXIT_CODES } from "../../bin/cli/output.mjs";
|
|
import { statusToExitCode, computeBackoff, RETRY_DEFAULTS } from "../../bin/cli/api.mjs";
|
|
import { t, resetForTests, setLocale } from "../../bin/cli/i18n.mjs";
|
|
|
|
// ─── exit code constants ──────────────────────────────────────────────────────
|
|
|
|
test("EXIT_CODES has expected values", () => {
|
|
assert.equal(EXIT_CODES.SUCCESS, 0);
|
|
assert.equal(EXIT_CODES.ERROR, 1);
|
|
assert.equal(EXIT_CODES.INVALID_ARG, 2);
|
|
assert.equal(EXIT_CODES.SERVER_OFFLINE, 3);
|
|
assert.equal(EXIT_CODES.AUTH, 4);
|
|
assert.equal(EXIT_CODES.RATE_LIMIT, 5);
|
|
assert.equal(EXIT_CODES.TIMEOUT, 124);
|
|
});
|
|
|
|
// ─── statusToExitCode mapping ────────────────────────────────────────────────
|
|
|
|
test("statusToExitCode maps HTTP statuses correctly", () => {
|
|
assert.equal(statusToExitCode(200), 0, "200 → 0");
|
|
assert.equal(statusToExitCode(201), 0, "201 → 0");
|
|
assert.equal(statusToExitCode(204), 0, "204 → 0");
|
|
assert.equal(statusToExitCode(400), 2, "400 → 2 (bad arg)");
|
|
assert.equal(statusToExitCode(401), 4, "401 → 4 (auth)");
|
|
assert.equal(statusToExitCode(403), 4, "403 → 4 (auth)");
|
|
assert.equal(statusToExitCode(404), 2, "404 → 2 (not found)");
|
|
assert.equal(statusToExitCode(408), 124, "408 → 124 (timeout)");
|
|
assert.equal(statusToExitCode(422), 2, "422 → 2 (validation)");
|
|
assert.equal(statusToExitCode(429), 5, "429 → 5 (rate limit)");
|
|
assert.equal(statusToExitCode(500), 1, "500 → 1 (server error)");
|
|
assert.equal(statusToExitCode(502), 1, "502 → 1 (gateway)");
|
|
assert.equal(statusToExitCode(503), 1, "503 → 1 (unavailable)");
|
|
assert.equal(statusToExitCode(504), 1, "504 → 1 (gateway timeout)");
|
|
});
|
|
|
|
// ─── retry backoff ────────────────────────────────────────────────────────────
|
|
|
|
test("computeBackoff respects Retry-After header", () => {
|
|
const delay = computeBackoff(1, "10");
|
|
assert.ok(delay <= RETRY_DEFAULTS.maxMs, "capped at maxMs");
|
|
assert.ok(delay <= 10_000, "respects 10s header");
|
|
assert.ok(delay > 0, "positive delay");
|
|
});
|
|
|
|
test("computeBackoff grows exponentially without header", () => {
|
|
const d1 = computeBackoff(1, null, { ...RETRY_DEFAULTS, jitter: false });
|
|
const d2 = computeBackoff(2, null, { ...RETRY_DEFAULTS, jitter: false });
|
|
const d3 = computeBackoff(3, null, { ...RETRY_DEFAULTS, jitter: false });
|
|
assert.ok(d2 > d1, "attempt 2 > attempt 1");
|
|
assert.ok(d3 >= d2, "attempt 3 >= attempt 2 (may cap)");
|
|
assert.ok(d3 <= RETRY_DEFAULTS.maxMs, "capped at maxMs");
|
|
});
|
|
|
|
test("computeBackoff with jitter stays within ±25% of base", () => {
|
|
const base = computeBackoff(1, null, { ...RETRY_DEFAULTS, jitter: false });
|
|
for (let i = 0; i < 20; i++) {
|
|
const jittered = computeBackoff(1, null, RETRY_DEFAULTS);
|
|
const tolerance = base * 0.25 + 1;
|
|
assert.ok(jittered >= base - tolerance, `jitter too low (${jittered} vs ${base})`);
|
|
assert.ok(jittered <= base + tolerance, `jitter too high (${jittered} vs ${base})`);
|
|
}
|
|
});
|
|
|
|
// ─── i18n ────────────────────────────────────────────────────────────────────
|
|
|
|
test("t() returns key for missing locale entry", () => {
|
|
resetForTests();
|
|
setLocale("en");
|
|
const result = t("nonexistent.key.that.does.not.exist");
|
|
assert.equal(result, "nonexistent.key.that.does.not.exist");
|
|
});
|
|
|
|
test("t() interpolates variables", () => {
|
|
resetForTests();
|
|
setLocale("en");
|
|
const result = t("common.error", { message: "disk full" });
|
|
assert.ok(result.includes("disk full"), `got: ${result}`);
|
|
});
|
|
|
|
test("t() falls back to en for unknown locale", () => {
|
|
resetForTests();
|
|
setLocale("xx-UNKNOWN");
|
|
const result = t("common.success");
|
|
assert.ok(result.length > 0 && result !== "common.success", `fallback failed: ${result}`);
|
|
});
|
|
|
|
test("t() supports pt-BR locale", () => {
|
|
resetForTests();
|
|
setLocale("pt-BR");
|
|
const en = (() => {
|
|
resetForTests();
|
|
setLocale("en");
|
|
return t("common.serverOffline");
|
|
})();
|
|
resetForTests();
|
|
setLocale("pt-BR");
|
|
const ptBR = t("common.serverOffline");
|
|
assert.notEqual(en, ptBR, "pt-BR should differ from en");
|
|
assert.ok(ptBR.length > 0 && ptBR !== "common.serverOffline");
|
|
});
|
|
|
|
test("t() does not expose __proto__ traversal", () => {
|
|
resetForTests();
|
|
setLocale("en");
|
|
const result = t("__proto__.polluted");
|
|
assert.equal(result, "__proto__.polluted", "should return key unchanged");
|
|
});
|