Files
OmniRoute/tests/unit/provider-error-detail-lastError.test.ts
Nguyen Thanh Dat 019ad33a61 fix(auth): keep the real upstream reason in lastError (#11376)
Merged via consolidated batch validation. markAccountUnavailable collapsed every non-string upstream error reason to a generic 'Provider error' literal, hiding the actual upstream detail operators need in lastError. Own test passes.
2026-08-24 12:13:11 -03:00

97 lines
4.0 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import {
describeUpstreamFailure,
extractErrorMessage,
} from "../../src/shared/utils/upstreamError.ts";
// `markAccountUnavailable` stored the upstream reason only when it was already a
// string — everything else collapsed to the literal "Provider error", which is
// what the dashboard shows as `lastError` and what the console line prints.
//
// The case that matters most is not a string: a failed fetch arrives as
// `TypeError: fetch failed` with the actionable part on `error.cause.code`, so a
// wrong port, a firewall and a blocked proxy were indistinguishable.
/** The shape Node produces for a refused connection. */
function fetchFailed(code: string): Error {
const error = new TypeError("fetch failed");
(error as Error & { cause?: unknown }).cause = Object.assign(
new Error(`connect ${code} 127.0.0.1:11434`),
{ code }
);
return error;
}
test("a string reason is unchanged and still clamped", () => {
assert.equal(describeUpstreamFailure("upstream said no"), "upstream said no");
assert.equal(describeUpstreamFailure("x".repeat(200)), "x".repeat(100));
});
test("the transport code behind `fetch failed` survives", () => {
assert.equal(describeUpstreamFailure(fetchFailed("ECONNREFUSED")), "fetch failed (ECONNREFUSED)");
assert.equal(describeUpstreamFailure(fetchFailed("ENOTFOUND")), "fetch failed (ENOTFOUND)");
});
test("a code already named in the message is not repeated", () => {
const error = Object.assign(new Error("connect ETIMEDOUT 10.0.0.5:443"), { code: "ETIMEDOUT" });
assert.equal(describeUpstreamFailure(error), "connect ETIMEDOUT 10.0.0.5:443");
});
test("the usual provider JSON shapes are read", () => {
assert.equal(
describeUpstreamFailure({ error: { message: "model not found" } }),
"model not found"
);
assert.equal(describeUpstreamFailure({ message: "quota exceeded" }), "quota exceeded");
assert.equal(describeUpstreamFailure({ error: "invalid api key" }), "invalid api key");
assert.equal(describeUpstreamFailure({ detail: "no such deployment" }), "no such deployment");
assert.equal(describeUpstreamFailure({ errors: [{ message: "a" }, { message: "b" }] }), "a, b");
});
test("a bare code is better than nothing", () => {
assert.equal(describeUpstreamFailure({ code: "EAI_AGAIN" }), "Provider error (EAI_AGAIN)");
});
test("nothing to say still yields the fallback", () => {
assert.equal(describeUpstreamFailure({}), "Provider error");
assert.equal(describeUpstreamFailure(null), "Provider error");
assert.equal(describeUpstreamFailure(undefined), "Provider error");
assert.equal(describeUpstreamFailure(42), "Provider error");
assert.equal(describeUpstreamFailure({}, "Upstream down"), "Upstream down");
});
test("the error object is never serialized wholesale", () => {
const withPayload = {
code: "EPIPE",
request: { headers: { authorization: "Bearer sk-do-not-store" } },
};
const reason = describeUpstreamFailure(withPayload);
assert.equal(reason, "Provider error (EPIPE)");
assert.ok(!reason.includes("sk-do-not-store"));
assert.ok(!reason.includes("authorization"));
});
test("newlines are collapsed so the dashboard row stays one line", () => {
assert.equal(describeUpstreamFailure({ message: "line one\nline two" }), "line one line two");
});
test("extractErrorMessage stays available to toJsonErrorPayload's callers", () => {
assert.equal(extractErrorMessage({ message: "hi" }), "hi");
assert.equal(extractErrorMessage("hi"), null);
});
test("markAccountUnavailable routes lastError through the helper", () => {
const src = fs.readFileSync(new URL("../../src/sse/services/auth.ts", import.meta.url), "utf8");
assert.ok(
src.includes("describeUpstreamFailure(errorText)"),
"auth.ts must describe the failure instead of discarding non-string errors"
);
assert.equal(
/typeof errorText === "string" \? errorText\.slice\(0, 100\) : "Provider error"/.test(src),
false,
"the string-only collapse must be gone"
);
});