Files
SnapOtter/tests/unit/shared/tool-errors.test.ts
T
SnapOtterandGitHub ae6a4c8b7c fix: error-only Sentry telemetry, storm-proof capture, and crash fixes (#476)
Removes Sentry tracing entirely (BullMQ idle polling burned 4.8M transactions in 2 days at the baked 0.1 rate), decouples PostHog sampling, and replaces the type-only error scrub with a vetted-field sanitizer plus SafeError/ToolInputError contracts. One classified capture path with per-signature throttles and a per-process ceiling makes storms impossible (NODE-1E was 4,541 events from one 30s loop). Browser errors move to a dedicated web Sentry project with their own source maps. Adds the SNAPOTTER_TELEMETRY runtime kill switch and silences test fleets.

Crash fixes: remote 204/304 SSRF process kill (NODE-20), conversion-preset boot crash loop (NODE-21), Redis version preflight + unhandled subscribe rejection (NODE-1T), Sign PDF on plain-http origins (NODE-1K/1M), wavesurfer/pdf.js teardown rejections (NODE-1P/1N), bundle-import ZlibError to 400 (NODE-1Z), chart-maker input errors declassified (NODE-1H/1J), asset requests skip the session DB lookup (NODE-1D).
2026-07-10 21:41:49 +08:00

66 lines
2.3 KiB
TypeScript

import {
isSafeMessageError,
isToolInputError,
markToolInputError,
SafeError,
ToolInputError,
} from "@snapotter/shared";
import { describe, expect, it } from "vitest";
describe("SafeError", () => {
it("carries a constant message, kind, and code", () => {
const err = new SafeError("Storage directory is not writable", {
kind: "operational",
code: "EACCES",
statusCode: 503,
});
expect(err.message).toBe("Storage directory is not writable");
expect(err.kind).toBe("operational");
expect(err.code).toBe("EACCES");
expect(err.statusCode).toBe(503);
expect(isSafeMessageError(err)).toBe(true);
});
it("defaults kind to operational and detects via marker not instanceof", () => {
const copy = Object.assign(new Error("x"), { isSafeMessage: true, message: "x" });
expect(new SafeError("m").kind).toBe("operational");
expect(isSafeMessageError(copy)).toBe(true);
expect(isSafeMessageError(new Error("m"))).toBe(false);
expect(isSafeMessageError(null)).toBe(false);
});
it("rejects a marked plain object that is not an Error", () => {
expect(isSafeMessageError({ isSafeMessage: true })).toBe(false);
});
it("leaves code and statusCode undefined by default", () => {
const s = new SafeError("m");
expect(s.code).toBeUndefined();
expect(s.statusCode).toBeUndefined();
});
it("threads cause through to the Error constructor", () => {
const inner = new Error("inner");
expect(new SafeError("m", { cause: inner }).cause).toBe(inner);
});
});
describe("ToolInputError", () => {
it("is marked and 400-shaped", () => {
const err = new ToolInputError("Column 2 must be numeric");
expect(err.statusCode).toBe(400);
expect(isToolInputError(err)).toBe(true);
});
it("markToolInputError flags a foreign error without changing its class", () => {
const raw = new Error("ffmpeg: received no packets");
expect(isToolInputError(raw)).toBe(false);
markToolInputError(raw);
expect(isToolInputError(raw)).toBe(true);
expect(raw).toBeInstanceOf(Error);
});
it("rejects non-Error values", () => {
expect(isToolInputError(null)).toBe(false);
expect(isToolInputError("x")).toBe(false);
});
it("markToolInputError returns the same reference", () => {
const e = new Error("y");
expect(markToolInputError(e)).toBe(e);
});
});