mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Replace the job timeout message that hardcoded "background-removal" for every tool with a tool-agnostic one that sets the CPU-vs-GPU expectation, the usual reason heavy AI times out on modest hardware. The client-side SSE stall message gets the same treatment. Both stay under friendlyError's 280-char limit so the guidance reaches the user instead of collapsing to a generic "Processing failed". Refs #591
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../../apps/api/src/config.js", () => ({
|
|
env: { PROCESSING_TIMEOUT_S: 0 },
|
|
}));
|
|
|
|
import {
|
|
computeExternalToolTimeout,
|
|
computeTimeout,
|
|
timeoutMessage,
|
|
} from "../../../apps/api/src/lib/timeout.js";
|
|
|
|
describe("computeTimeout", () => {
|
|
it("returns correct timeout for sharp category", () => {
|
|
const result = computeTimeout(50, "sharp");
|
|
expect(result).toBe(Math.max(60_000, 50 * 2 * 1000));
|
|
});
|
|
|
|
it("returns correct timeout for ai_cpu category", () => {
|
|
const result = computeTimeout(10, "ai_cpu");
|
|
expect(result).toBe(Math.max(60_000, 10 * 30 * 1000));
|
|
});
|
|
|
|
it("returns correct timeout for ai_gpu category", () => {
|
|
const result = computeTimeout(20, "ai_gpu");
|
|
expect(result).toBe(Math.max(60_000, 20 * 5 * 1000));
|
|
});
|
|
|
|
it("returns correct timeout for external category", () => {
|
|
const result = computeTimeout(10, "external");
|
|
expect(result).toBe(Math.max(60_000, 10 * 10 * 1000));
|
|
});
|
|
|
|
it("returns correct timeout for python category", () => {
|
|
const result = computeTimeout(10, "python");
|
|
expect(result).toBe(Math.max(60_000, 10 * 15 * 1000));
|
|
});
|
|
|
|
it("enforces minimum timeout of 60_000ms", () => {
|
|
const result = computeTimeout(0.001, "sharp");
|
|
expect(result).toBe(60_000);
|
|
});
|
|
|
|
it("enforces minimum timeout for small megapixel values", () => {
|
|
const result = computeTimeout(1, "sharp");
|
|
expect(result).toBe(60_000);
|
|
});
|
|
|
|
it("multiplies timeout by file count", () => {
|
|
const single = computeTimeout(50, "sharp");
|
|
const triple = computeTimeout(50, "sharp", 3);
|
|
expect(triple).toBe(single * 3);
|
|
});
|
|
|
|
it("defaults to fileCount of 1", () => {
|
|
const result = computeTimeout(50, "sharp");
|
|
const explicit = computeTimeout(50, "sharp", 1);
|
|
expect(result).toBe(explicit);
|
|
});
|
|
|
|
it("scales correctly for large megapixel values", () => {
|
|
const result = computeTimeout(100, "ai_cpu");
|
|
expect(result).toBe(100 * 30 * 1000);
|
|
});
|
|
|
|
it("uses override when PROCESSING_TIMEOUT_S > 0", async () => {
|
|
const configMod = await import("../../../apps/api/src/config.js");
|
|
const saved = configMod.env.PROCESSING_TIMEOUT_S;
|
|
configMod.env.PROCESSING_TIMEOUT_S = 120;
|
|
try {
|
|
const result = computeTimeout(999, "ai_gpu", 10);
|
|
expect(result).toBe(120 * 1000);
|
|
} finally {
|
|
configMod.env.PROCESSING_TIMEOUT_S = saved;
|
|
}
|
|
});
|
|
|
|
it("override ignores megapixels and fileCount", async () => {
|
|
const configMod = await import("../../../apps/api/src/config.js");
|
|
const saved = configMod.env.PROCESSING_TIMEOUT_S;
|
|
configMod.env.PROCESSING_TIMEOUT_S = 60;
|
|
try {
|
|
const result = computeTimeout(200, "python", 5);
|
|
expect(result).toBe(60 * 1000);
|
|
} finally {
|
|
configMod.env.PROCESSING_TIMEOUT_S = saved;
|
|
}
|
|
});
|
|
|
|
it("minimum applies per-file before multiplying by count", () => {
|
|
const result = computeTimeout(0, "sharp", 3);
|
|
expect(result).toBe(60_000 * 3);
|
|
});
|
|
});
|
|
|
|
describe("computeExternalToolTimeout", () => {
|
|
it("returns correct timeout for external category", () => {
|
|
const result = computeExternalToolTimeout(10);
|
|
expect(result).toBe(10 * 10 * 1000);
|
|
});
|
|
|
|
it("enforces minimum timeout of 60_000ms", () => {
|
|
const result = computeExternalToolTimeout(0);
|
|
expect(result).toBe(60_000);
|
|
});
|
|
|
|
it("scales with megapixels", () => {
|
|
const result = computeExternalToolTimeout(50);
|
|
expect(result).toBe(50 * 10 * 1000);
|
|
});
|
|
|
|
it("uses override when PROCESSING_TIMEOUT_S > 0", async () => {
|
|
const configMod = await import("../../../apps/api/src/config.js");
|
|
const saved = configMod.env.PROCESSING_TIMEOUT_S;
|
|
configMod.env.PROCESSING_TIMEOUT_S = 300;
|
|
try {
|
|
const result = computeExternalToolTimeout(999);
|
|
expect(result).toBe(300 * 1000);
|
|
} finally {
|
|
configMod.env.PROCESSING_TIMEOUT_S = saved;
|
|
}
|
|
});
|
|
|
|
it("enforces minimum for small megapixel values", () => {
|
|
const result = computeExternalToolTimeout(0.5);
|
|
expect(result).toBe(60_000);
|
|
});
|
|
});
|
|
|
|
describe("timeoutMessage", () => {
|
|
it("reports the elapsed timeout in seconds", () => {
|
|
expect(timeoutMessage(120_000)).toContain("120s");
|
|
expect(timeoutMessage(5_000)).toContain("5s");
|
|
});
|
|
|
|
it("is tool-agnostic (no hardcoded background-removal)", () => {
|
|
// The old copy told every timed-out job a background-removal model was
|
|
// downloading, which was wrong for AI upscale, video, etc. (#591).
|
|
expect(timeoutMessage(120_000)).not.toMatch(/background-removal/i);
|
|
});
|
|
|
|
it("sets the CPU-vs-GPU expectation for heavy tools", () => {
|
|
const msg = timeoutMessage(120_000);
|
|
expect(msg).toMatch(/CPU/);
|
|
expect(msg).toMatch(/GPU/);
|
|
});
|
|
|
|
it("survives friendlyError (single line, at most 280 chars)", () => {
|
|
// friendlyError collapses any message over 280 chars or more than 3 lines
|
|
// to a generic "Processing failed" sentence, which would hide this guidance.
|
|
// Use the longest realistic timeout (JOB_TIMEOUT_LONG_S default of 2h).
|
|
const msg = timeoutMessage(7_200_000);
|
|
expect(msg.length).toBeLessThanOrEqual(280);
|
|
expect(msg.split("\n")).toHaveLength(1);
|
|
});
|
|
});
|