From cfd7b73de4b4b1b6a7c47a76c92eb0e3537d2202 Mon Sep 17 00:00:00 2001 From: tdurieux Date: Thu, 8 Oct 2026 13:10:47 +0200 Subject: [PATCH] fix: send queue job payloads to the admin page as objects BullMQ's asJSON() stringifies data, failedReason, stacktrace and returnvalue. The Queues page reads them as objects, so payload repo IDs never showed, failure reasons kept their JSON quotes, and a stack trace rendered one block per character. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/server/routes/admin.ts | 11 ++++++++++- test/admin-queue-jobs.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 test/admin-queue-jobs.test.js diff --git a/src/server/routes/admin.ts b/src/server/routes/admin.ts index cb0be27..4babe89 100644 --- a/src/server/routes/admin.ts +++ b/src/server/routes/admin.ts @@ -381,7 +381,16 @@ router.get("/queues", dashboardCache, async (req, res) => { ...QUEUE_STATES.map(async (state) => { const jobs = await targetQueue.getJobs([state], 0, 199); return jobs.map((j) => { - const json: Record = { ...j.asJSON(), _state: state }; + // asJSON() serializes these fields to strings; the dashboard needs + // the objects (payload repoId, stack lines, failure reason). + const json: Record = { + ...j.asJSON(), + data: j.data, + failedReason: j.failedReason, + stacktrace: j.stacktrace, + returnvalue: j.returnvalue, + _state: state, + }; if (state === "delayed" && j.delay > 0) { json.delayUntil = j.timestamp + j.delay; } diff --git a/test/admin-queue-jobs.test.js b/test/admin-queue-jobs.test.js new file mode 100644 index 0000000..6df891e --- /dev/null +++ b/test/admin-queue-jobs.test.js @@ -0,0 +1,32 @@ +const { expect } = require("chai"); + +describe("admin queue jobs", function () { + it("returns job payloads, failure reasons and stacks as values, not JSON strings", async function () { + const queues = require("../src/queue"); + const metrics = require("../src/queue/queueMetrics"); + const router = require("../src/server/routes/admin").default; + const handler = router.stack.find(layer => layer.route?.path === "/queues").route.stack.at(-1).handle; + const originals = [queues.downloadQueue, queues.removeQueue, queues.cacheQueue, metrics.queryMetrics]; + const failed = { + data: { repoId: "paper-7F2A" }, failedReason: "branch_not_found", stacktrace: ["Error: branch_not_found\n at fetch"], returnvalue: null, + // BullMQ's asJSON() stringifies these four fields. + asJSON() { return { id: "paper-7F2A", name: "paper-7F2A", data: JSON.stringify(this.data), failedReason: JSON.stringify(this.failedReason), stacktrace: JSON.stringify(this.stacktrace), returnvalue: JSON.stringify(this.returnvalue) }; }, + }; + const queue = { getJobCounts: async () => ({}), getWorkers: async () => [], isPaused: async () => false, + getJobs: async ([state]) => state === "failed" ? [failed] : [] }; + queues.downloadQueue = queue; queues.removeQueue = queue; queues.cacheQueue = queue; + metrics.queryMetrics = async () => []; + try { + let body; + // The remove queue keeps this snapshot apart from other tests' cache entries. + await handler({ query: { queue: "remove" } }, { json: data => { body = data; } }); + const job = body.jobs.find(j => j.id === "paper-7F2A"); + expect(job.data).to.deep.equal({ repoId: "paper-7F2A" }); + expect(job.failedReason).to.equal("branch_not_found"); + expect(job.stacktrace).to.deep.equal(["Error: branch_not_found\n at fetch"]); + expect(job._state).to.equal("failed"); + } finally { + [queues.downloadQueue, queues.removeQueue, queues.cacheQueue, metrics.queryMetrics] = originals; + } + }); +});