mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
CONTRACT CHANGE. classifyChatGptSessionError put message-pattern matching ahead of an explicit numeric status, so an adapter error carrying status 503 whose prose mentioned signing in was classified 401 session_expired — marking a healthy account's credentials expired and pulling it out of rotation. The vendor documents the field as "Authoritative upstream/proxy status when known; avoids message-based classification" (vendor/codex-chatgpt-web/types.ts). Explicit status now runs third (after ChatGptSessionInputError and TimeoutError, both of which are stronger signals), uses the event's own code when present, and attaches fallbackHint: "connection_cooldown" for 503/429 so a genuine outage cools one connection instead of tripping the whole-provider breaker. Message matching is unchanged and still essential — errors thrown by the executor itself carry no status. ChatGptSessionStreamOpen's error arm now carries fallbackHint, so the executor uses the bridge's classification directly instead of re-classifying the sanitized message. The test "a matching message wins over an explicit upstream status" is deliberately inverted and renamed; its comment records why.
158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { classifyChatGptSessionError } from "../../open-sse/executors/chatgpt-session/errors.ts";
|
|
import { ChatGptSessionInputError } from "../../open-sse/executors/chatgpt-session/messages.ts";
|
|
|
|
test("a missing browser is a cooldown-hinted 503", () => {
|
|
const result = classifyChatGptSessionError(
|
|
new Error("No supported Chrome or Chromium executable was found")
|
|
);
|
|
assert.equal(result.status, 503);
|
|
assert.equal(result.code, "browser_unavailable");
|
|
assert.equal(result.fallbackHint, "connection_cooldown");
|
|
});
|
|
|
|
test("a playwright launch failure is also a cooldown-hinted 503", () => {
|
|
const result = classifyChatGptSessionError(
|
|
new Error("browserType.launch: Executable doesn't exist at /root/.cache/ms-playwright/x")
|
|
);
|
|
assert.equal(result.status, 503);
|
|
assert.equal(result.fallbackHint, "connection_cooldown");
|
|
});
|
|
|
|
test("missing credentials are a 401", () => {
|
|
assert.equal(
|
|
classifyChatGptSessionError(new Error("ChatGPT browser credentials are missing")).status,
|
|
401
|
|
);
|
|
});
|
|
|
|
test("an expired session is a 401 session_expired", () => {
|
|
const result = classifyChatGptSessionError(new Error("ChatGPT page is not authenticated"));
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.code, "session_expired");
|
|
});
|
|
|
|
test("a rate-limit dialog is a 429", () => {
|
|
const result = classifyChatGptSessionError(new Error("ChatGPT reported a usage limit"));
|
|
assert.equal(result.status, 429);
|
|
assert.equal(result.code, "rate_limited");
|
|
});
|
|
|
|
test("an account-capability mismatch is a terminal 400", () => {
|
|
const result = classifyChatGptSessionError(
|
|
new Error("pro is not available for this non-Pro connection")
|
|
);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.code, "route_unavailable");
|
|
assert.equal(result.fallbackHint, undefined);
|
|
});
|
|
|
|
test("a DOM timeout is a terminal 400, not a retryable 5xx", () => {
|
|
const timeout = new Error("locator.waitForSelector: Timeout 30000ms exceeded");
|
|
timeout.name = "TimeoutError";
|
|
const result = classifyChatGptSessionError(timeout);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.code, "browser_ui_timeout");
|
|
});
|
|
|
|
test("a TimeoutError naming a login selector is a UI timeout, not an expired session", () => {
|
|
const timeout = new Error(
|
|
'page.waitForSelector: Timeout 15000ms exceeded ... selector "button:has-text(\\"Log in\\")"'
|
|
);
|
|
timeout.name = "TimeoutError";
|
|
const result = classifyChatGptSessionError(timeout);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.code, "browser_ui_timeout");
|
|
});
|
|
|
|
test("input errors map to their own 400 codes", () => {
|
|
const result = classifyChatGptSessionError(
|
|
new ChatGptSessionInputError("vision_unsupported", "no images")
|
|
);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.code, "vision_unsupported");
|
|
});
|
|
|
|
test("an explicit upstream status is used when no message pattern matches", () => {
|
|
const result = classifyChatGptSessionError({ message: "anything", status: 502, code: "x" });
|
|
assert.equal(result.status, 502);
|
|
});
|
|
|
|
// CONTRACT CHANGE (deliberate inversion of the previous expectation). This test used to assert
|
|
// that a recognised message beat an explicit upstream status; the precedence is now the other way
|
|
// round. The vendor documents `AdapterEvent.error.status` as "Authoritative upstream/proxy status
|
|
// when known; avoids message-based classification", and honouring that is what stops a 503 whose
|
|
// prose happens to mention signing in from being reported as an expired session — which would
|
|
// mark a HEALTHY account's credentials dead and pull it out of rotation. Message matching still
|
|
// runs, just as the fallback for failures that carry no status at all (everything this executor
|
|
// throws itself).
|
|
test("an explicit upstream status wins over a matching message", () => {
|
|
const result = classifyChatGptSessionError({
|
|
message: "ChatGPT reported a usage limit",
|
|
status: 500,
|
|
});
|
|
assert.equal(result.status, 500);
|
|
assert.equal(result.code, "turn_failed");
|
|
});
|
|
|
|
test("a session-expired message cannot downgrade an explicit 503 to a 401", () => {
|
|
const result = classifyChatGptSessionError({
|
|
message: "Please sign in to continue",
|
|
status: 503,
|
|
});
|
|
assert.equal(result.status, 503);
|
|
assert.notEqual(result.code, "session_expired");
|
|
});
|
|
|
|
test("an explicit 503 carries the connection cooldown hint", () => {
|
|
const result = classifyChatGptSessionError({ message: "upstream unavailable", status: 503 });
|
|
assert.equal(result.status, 503);
|
|
assert.equal(result.fallbackHint, "connection_cooldown");
|
|
});
|
|
|
|
test("an explicit 429 carries the connection cooldown hint", () => {
|
|
const result = classifyChatGptSessionError({ message: "slow down", status: 429 });
|
|
assert.equal(result.status, 429);
|
|
assert.equal(result.fallbackHint, "connection_cooldown");
|
|
});
|
|
|
|
test("an explicit 400 carries no cooldown hint", () => {
|
|
const result = classifyChatGptSessionError({ message: "bad request", status: 400 });
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.fallbackHint, undefined);
|
|
});
|
|
|
|
test("an explicit status keeps the event's own code when it carries one", () => {
|
|
const result = classifyChatGptSessionError({
|
|
message: "anything",
|
|
status: 503,
|
|
code: "upstream_unavailable",
|
|
});
|
|
assert.equal(result.code, "upstream_unavailable");
|
|
});
|
|
|
|
test("a TimeoutError still outranks an explicit upstream status", () => {
|
|
const timeout = new Error("locator.waitForSelector: Timeout 30000ms exceeded") as Error & {
|
|
status?: number;
|
|
};
|
|
timeout.name = "TimeoutError";
|
|
timeout.status = 503;
|
|
const result = classifyChatGptSessionError(timeout);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.code, "browser_ui_timeout");
|
|
});
|
|
|
|
test("message patterns still classify failures that carry no status", () => {
|
|
const result = classifyChatGptSessionError(new Error("ChatGPT reported a usage limit"));
|
|
assert.equal(result.status, 429);
|
|
assert.equal(result.code, "rate_limited");
|
|
});
|
|
|
|
test("an unrecognised failure is a retryable 502", () => {
|
|
const result = classifyChatGptSessionError(new Error("something odd happened"));
|
|
assert.equal(result.status, 502);
|
|
assert.equal(result.code, "turn_failed");
|
|
});
|