Files
OmniRoute/tests/unit/cursor-errors-classify.test.ts
SB Yoon c130f2aa1c feat(providers): Cursor PKCE login with Bearer quota, auto router, and empty-turn errors (#9909)
5 — Cursor PKCE login com Bearer quota, auto router e empty-turn errors. Feature completa e testada (11 arquivos de teste, 133 testes focados, todos verdes).

**Validação (worktree combinado `.claude/worktrees/fix-9909`, board sobre `origin/release/v3.8.50`):**
- 3 conflitos reais resolvidos: `config/quality/eslint-suppressions.json` (aditivo), `open-sse/config/providers/registry/cursor/index.ts` (dedup de 208 entradas de catálogo, 0 IDs duplicados verificado), `open-sse/executors/cursor.ts` (imports aditivos).
- `npm run typecheck:core`: limpo.
- `check-changelog-integrity`, `check-file-size`, `check-complexity` (2615/2774), `check-cognitive-complexity` (1175/1223), `check-dead-code` (410/416): todos OK.
- `check-public-creds`: 1 entrada obsoleta pré-existente na allowlist (`copilot-m365-web.ts:330`), já presente no tip da release — não é desta PR.
- `npm run lint`: 0 errors (5 warnings pré-existentes).
- Testes focados (`cursor-agent-cli-version`, `cursor-available-models`, `cursor-catalog-combo-compat`, `cursor-errors-classify`, `cursor-login-pkce`, `cursor-model-effort-suffix-7289`, `cursor-streaming`, `cursor-token-extractor`, `cursor-token-refresh-wiring`, `cursor-usage-fetcher`, `empty-stream-no-content-8649`): 133/133 verdes.
- Corrigido durante a validação: 1 teste novo da própria PR (`cursor-model-effort-suffix-7289.test.ts`, "splits effort off legacy grok- ids") colidia com `CURSOR_MODEL_ALIASES` já mesclado na release (mapeia `grok-4.5-high` → `cursor-grok-4.5-high` antes do fallback legado rodar); ajustado para usar um id não-aliasado (`grok-3-high`) que de fato exercita o fallback — commit `68b58ed`.

Obrigado pela contribuição, @yansigit — feature robusta com boa cobertura de testes.
2026-08-21 10:28:01 -03:00

79 lines
3.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
classifyCursorError,
classifyCursorErrorKind,
isCursorBenignCancelError,
isCursorRequestTooLargeDetail,
resolveCursorEmptyTurnError,
CURSOR_EMPTY_TURN_MESSAGE,
} from "../../open-sse/executors/cursor/cursorErrors.ts";
test("classifyCursorErrorKind: resource_exhausted without size cue is rate_limit", () => {
assert.equal(classifyCursorErrorKind("resource_exhausted: quota exhausted"), "rate_limit");
assert.equal(classifyCursorErrorKind("You're out of usage. Increase limits"), "rate_limit");
});
test("classifyCursorErrorKind: resource_exhausted + size cue is invalid", () => {
assert.equal(classifyCursorErrorKind("resource_exhausted: tool catalog too large"), "invalid");
assert.equal(isCursorRequestTooLargeDetail("tool catalog too large"), true);
assert.equal(
isCursorRequestTooLargeDetail("resource_exhausted while loading tool catalog: quota exhausted"),
false
);
});
test("classifyCursorErrorKind: auth cues", () => {
assert.equal(classifyCursorErrorKind("Authentication error"), "auth");
assert.equal(classifyCursorErrorKind("unauthenticated"), "auth");
});
test("classifyCursorErrorKind: AI Model Not Found is rate_limit (Cursor out-of-usage)", () => {
assert.equal(
classifyCursorErrorKind("not_found: AI Model Not Found (reset after 109h 9s)"),
"rate_limit"
);
assert.equal(classifyCursorErrorKind("not_found: AI Model Not Found"), "rate_limit");
assert.equal(classifyCursorErrorKind("model not found"), "invalid");
});
test("classifyCursorError redacts credentials and paths", () => {
const out = classifyCursorError(
"Authentication failed authorization=secret-token path=/Users/alice/code/file"
);
assert.equal(out.kind, "auth");
assert.equal(out.status, 401);
assert.match(out.message, /\[REDACTED\]/);
assert.match(out.message, /\[REDACTED_PATH\]/);
assert.doesNotMatch(out.message, /secret-token/);
assert.doesNotMatch(out.message, /\/Users\/alice/);
});
test("isCursorBenignCancelError recognizes NGHTTP2_CANCEL / suspend", () => {
assert.equal(isCursorBenignCancelError({ code: "NGHTTP2_CANCEL" }), true);
assert.equal(isCursorBenignCancelError(new Error("nghttp2_cancel")), true);
assert.equal(isCursorBenignCancelError(new Error("Cursor stream suspended")), true);
assert.equal(isCursorBenignCancelError(new Error("resource_exhausted")), false);
});
test("resolveCursorEmptyTurnError: no upstream → actionable 502", () => {
const out = resolveCursorEmptyTurnError({});
assert.equal(out.status, 502);
assert.equal(out.message, CURSOR_EMPTY_TURN_MESSAGE);
});
test("resolveCursorEmptyTurnError: quotaExhaustedHint → 429", () => {
const out = resolveCursorEmptyTurnError({ quotaExhaustedHint: true });
assert.equal(out.status, 429);
assert.equal(out.type, "rate_limit_error");
assert.equal(out.message, CURSOR_EMPTY_TURN_MESSAGE);
});
test("resolveCursorEmptyTurnError: classifies upstream quota message as 429", () => {
const out = resolveCursorEmptyTurnError({
upstreamMessage: "resource_exhausted: too many requests",
});
assert.equal(out.status, 429);
assert.equal(out.kind, "rate_limit");
});