diff --git a/apps/api/src/jobs/batch-progress.ts b/apps/api/src/jobs/batch-progress.ts index ee1fc8d5..6af1eb28 100644 --- a/apps/api/src/jobs/batch-progress.ts +++ b/apps/api/src/jobs/batch-progress.ts @@ -12,6 +12,7 @@ * "failed" only when every file failed */ +import { stripInternalPaths } from "../lib/errors.js"; import { updateJobProgress } from "../routes/progress.js"; import { sharedRedis } from "./connection.js"; import { bullPrefix } from "./types.js"; @@ -37,7 +38,8 @@ export async function recordChildOutcome( const base = `${bullPrefix()}:batch:${parentId}`; const done = await r.incr(`${base}:${error ? "failed" : "done"}`); const other = Number((await r.get(`${base}:${error ? "done" : "failed"}`)) ?? 0); - if (error) await r.rpush(`${base}:errors`, JSON.stringify({ filename, error })); + if (error) + await r.rpush(`${base}:errors`, JSON.stringify({ filename, error: stripInternalPaths(error) })); await r.expire(`${base}:done`, 3600); await r.expire(`${base}:failed`, 3600); await r.expire(`${base}:errors`, 3600); diff --git a/apps/api/src/jobs/worker.ts b/apps/api/src/jobs/worker.ts index ba7fafd6..a9a5ab76 100644 --- a/apps/api/src/jobs/worker.ts +++ b/apps/api/src/jobs/worker.ts @@ -387,10 +387,11 @@ async function processPipelineStep(job: Job): Promise): Promise): Promise): Promise await recordChildOutcome(job.data.parentId!, job.data.totalFiles!, job.data.filename); return result; } catch (err) { - const error = err instanceof Error ? err.message : String(err); + const error = stripInternalPaths(err instanceof Error ? err.message : String(err)); await recordChildOutcome(job.data.parentId!, job.data.totalFiles!, job.data.filename, error); // Return a completed job with a failure marker so the parent runs. return { @@ -647,7 +648,7 @@ async function processBatchFinalize(job: Job): Promise { it("formats single issue with path", () => { @@ -34,3 +34,58 @@ describe("formatZodErrors", () => { expect(result).toBe("settings.quality: Too high"); }); }); + +describe("stripInternalPaths", () => { + it("strips /tmp scratch paths", () => { + const msg = "ENOENT: no such file or directory, open '/tmp/workspace/abc123/input.png'"; + expect(stripInternalPaths(msg)).toBe("ENOENT: no such file or directory, open '[internal]'"); + }); + + it("strips /data/ai/venv Python traceback paths", () => { + const msg = 'File "/data/ai/venv/lib/python3.11/site-packages/rembg/bg.py", line 42, in remove'; + expect(stripInternalPaths(msg)).toBe('File "[internal]", line 42, in remove'); + }); + + it("strips /app container paths", () => { + const msg = "Error loading model from /app/models/realesrgan-x4.pth"; + expect(stripInternalPaths(msg)).toBe("Error loading model from [internal]"); + }); + + it("strips /opt paths", () => { + const msg = "Cannot find /opt/libreoffice/program/soffice"; + expect(stripInternalPaths(msg)).toBe("Cannot find [internal]"); + }); + + it("strips /home paths", () => { + const msg = "Permission denied: /home/node/.cache/sharp"; + expect(stripInternalPaths(msg)).toBe("Permission denied: [internal]"); + }); + + it("strips /workspace paths", () => { + const msg = "failed at /workspace/snapotter/packages/image-engine/src/convert.ts:42"; + expect(stripInternalPaths(msg)).toBe("failed at [internal]"); + }); + + it("strips multiple paths in one message", () => { + const msg = "cp: /tmp/scratch/a.png -> /data/output/b.png failed"; + expect(stripInternalPaths(msg)).toBe("cp: [internal] -> [internal] failed"); + }); + + it("leaves safe messages untouched", () => { + const msg = "Invalid image format: expected PNG or JPEG"; + expect(stripInternalPaths(msg)).toBe(msg); + }); + + it("leaves timeout messages untouched", () => { + const msg = "Timed out after 120s"; + expect(stripInternalPaths(msg)).toBe(msg); + }); + + it("handles pipeline step error wrapping", () => { + const msg = "Step 2: Command failed: /tmp/workspace/job123/ffmpeg -i /data/input.mp4"; + const result = stripInternalPaths(msg); + expect(result).not.toContain("/tmp/"); + expect(result).not.toContain("/data/"); + expect(result).toBe("Step 2: Command failed: [internal] -i [internal]"); + }); +});