From 4ea2cce1a07462185ada525513de39c90cb9e085 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 21:54:35 +0900 Subject: [PATCH 1/9] fix(viewer): rebuild stale-render suppression on protected main --- scripts/test_viewer_render_cancellation.py | 186 ++++++++++++++++++ .../resources/static/assets/viewer/viewer.js | 34 +++- 2 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 scripts/test_viewer_render_cancellation.py diff --git a/scripts/test_viewer_render_cancellation.py b/scripts/test_viewer_render_cancellation.py new file mode 100644 index 000000000..307f8a8f2 --- /dev/null +++ b/scripts/test_viewer_render_cancellation.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Exercise viewer PDF rendering cancellation in a real JavaScript runtime.""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +VIEWER_SOURCE = REPOSITORY_ROOT / "src/main/resources/static/assets/viewer/viewer.js" + +NODE_HARNESS = r""" +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const vm = require("node:vm"); + +function makeElement(tagName = "div") { + return { + tagName: tagName.toUpperCase(), + hidden: false, + disabled: false, + textContent: "", + className: "", + href: "", + target: "", + rel: "", + clientWidth: 800, + children: [], + style: {}, + attributes: new Map(), + setAttribute(name, value) { + this.attributes.set(name, String(value)); + }, + getAttribute(name) { + return this.attributes.get(name) ?? null; + }, + addEventListener() {}, + appendChild(child) { + this.children.push(child); + return child; + }, + querySelector() { + return null; + }, + querySelectorAll() { + return []; + }, + remove() {}, + focus() {}, + getContext() { + return {}; + }, + }; +} + +(async () => { + const elements = new Map(); + const ids = [ + "doc-meta", + "live-status", + "error", + "error-title", + "error-message", + "retry-btn", + "open-json-link", + "preview", + ]; + for (const id of ids) { + elements.set(id, makeElement()); + } + + const documentStub = { + getElementById(id) { + return elements.get(id) ?? null; + }, + querySelector() { + return null; + }, + createElement(tagName) { + return makeElement(tagName); + }, + }; + + const windowStub = { + location: { + origin: "https://viewer.example.test", + search: "", + }, + setTimeout() {}, + open() { + return null; + }, + }; + + const context = { + AbortController, + URL, + URLSearchParams, + console, + document: documentStub, + window: windowStub, + }; + context.globalThis = context; + vm.createContext(context); + + const viewerPath = process.argv[1]; + const viewerSource = fs.readFileSync(viewerPath, "utf8"); + vm.runInContext( + `${viewerSource}\n;globalThis.__viewerTest = { renderPdfInline };`, + context, + { filename: viewerPath }, + ); + + let resolveRender; + const renderPromise = new Promise(resolve => { + resolveRender = resolve; + }); + const pdfDocument = { + numPages: 1, + async getPage() { + return { + getViewport({ scale }) { + return { width: 100 * scale, height: 200 * scale }; + }, + render() { + return { promise: renderPromise }; + }, + }; + }, + async destroy() {}, + }; + context.__pdfJs = { + GlobalWorkerOptions: {}, + getDocument() { + return { promise: Promise.resolve(pdfDocument) }; + }, + }; + vm.runInContext( + "pdfJsModulePromise = Promise.resolve(globalThis.__pdfJs);", + context, + ); + + const controller = new AbortController(); + const rendering = context.__viewerTest.renderPdfInline( + "/artifacts/document.pdf", + controller.signal, + ); + + // Let getDocument/getPage reach the deliberately unresolved render promise. + await Promise.resolve(); + await Promise.resolve(); + controller.abort(); + resolveRender(); + await rendering; + + const preview = elements.get("preview"); + assert.equal( + preview.children.length, + 0, + "a superseded PDF render must not publish canvas or metadata", + ); +})().catch(error => { + console.error(error.stack || String(error)); + process.exitCode = 1; +}); +""" + + +def test_superseded_pdf_render_does_not_publish() -> None: + """Abort an in-flight render and require zero stale DOM publication.""" + + node = shutil.which("node") + assert node is not None, "Node.js is required for the viewer runtime regression" + + result = subprocess.run( + [node, "-e", NODE_HARNESS, str(VIEWER_SOURCE)], + cwd=REPOSITORY_ROOT, + capture_output=True, + check=False, + text=True, + timeout=15, + ) + + assert result.returncode == 0, result.stdout + result.stderr diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index a998675c7..33ec05397 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -131,20 +131,36 @@ function getPdfJsModule() { return pdfJsModulePromise; } -async function renderPdfInline(path) { +async function renderPdfInline(path, abortSignal) { const resolvedPath = resolveSameOriginHttpUrl(path); if (resolvedPath === null) { throw new Error("PDF preview resource is invalid"); } + if (abortSignal?.aborted) { + return false; + } + const pdfJs = await getPdfJsModule(); + if (abortSignal?.aborted) { + return false; + } + const loadingTask = pdfJs.getDocument({ url: resolvedPath, withCredentials: true, }); const pdfDocument = await loadingTask.promise; try { + if (abortSignal?.aborted) { + return false; + } + const page = await pdfDocument.getPage(1); + if (abortSignal?.aborted) { + return false; + } + const unscaledViewport = page.getViewport({ scale: 1 }); const availableWidth = Math.max(320, el.preview.clientWidth - 32); const renderScale = Math.min(2, availableWidth / unscaledViewport.width); @@ -164,6 +180,10 @@ async function renderPdfInline(path) { canvas.setAttribute("aria-label", `Rendered first page of a ${pdfDocument.numPages}-page PDF`); await page.render({ canvasContext: context, viewport }).promise; + if (abortSignal?.aborted) { + return false; + } + el.preview.appendChild(canvas); const metadata = document.createElement("p"); @@ -172,6 +192,7 @@ async function renderPdfInline(path) { ? "Showing the document page." : `Showing page 1 of ${pdfDocument.numPages}. Open the artifact for the complete document.`; el.preview.appendChild(metadata); + return true; } finally { await pdfDocument.destroy(); } @@ -265,12 +286,19 @@ async function poll(docId, abortSignal) { clearPreview(); const path = bootstrap.data.previewResourcePath; if (typeof path === "string" && path.endsWith(".pdf")) { - await renderPdfInline(path); + await renderPdfInline(path, abortSignal); + if (abortSignal.aborted) { + return; + } } if (typeof path === "string" && path.length > 0) { renderPreviewLink(path); } + if (abortSignal.aborted) { + return; + } + el.preview.setAttribute("aria-busy", "false"); el.liveStatus.textContent = "Ready."; el.retryBtn.disabled = false; @@ -348,4 +376,4 @@ async function init() { start(); } -void init(); +void init(); \ No newline at end of file From 6c8c0a0ca2b61b0d2d1aaf368d1d6548a571683f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 22:46:51 +0900 Subject: [PATCH 2/9] test(viewer): require active PDF.js cancellation --- scripts/test_viewer_render_cancellation.py | 118 +++++++++++++++++++-- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/scripts/test_viewer_render_cancellation.py b/scripts/test_viewer_render_cancellation.py index 307f8a8f2..c3c6f5f8a 100644 --- a/scripts/test_viewer_render_cancellation.py +++ b/scripts/test_viewer_render_cancellation.py @@ -55,7 +55,7 @@ }; } -(async () => { +function makeHarness() { const elements = new Map(); const ids = [ "doc-meta", @@ -113,7 +113,20 @@ { filename: viewerPath }, ); + return { context, elements }; +} + +async function flushMicrotasks(turns = 8) { + for (let index = 0; index < turns; index += 1) { + await Promise.resolve(); + } +} + +async function activeRenderCancellationIsPropagated() { + const { context, elements } = makeHarness(); let resolveRender; + let renderCancelCalls = 0; + let documentDestroyCalls = 0; const renderPromise = new Promise(resolve => { resolveRender = resolve; }); @@ -125,11 +138,19 @@ return { width: 100 * scale, height: 200 * scale }; }, render() { - return { promise: renderPromise }; + return { + promise: renderPromise, + cancel() { + renderCancelCalls += 1; + resolveRender(); + }, + }; }, }; }, - async destroy() {}, + async destroy() { + documentDestroyCalls += 1; + }, }; context.__pdfJs = { GlobalWorkerOptions: {}, @@ -148,19 +169,94 @@ controller.signal, ); - // Let getDocument/getPage reach the deliberately unresolved render promise. - await Promise.resolve(); - await Promise.resolve(); + await flushMicrotasks(); controller.abort(); - resolveRender(); + await flushMicrotasks(); + const cancelCallsObservedOnAbort = renderCancelCalls; + if (renderCancelCalls === 0) { + resolveRender(); + } await rendering; - const preview = elements.get("preview"); assert.equal( - preview.children.length, + cancelCallsObservedOnAbort, + 1, + "aborting an active PDF render must call RenderTask.cancel() immediately", + ); + assert.equal(documentDestroyCalls, 1, "the loaded PDF document must be destroyed exactly once"); + assert.equal( + elements.get("preview").children.length, 0, "a superseded PDF render must not publish canvas or metadata", ); +} + +async function loadingCancellationIsPropagated() { + const { context, elements } = makeHarness(); + let resolveLoading; + let loadingDestroyCalls = 0; + let documentDestroyCalls = 0; + const pdfDocument = { + numPages: 1, + async getPage() { + throw new Error("aborted loading must not request a page"); + }, + async destroy() { + documentDestroyCalls += 1; + }, + }; + const loadingPromise = new Promise(resolve => { + resolveLoading = resolve; + }); + const loadingTask = { + promise: loadingPromise, + async destroy() { + loadingDestroyCalls += 1; + resolveLoading(pdfDocument); + }, + }; + context.__pdfJs = { + GlobalWorkerOptions: {}, + getDocument() { + return loadingTask; + }, + }; + vm.runInContext( + "pdfJsModulePromise = Promise.resolve(globalThis.__pdfJs);", + context, + ); + + const controller = new AbortController(); + const rendering = context.__viewerTest.renderPdfInline( + "/artifacts/document.pdf", + controller.signal, + ); + + await flushMicrotasks(); + controller.abort(); + await flushMicrotasks(); + const destroyCallsObservedOnAbort = loadingDestroyCalls; + if (loadingDestroyCalls === 0) { + resolveLoading(pdfDocument); + } + await rendering; + + assert.equal( + destroyCallsObservedOnAbort, + 1, + "aborting PDF loading must call PDFDocumentLoadingTask.destroy() immediately", + ); + assert.equal(documentDestroyCalls, 1, "a loaded proxy produced during cancellation must be destroyed"); + assert.equal( + elements.get("preview").children.length, + 0, + "an aborted loading task must not publish preview DOM", + ); +} + +(async () => { + await activeRenderCancellationIsPropagated(); + await loadingCancellationIsPropagated(); })().catch(error => { console.error(error.stack || String(error)); process.exitCode = 1; @@ -168,8 +264,8 @@ """ -def test_superseded_pdf_render_does_not_publish() -> None: - """Abort an in-flight render and require zero stale DOM publication.""" +def test_pdfjs_tasks_receive_abort_cancellation() -> None: + """Abort in-flight PDF.js work and require active resource cancellation.""" node = shutil.which("node") assert node is not None, "Node.js is required for the viewer runtime regression" From e77fd2e55638eb497008fe63d11f1473fdf09224 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 22:48:54 +0900 Subject: [PATCH 3/9] fix(viewer): cancel superseded PDF.js tasks --- .../resources/static/assets/viewer/viewer.js | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index 33ec05397..57c764f31 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -150,7 +150,28 @@ async function renderPdfInline(path, abortSignal) { url: resolvedPath, withCredentials: true, }); - const pdfDocument = await loadingTask.promise; + const cancelLoading = () => { + if (typeof loadingTask.destroy === "function") { + void Promise.resolve(loadingTask.destroy()).catch(() => {}); + } + }; + abortSignal?.addEventListener("abort", cancelLoading, { once: true }); + if (abortSignal?.aborted) { + cancelLoading(); + } + + let pdfDocument; + try { + pdfDocument = await loadingTask.promise; + } catch (error) { + if (abortSignal?.aborted) { + return false; + } + throw error; + } finally { + abortSignal?.removeEventListener("abort", cancelLoading); + } + try { if (abortSignal?.aborted) { return false; @@ -179,7 +200,30 @@ async function renderPdfInline(path, abortSignal) { canvas.setAttribute("role", "img"); canvas.setAttribute("aria-label", `Rendered first page of a ${pdfDocument.numPages}-page PDF`); - await page.render({ canvasContext: context, viewport }).promise; + const renderTask = page.render({ canvasContext: context, viewport }); + const cancelRender = () => { + if (typeof renderTask.cancel === "function") { + try { + renderTask.cancel(); + } catch (_error) { + // Cancellation is best-effort; the abort fence below still blocks stale publication. + } + } + }; + abortSignal?.addEventListener("abort", cancelRender, { once: true }); + if (abortSignal?.aborted) { + cancelRender(); + } + try { + await renderTask.promise; + } catch (error) { + if (abortSignal?.aborted) { + return false; + } + throw error; + } finally { + abortSignal?.removeEventListener("abort", cancelRender); + } if (abortSignal?.aborted) { return false; } From d34bde9a32efaaaf322c22942a5cb86040077ec0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 22:50:22 +0900 Subject: [PATCH 4/9] revert(viewer): preserve bounded #323 scope --- .../resources/static/assets/viewer/viewer.js | 48 +------------------ 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index 57c764f31..33ec05397 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -150,28 +150,7 @@ async function renderPdfInline(path, abortSignal) { url: resolvedPath, withCredentials: true, }); - const cancelLoading = () => { - if (typeof loadingTask.destroy === "function") { - void Promise.resolve(loadingTask.destroy()).catch(() => {}); - } - }; - abortSignal?.addEventListener("abort", cancelLoading, { once: true }); - if (abortSignal?.aborted) { - cancelLoading(); - } - - let pdfDocument; - try { - pdfDocument = await loadingTask.promise; - } catch (error) { - if (abortSignal?.aborted) { - return false; - } - throw error; - } finally { - abortSignal?.removeEventListener("abort", cancelLoading); - } - + const pdfDocument = await loadingTask.promise; try { if (abortSignal?.aborted) { return false; @@ -200,30 +179,7 @@ async function renderPdfInline(path, abortSignal) { canvas.setAttribute("role", "img"); canvas.setAttribute("aria-label", `Rendered first page of a ${pdfDocument.numPages}-page PDF`); - const renderTask = page.render({ canvasContext: context, viewport }); - const cancelRender = () => { - if (typeof renderTask.cancel === "function") { - try { - renderTask.cancel(); - } catch (_error) { - // Cancellation is best-effort; the abort fence below still blocks stale publication. - } - } - }; - abortSignal?.addEventListener("abort", cancelRender, { once: true }); - if (abortSignal?.aborted) { - cancelRender(); - } - try { - await renderTask.promise; - } catch (error) { - if (abortSignal?.aborted) { - return false; - } - throw error; - } finally { - abortSignal?.removeEventListener("abort", cancelRender); - } + await page.render({ canvasContext: context, viewport }).promise; if (abortSignal?.aborted) { return false; } From a616a3177bcabe29bc576f3cfa64a90476a58e1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 22:50:58 +0900 Subject: [PATCH 5/9] revert(test): preserve bounded #323 acceptance --- scripts/test_viewer_render_cancellation.py | 120 +++------------------ 1 file changed, 12 insertions(+), 108 deletions(-) diff --git a/scripts/test_viewer_render_cancellation.py b/scripts/test_viewer_render_cancellation.py index c3c6f5f8a..5fd420ce2 100644 --- a/scripts/test_viewer_render_cancellation.py +++ b/scripts/test_viewer_render_cancellation.py @@ -55,7 +55,7 @@ }; } -function makeHarness() { +(async () => { const elements = new Map(); const ids = [ "doc-meta", @@ -113,20 +113,7 @@ { filename: viewerPath }, ); - return { context, elements }; -} - -async function flushMicrotasks(turns = 8) { - for (let index = 0; index < turns; index += 1) { - await Promise.resolve(); - } -} - -async function activeRenderCancellationIsPropagated() { - const { context, elements } = makeHarness(); let resolveRender; - let renderCancelCalls = 0; - let documentDestroyCalls = 0; const renderPromise = new Promise(resolve => { resolveRender = resolve; }); @@ -138,19 +125,11 @@ return { width: 100 * scale, height: 200 * scale }; }, render() { - return { - promise: renderPromise, - cancel() { - renderCancelCalls += 1; - resolveRender(); - }, - }; + return { promise: renderPromise }; }, }; }, - async destroy() { - documentDestroyCalls += 1; - }, + async destroy() {}, }; context.__pdfJs = { GlobalWorkerOptions: {}, @@ -169,94 +148,19 @@ controller.signal, ); - await flushMicrotasks(); + // Let getDocument/getPage reach the deliberately unresolved render promise. + await Promise.resolve(); + await Promise.resolve(); controller.abort(); - await flushMicrotasks(); - const cancelCallsObservedOnAbort = renderCancelCalls; - if (renderCancelCalls === 0) { - resolveRender(); - } + resolveRender(); await rendering; + const preview = elements.get("preview"); assert.equal( - cancelCallsObservedOnAbort, - 1, - "aborting an active PDF render must call RenderTask.cancel() immediately", - ); - assert.equal(documentDestroyCalls, 1, "the loaded PDF document must be destroyed exactly once"); - assert.equal( - elements.get("preview").children.length, + preview.children.length, 0, "a superseded PDF render must not publish canvas or metadata", ); -} - -async function loadingCancellationIsPropagated() { - const { context, elements } = makeHarness(); - let resolveLoading; - let loadingDestroyCalls = 0; - let documentDestroyCalls = 0; - const pdfDocument = { - numPages: 1, - async getPage() { - throw new Error("aborted loading must not request a page"); - }, - async destroy() { - documentDestroyCalls += 1; - }, - }; - const loadingPromise = new Promise(resolve => { - resolveLoading = resolve; - }); - const loadingTask = { - promise: loadingPromise, - async destroy() { - loadingDestroyCalls += 1; - resolveLoading(pdfDocument); - }, - }; - context.__pdfJs = { - GlobalWorkerOptions: {}, - getDocument() { - return loadingTask; - }, - }; - vm.runInContext( - "pdfJsModulePromise = Promise.resolve(globalThis.__pdfJs);", - context, - ); - - const controller = new AbortController(); - const rendering = context.__viewerTest.renderPdfInline( - "/artifacts/document.pdf", - controller.signal, - ); - - await flushMicrotasks(); - controller.abort(); - await flushMicrotasks(); - const destroyCallsObservedOnAbort = loadingDestroyCalls; - if (loadingDestroyCalls === 0) { - resolveLoading(pdfDocument); - } - await rendering; - - assert.equal( - destroyCallsObservedOnAbort, - 1, - "aborting PDF loading must call PDFDocumentLoadingTask.destroy() immediately", - ); - assert.equal(documentDestroyCalls, 1, "a loaded proxy produced during cancellation must be destroyed"); - assert.equal( - elements.get("preview").children.length, - 0, - "an aborted loading task must not publish preview DOM", - ); -} - -(async () => { - await activeRenderCancellationIsPropagated(); - await loadingCancellationIsPropagated(); })().catch(error => { console.error(error.stack || String(error)); process.exitCode = 1; @@ -264,8 +168,8 @@ """ -def test_pdfjs_tasks_receive_abort_cancellation() -> None: - """Abort in-flight PDF.js work and require active resource cancellation.""" +def test_superseded_pdf_render_does_not_publish() -> None: + """Abort an in-flight render and require zero stale DOM publication.""" node = shutil.which("node") assert node is not None, "Node.js is required for the viewer runtime regression" @@ -279,4 +183,4 @@ def test_pdfjs_tasks_receive_abort_cancellation() -> None: timeout=15, ) - assert result.returncode == 0, result.stdout + result.stderr + assert result.returncode == 0, result.stdout + result.stderr \ No newline at end of file From 8914580339c644f2cfdfef6a1634dfd5cff53693 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 22:51:43 +0900 Subject: [PATCH 6/9] chore(viewer): restore exact bounded test blob --- scripts/test_viewer_render_cancellation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_viewer_render_cancellation.py b/scripts/test_viewer_render_cancellation.py index 5fd420ce2..307f8a8f2 100644 --- a/scripts/test_viewer_render_cancellation.py +++ b/scripts/test_viewer_render_cancellation.py @@ -183,4 +183,4 @@ def test_superseded_pdf_render_does_not_publish() -> None: timeout=15, ) - assert result.returncode == 0, result.stdout + result.stderr \ No newline at end of file + assert result.returncode == 0, result.stdout + result.stderr From 89919e76490eeafd4440b0d1a72cb1e761e4c104 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 03:21:17 +0900 Subject: [PATCH 7/9] test(viewer): make cancellation regression deterministic --- scripts/test_viewer_render_cancellation.py | 51 ++++++++++++++-------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/scripts/test_viewer_render_cancellation.py b/scripts/test_viewer_render_cancellation.py index 307f8a8f2..570899d17 100644 --- a/scripts/test_viewer_render_cancellation.py +++ b/scripts/test_viewer_render_cancellation.py @@ -5,6 +5,7 @@ import shutil import subprocess +import unittest from pathlib import Path @@ -113,6 +114,10 @@ { filename: viewerPath }, ); + let renderStarted; + const renderStartedPromise = new Promise(resolve => { + renderStarted = resolve; + }); let resolveRender; const renderPromise = new Promise(resolve => { resolveRender = resolve; @@ -125,6 +130,7 @@ return { width: 100 * scale, height: 200 * scale }; }, render() { + renderStarted(); return { promise: renderPromise }; }, }; @@ -148,12 +154,15 @@ controller.signal, ); - // Let getDocument/getPage reach the deliberately unresolved render promise. - await Promise.resolve(); - await Promise.resolve(); + // Wait until page.render() starts so the post-render abort boundary is deterministic. + await renderStartedPromise; controller.abort(); resolveRender(); - await rendering; + assert.equal( + await rendering, + false, + "an aborted render must report failure", + ); const preview = elements.get("preview"); assert.equal( @@ -168,19 +177,27 @@ """ -def test_superseded_pdf_render_does_not_publish() -> None: - """Abort an in-flight render and require zero stale DOM publication.""" +class ViewerRenderCancellationTest(unittest.TestCase): + """Validate PDF render supersession behavior through the Node.js harness.""" + + def test_superseded_pdf_render_does_not_publish(self) -> None: + """Abort an in-flight render and require zero stale DOM publication.""" + + node = shutil.which("node") + self.assertIsNotNone(node, "Node.js is required for the viewer runtime regression") + assert node is not None + + result = subprocess.run( + [node, "-e", NODE_HARNESS, str(VIEWER_SOURCE)], + cwd=REPOSITORY_ROOT, + capture_output=True, + check=False, + text=True, + timeout=15, + ) - node = shutil.which("node") - assert node is not None, "Node.js is required for the viewer runtime regression" + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = subprocess.run( - [node, "-e", NODE_HARNESS, str(VIEWER_SOURCE)], - cwd=REPOSITORY_ROOT, - capture_output=True, - check=False, - text=True, - timeout=15, - ) - assert result.returncode == 0, result.stdout + result.stderr +if __name__ == "__main__": + unittest.main() From d7a5e2c4848a02800c6784b67787a97cbca8b070 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 2 Oct 2026 07:00:10 +0900 Subject: [PATCH 8/9] test(worker): await terminal retry publication --- .../clearfolio/viewer/service/DefaultConversionWorkerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java index 88503b486..378c4a2ce 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java @@ -392,7 +392,7 @@ void workerReschedulesWhenRetryAtIsInTheFuture() throws Exception { worker.enqueue(jobId); - await(() -> attempts.get() >= 1, 3_000); + await(() -> attempts.get() >= 1 && job.getStatus() == ConversionJobStatus.SUCCEEDED, 3_000); assertEquals(1, attempts.get()); assertEquals(1, job.getAttemptCount()); From 2fc4ec4993dea5a7beadef8a2e87433201d4956a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 2 Oct 2026 07:01:17 +0900 Subject: [PATCH 9/9] revert: keep worker retry test on canonical owner --- .../clearfolio/viewer/service/DefaultConversionWorkerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java index 378c4a2ce..88503b486 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultConversionWorkerTest.java @@ -392,7 +392,7 @@ void workerReschedulesWhenRetryAtIsInTheFuture() throws Exception { worker.enqueue(jobId); - await(() -> attempts.get() >= 1 && job.getStatus() == ConversionJobStatus.SUCCEEDED, 3_000); + await(() -> attempts.get() >= 1, 3_000); assertEquals(1, attempts.get()); assertEquals(1, job.getAttemptCount());