test: close tea and error-classifier coverage gaps, raise thresholds
All checks were successful
CI / test (pull_request) Successful in 53s
CI / test (push) Successful in 21s

Cover the previously untested credential and error-classification paths:
- tea.ts: failing/invalid/non-array `tea login list`, a failing token
  helper, and an empty token, all driven through the CLI seam via the fake
  tea harness (now parametrizable for exit codes and output).
- errors.ts: a unit-tier test of classifyHttpError for the pure-classifier
  branches no shipped command reaches yet (issue/pull 404s, the
  already-classified passthrough, transport failures, no-body defaults).

errors.ts is now fully covered and tea.ts is at 96% (the remainder is
defensive code unreachable through the seam). Overall coverage rose from
~87% to ~93% statements / ~89% branches; the ratchet is raised to
92/87/95/92 accordingly.
This commit was merged in pull request #1.
This commit is contained in:
2026-07-11 09:15:15 -04:00
parent 94e81966be
commit ae0f4c2675
3 changed files with 226 additions and 11 deletions

View File

@@ -0,0 +1,95 @@
import { AxiError } from "axi-sdk-js";
import { describe, expect, it } from "vitest";
import { classifyHttpError } from "../src/errors.js";
/**
* Unit-tier coverage of the HTTP error classifier (a pure function, no I/O).
* The integration tier drives the classifier through the issue-list seam, but
* some branches belong to paths no shipped command reaches yet — pull-request
* 404s, the already-classified passthrough, and non-HTTP transport failures —
* so they are exercised directly here against crafted inputs.
*/
function httpError(status: number, url: string, body?: unknown) {
return { status, url, error: body };
}
const REPO = "http://gitea.example/api/v1/repos/o/r";
describe("classifyHttpError", () => {
it("passes an already-classified AxiError through unchanged", () => {
const original = new AxiError("boom", "FORBIDDEN", ["hint"]);
expect(classifyHttpError(original)).toBe(original);
});
it("classifies a 404 on an issue path as ISSUE_NOT_FOUND", () => {
const result = classifyHttpError(httpError(404, `${REPO}/issues/42`));
expect(result.code).toBe("ISSUE_NOT_FOUND");
expect(result.message).toContain("#42");
});
it("classifies a 404 on a pull path as PR_NOT_FOUND", () => {
const result = classifyHttpError(httpError(404, `${REPO}/pulls/7`));
expect(result.code).toBe("PR_NOT_FOUND");
expect(result.message).toContain("#7");
});
it("classifies a 404 on the repo subtree as REPO_NOT_FOUND", () => {
const result = classifyHttpError(httpError(404, `${REPO}/issues`));
expect(result.code).toBe("REPO_NOT_FOUND");
expect(result.message).toContain("o/r");
});
it("classifies a 404 on a non-repo path as UNKNOWN", () => {
const result = classifyHttpError(httpError(404, "http://gitea.example/api/v1/version"));
expect(result.code).toBe("UNKNOWN");
});
it("falls back to the raw url when the response url does not parse", () => {
const result = classifyHttpError(httpError(404, "::not a url::"));
expect(result.code).toBe("UNKNOWN");
expect(result.message).toContain("::not a url::");
});
it("uses the body message when present, and a default when absent", () => {
expect(classifyHttpError(httpError(401, REPO, { message: "token expired" })).message).toBe(
"token expired",
);
expect(classifyHttpError(httpError(401, REPO, {})).message).toBe("Authentication required");
});
it("maps 403 and every validation status (405/409/422) and 429 to their codes", () => {
expect(classifyHttpError(httpError(403, REPO, {})).code).toBe("FORBIDDEN");
expect(classifyHttpError(httpError(405, REPO, {})).code).toBe("VALIDATION_ERROR");
expect(classifyHttpError(httpError(409, REPO, {})).code).toBe("VALIDATION_ERROR");
expect(classifyHttpError(httpError(422, REPO, {})).code).toBe("VALIDATION_ERROR");
expect(classifyHttpError(httpError(429, REPO, {})).code).toBe("RATE_LIMITED");
});
it("maps an unexpected status to UNKNOWN, with and without a body message", () => {
expect(classifyHttpError(httpError(500, REPO, { message: "kaboom" })).message).toContain(
"kaboom",
);
const bare = classifyHttpError(httpError(503, REPO, {}));
expect(bare.code).toBe("UNKNOWN");
expect(bare.message).toContain("503");
});
it("classifies a plain Error transport failure as UNKNOWN", () => {
const result = classifyHttpError(new Error("network down"));
expect(result.code).toBe("UNKNOWN");
expect(result.message).toContain("network down");
});
it("includes the underlying cause when a transport failure carries one", () => {
const error = new Error("fetch failed", { cause: new Error("ECONNREFUSED") });
const result = classifyHttpError(error);
expect(result.message).toContain("fetch failed");
expect(result.message).toContain("ECONNREFUSED");
});
it("stringifies a non-Error thrown value", () => {
const result = classifyHttpError("just a string");
expect(result.code).toBe("UNKNOWN");
expect(result.message).toContain("just a string");
});
});