From 5ca7ec86e1f62916dd0379db0d38f17f34642f50 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 20 Sep 2026 10:27:30 +0200 Subject: [PATCH] test(providers): pin opencode 400 model-unavailable catalog rule at registry level --- ...251-opencode-400-model-unavailable-rule.md | 1 + ...ode-400-model-unavailable-registry.test.ts | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 changelog.d/fixes/14251-opencode-400-model-unavailable-rule.md create mode 100644 tests/unit/opencode-400-model-unavailable-registry.test.ts diff --git a/changelog.d/fixes/14251-opencode-400-model-unavailable-rule.md b/changelog.d/fixes/14251-opencode-400-model-unavailable-rule.md new file mode 100644 index 000000000000..4845e6da02dd --- /dev/null +++ b/changelog.d/fixes/14251-opencode-400-model-unavailable-rule.md @@ -0,0 +1 @@ +- **test(providers):** pin the opencode 400 model-unavailable catalog rule at registry level ([#14251](https://github.com/diegosouzapw/OmniRoute/pull/14251)) — thanks @maxmad64bis diff --git a/tests/unit/opencode-400-model-unavailable-registry.test.ts b/tests/unit/opencode-400-model-unavailable-registry.test.ts new file mode 100644 index 000000000000..1f5702bb52b1 --- /dev/null +++ b/tests/unit/opencode-400-model-unavailable-registry.test.ts @@ -0,0 +1,71 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + getProviderErrorRuleMatch, + getOpencodeModelUnavailableMatch, + honorsRuleLockScope, +} from "../../open-sse/config/providerErrorRules.ts"; + +/** + * Registry-level pin of the `opencode-400-model-unavailable` catalog rule. + * + * The end-to-end path (checkFallbackError → persistence → combo) is covered by + * `opencode-400-model-unavailable.test.ts`; this file asserts the registry + * contract directly: marker 400 → model-scope lock with the rule-owned + * cooldown, any other 400 → null (terminal, locks nothing), helper fenced to + * the opencode family. Never imports accountFallback (registry/behavior + * boundary). + */ + +const MARKER = "Upstream request failed: Model is unavailable."; +const FAMILY = ["opencode", "opencode-zen", "opencode-go", "opencode-cli"]; +const NON_FAMILY = ["agentrouter", "openrouter", "minimax", "unknown-vendor"]; + +test("marker 400 returns the model-scope lock with the rule-owned cooldown", () => { + for (const provider of FAMILY) { + const match = getProviderErrorRuleMatch(provider, 400, {}, { error: { message: MARKER } }); + assert.deepStrictEqual(match, { + reason: "model_capacity", + scope: "model", + cooldownMs: 3_600_000, + }); + } + // String envelope + canonical-case provider (lookup is case-insensitive). + const stringEnvelope = getProviderErrorRuleMatch( + "OpenCode", + 400, + null, + `{"message":"${MARKER}"}` + ); + assert.equal(stringEnvelope?.scope, "model"); + assert.equal(stringEnvelope?.reason, "model_capacity"); + assert.equal(stringEnvelope?.cooldownMs, 3_600_000); +}); + +test("any other 400 stays terminal and locks nothing", () => { + for (const body of [ + "improperly formed request: invalid message format", + "invalid api key", + "{}", + "", + ]) { + assert.equal(getProviderErrorRuleMatch("opencode", 400, {}, body), null, body); + } + // Right marker, wrong status: the rule is 400-only. + assert.equal(getProviderErrorRuleMatch("opencode", 429, {}, MARKER), null); +}); + +test("helper is fenced to the opencode family and the allowlist honors it", () => { + for (const provider of FAMILY) { + assert.equal(honorsRuleLockScope(provider), true, provider); + } + assert.deepStrictEqual(getOpencodeModelUnavailableMatch("opencode", 400, null, MARKER), { + reason: "model_capacity", + scope: "model", + cooldownMs: 3_600_000, + }); + for (const provider of NON_FAMILY) { + assert.equal(getOpencodeModelUnavailableMatch(provider, 400, null, MARKER), null, provider); + } + assert.equal(getOpencodeModelUnavailableMatch("opencode", 429, null, MARKER), null); +});