Files
fusion/packages/engine/src/__tests__/usage-limit-detector.test.ts
gsxdsm bce7dbd96f chore: consolidate test files into __tests__/ dirs and clean stray engine artifacts
- Move all co-located *.test.* files into sibling __tests__/ directories so the
  layout is consistent across packages (159 renames + content-rewrite moves).
  Updates relative imports, vi.mock specifiers, and __dirname/import.meta.url
  path resolutions where tests read fixtures from disk.
- Drop tracked tsc-emit alongside engine .ts sources (auth-storage/logger/
  skill-resolver/context-limit-detector/pi.{js,d.ts,*.map}). These were
  accidentally committed in a merge and the stale pi.js was masking a real
  test-mock vs source mismatch (tests imported "../pi.js" and vite preferred
  the stale build over pi.ts).
- Add packages/engine/.gitignore to block future src/*.{js,d.ts,map}.
- Refactor plugin pi-module seams (openclaw/paperclip/hermes) to ESM-import
  createFnAgent / promptWithFallback / describeModel from @fusion/engine
  instead of require()-ing packages/engine/src/pi.js. Adds @fusion/engine to
  the two plugin package.jsons that were missing it; exports describeModel
  from the engine public API.
- Fix engine test mocks now that they run against current pi.ts: add
  ModelRegistry.create static to mocks in pi.test.ts and pi-create-fn-agent
  .test.ts; switch three boundary-result toEqual assertions to toMatchObject
  so the new content/isError fields don't trip exact-match comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:39:20 -07:00

212 lines
7.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { isUsageLimitError, UsageLimitPauser, checkSessionError } from "../usage-limit-detector.js";
// ── isUsageLimitError classification tests ───────────────────────────
describe("isUsageLimitError", () => {
describe("should match usage-limit errors", () => {
const usageLimitMessages = [
// Anthropic overloaded
"overloaded_error: Overloaded",
"API is overloaded",
// Rate limiting
"rate_limit_error: Rate limit exceeded",
"rate limit exceeded",
"Rate Limit Reached",
"Too many requests",
"too many requests, please retry after 60s",
// HTTP status codes
"Request failed with status 429",
"HTTP 429: Too Many Requests",
"529 overloaded",
"Status 529",
// Quota / billing
"quota exceeded for this billing period",
"Quota limit reached",
"billing account is inactive",
"Billing issue detected",
"insufficient credit balance",
"Insufficient credits",
"credit balance too low",
];
for (const msg of usageLimitMessages) {
it(`matches: "${msg}"`, () => {
expect(isUsageLimitError(msg)).toBe(true);
});
}
});
describe("should NOT match transient server errors", () => {
const transientMessages = [
"Internal Server Error",
"Request failed with status 500",
"HTTP 502: Bad Gateway",
"503 Service Unavailable",
"504 Gateway Timeout",
"connection refused",
"Connection reset by peer",
"ECONNREFUSED",
"timeout exceeded",
"request timed out",
"socket hang up",
"network error",
"ETIMEDOUT",
"DNS lookup failed",
"getaddrinfo ENOTFOUND",
];
for (const msg of transientMessages) {
it(`does not match: "${msg}"`, () => {
expect(isUsageLimitError(msg)).toBe(false);
});
}
});
it("returns false for empty string", () => {
expect(isUsageLimitError("")).toBe(false);
});
it("returns false for generic error messages", () => {
expect(isUsageLimitError("Something went wrong")).toBe(false);
expect(isUsageLimitError("Unexpected token in JSON")).toBe(false);
});
});
// ── checkSessionError tests ──────────────────────────────────────────
describe("checkSessionError", () => {
it("throws when session.state.error is set", () => {
const session = { state: { error: "rate_limit_error: Rate limit exceeded" } };
expect(() => checkSessionError(session)).toThrow("rate_limit_error: Rate limit exceeded");
});
it("does not throw when session.state.error is undefined", () => {
const session = { state: { error: undefined } };
expect(() => checkSessionError(session)).not.toThrow();
});
it("does not throw when session.state.error is empty string", () => {
const session = { state: { error: "" } };
expect(() => checkSessionError(session)).not.toThrow();
});
it("thrown error message matches session.state.error exactly", () => {
const errorMessage = "overloaded_error: Overloaded";
const session = { state: { error: errorMessage } };
let thrownMessage: string | undefined;
try {
checkSessionError(session);
} catch (err: any) {
thrownMessage = err.message;
}
expect(thrownMessage).toBe(errorMessage);
// Verify isUsageLimitError can classify it
expect(isUsageLimitError(thrownMessage!)).toBe(true);
});
it("thrown error message for rate limit is classifiable by isUsageLimitError", () => {
const session = { state: { error: "429 Too Many Requests" } };
let thrownMessage: string | undefined;
try {
checkSessionError(session);
} catch (err: any) {
thrownMessage = err.message;
}
expect(isUsageLimitError(thrownMessage!)).toBe(true);
});
it("does not throw when state has no error property", () => {
const session = { state: {} };
expect(() => checkSessionError(session as any)).not.toThrow();
});
});
// ── UsageLimitPauser tests ───────────────────────────────────────────
function createMockStore(globalPause = false) {
return {
getSettings: vi.fn().mockResolvedValue({ globalPause }),
updateSettings: vi.fn().mockResolvedValue({ globalPause: true }),
logEntry: vi.fn().mockResolvedValue(undefined),
} as any;
}
describe("UsageLimitPauser", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("calls store.updateSettings({ globalPause: true, globalPauseReason: \"rate-limit\" }) on usage limit hit", async () => {
const store = createMockStore();
const pauser = new UsageLimitPauser(store);
await pauser.onUsageLimitHit("executor", "FN-001", "rate_limit_error: Rate limit exceeded");
expect(store.updateSettings).toHaveBeenCalledWith({
globalPause: true,
globalPauseReason: "rate-limit",
});
});
it("logs the triggering error on the task via store.logEntry", async () => {
const store = createMockStore();
const pauser = new UsageLimitPauser(store);
await pauser.onUsageLimitHit("triage", "FN-002", "overloaded_error");
expect(store.logEntry).toHaveBeenCalledWith(
"FN-002",
"Usage limit detected (triage): overloaded_error",
);
});
it("is idempotent — calling multiple times only triggers one pause", async () => {
const store = createMockStore();
// After first call, globalPause will be true
store.getSettings.mockResolvedValue({ globalPause: true });
const pauser = new UsageLimitPauser(store);
await pauser.onUsageLimitHit("executor", "FN-001", "rate limit");
await pauser.onUsageLimitHit("triage", "FN-002", "rate limit");
await pauser.onUsageLimitHit("merger", "FN-003", "rate limit");
// updateSettings should only be called once
expect(store.updateSettings).toHaveBeenCalledTimes(1);
});
it("re-triggers pause if globalPause was externally reset to false", async () => {
const store = createMockStore();
const pauser = new UsageLimitPauser(store);
// First hit — triggers pause
store.getSettings.mockResolvedValue({ globalPause: true });
await pauser.onUsageLimitHit("executor", "FN-001", "rate limit");
expect(store.updateSettings).toHaveBeenCalledTimes(1);
// External reset: globalPause set to false
store.getSettings.mockResolvedValue({ globalPause: false });
// Second hit — should trigger again since it was reset
await pauser.onUsageLimitHit("executor", "FN-004", "rate limit again");
expect(store.updateSettings).toHaveBeenCalledTimes(2);
});
it("includes agent type in the log entry", async () => {
const store = createMockStore();
const pauser = new UsageLimitPauser(store);
await pauser.onUsageLimitHit("merger", "FN-005", "quota exceeded");
expect(store.logEntry).toHaveBeenCalledWith(
"FN-005",
expect.stringContaining("merger"),
);
});
});