From f856e0b72f1b574cd561e35b1a50db2531c054fd Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 7 Apr 2026 12:01:27 +0100 Subject: [PATCH] refactor(plugins): annotate boundary failure metadata --- .../check-extension-package-tsc-boundary.mjs | 68 +++++++++++++++++-- ...eck-extension-package-tsc-boundary.test.ts | 23 ++++++- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index 1bc09674aef..e1704d0ea14 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -54,14 +54,32 @@ function summarizeOutputSection(name, output) { return `${name}:\n[... ${omittedLineCount} earlier lines omitted ...]\n${tail}`; } +function formatFailureFooter(params = {}) { + const footerLines = []; + if (params.kind) { + footerLines.push(`kind: ${params.kind}`); + } + if (Number.isFinite(params.elapsedMs)) { + footerLines.push(`elapsed: ${params.elapsedMs}ms`); + } + if (params.note) { + footerLines.push(params.note); + } + return footerLines.join("\n"); +} + export function formatStepFailure(label, params = {}) { const stdoutSection = summarizeOutputSection("stdout", params.stdout ?? ""); const stderrSection = summarizeOutputSection("stderr", params.stderr ?? ""); - return [label, stdoutSection, stderrSection, params.note ?? ""].filter(Boolean).join("\n\n"); + const footer = formatFailureFooter(params); + return [label, stdoutSection, stderrSection, footer].filter(Boolean).join("\n\n"); } function attachStepFailureMetadata(error, label, params = {}) { - error.fullOutput = [label, params.stdout ?? "", params.stderr ?? "", params.note ?? ""] + error.stepLabel = label; + error.kind = params.kind ?? "unknown"; + error.elapsedMs = params.elapsedMs ?? null; + error.fullOutput = [label, params.stdout ?? "", params.stderr ?? "", formatFailureFooter(params)] .filter(Boolean) .join("\n") .trim(); @@ -105,6 +123,7 @@ function collectCanaryExtensionIds(extensionIds) { } function runNodeStep(label, args, timeoutMs) { + const startedAt = Date.now(); const result = spawnSync(process.execPath, args, { cwd: repoRoot, encoding: "utf8", @@ -122,11 +141,15 @@ function runNodeStep(label, args, timeoutMs) { : ""; const errorSuffix = result.error ? result.error.message : ""; const note = [timeoutSuffix, errorSuffix].filter(Boolean).join("\n"); + const elapsedMs = Date.now() - startedAt; + const kind = timeoutSuffix ? "timeout" : result.error ? "spawn-error" : "nonzero-exit"; const failure = attachStepFailureMetadata( new Error( formatStepFailure(label, { stdout: result.stdout, stderr: result.stderr, + kind, + elapsedMs, note, }), ), @@ -134,6 +157,8 @@ function runNodeStep(label, args, timeoutMs) { { stdout: result.stdout, stderr: result.stderr, + kind, + elapsedMs, note, }, ); @@ -150,6 +175,7 @@ function abortSiblingSteps(abortController) { export function runNodeStepAsync(label, args, timeoutMs, params = {}) { const abortController = params.abortController; const onFailure = params.onFailure; + const startedAt = Date.now(); return new Promise((resolvePromise, rejectPromise) => { const child = spawn(process.execPath, args, { cwd: repoRoot, @@ -172,6 +198,8 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { formatStepFailure(label, { stdout, stderr, + kind: "timeout", + elapsedMs: Date.now() - startedAt, note: `${label} timed out after ${timeoutMs}ms`, }), ), @@ -179,6 +207,8 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { { stdout, stderr, + kind: "timeout", + elapsedMs: Date.now() - startedAt, note: `${label} timed out after ${timeoutMs}ms`, }, ); @@ -202,7 +232,13 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { clearTimeout(timer); settled = true; if (error.name === "AbortError" && abortController?.signal.aborted) { - rejectPromise(new Error(`${label} canceled after sibling failure`)); + rejectPromise( + attachStepFailureMetadata(new Error(`${label} canceled after sibling failure`), label, { + kind: "canceled", + elapsedMs: Date.now() - startedAt, + note: "canceled after sibling failure", + }), + ); return; } const failure = attachStepFailureMetadata( @@ -210,6 +246,8 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { formatStepFailure(label, { stdout, stderr, + kind: "spawn-error", + elapsedMs: Date.now() - startedAt, note: error.message, }), ), @@ -217,6 +255,8 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { { stdout, stderr, + kind: "spawn-error", + elapsedMs: Date.now() - startedAt, note: error.message, }, ); @@ -239,12 +279,16 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { formatStepFailure(label, { stdout, stderr, + kind: "nonzero-exit", + elapsedMs: Date.now() - startedAt, }), ), label, { stdout, stderr, + kind: "nonzero-exit", + elapsedMs: Date.now() - startedAt, }, ); onFailure?.(error); @@ -334,9 +378,21 @@ export function acquireBoundaryCheckLock(params = {}) { mkdirSync(lockPath); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") { - throw new Error( - `another extension package boundary check is already running in this checkout\n${lockPath}`, - { cause: error }, + throw attachStepFailureMetadata( + new Error( + [ + "extension package boundary check", + "kind: lock-contention", + `lock: ${lockPath}`, + "another extension package boundary check is already running in this checkout", + ].join("\n\n"), + { cause: error }, + ), + "extension package boundary check", + { + kind: "lock-contention", + note: `lock: ${lockPath}\nanother extension package boundary check is already running in this checkout`, + }, ); } throw error; diff --git a/test/scripts/check-extension-package-tsc-boundary.test.ts b/test/scripts/check-extension-package-tsc-boundary.test.ts index 01cf885cdd6..6e6ce0b6056 100644 --- a/test/scripts/check-extension-package-tsc-boundary.test.ts +++ b/test/scripts/check-extension-package-tsc-boundary.test.ts @@ -89,9 +89,20 @@ describe("check-extension-package-tsc-boundary", () => { const processObject = new EventEmitter(); const release = acquireBoundaryCheckLock({ processObject, rootDir }); - expect(() => acquireBoundaryCheckLock({ rootDir })).toThrow( - "another extension package boundary check is already running", - ); + let thrownError = null; + try { + acquireBoundaryCheckLock({ rootDir }); + } catch (error) { + thrownError = error; + } + + expect(thrownError).toMatchObject({ + message: expect.stringContaining("kind: lock-contention"), + fullOutput: expect.stringContaining( + "another extension package boundary check is already running", + ), + kind: "lock-contention", + }); release(); @@ -106,12 +117,16 @@ describe("check-extension-package-tsc-boundary", () => { const message = formatStepFailure("demo-plugin", { stdout, stderr, + kind: "timeout", + elapsedMs: 4_321, note: "demo-plugin timed out after 5000ms", }); const messageLines = message.split("\n"); expect(message).toContain("demo-plugin"); expect(message).toContain("[... 5 earlier lines omitted ...]"); + expect(message).toContain("kind: timeout"); + expect(message).toContain("elapsed: 4321ms"); expect(message).toContain("stdout 45"); expect(messageLines).not.toContain("stdout 1"); expect(message).toContain("stderr:\nstderr 1\nstderr 2\nstderr 3"); @@ -136,6 +151,8 @@ describe("check-extension-package-tsc-boundary", () => { ).rejects.toMatchObject({ message: expect.stringContaining("[... 6 earlier lines omitted ...]"), fullOutput: expect.stringContaining("src/cli/acp-cli.ts"), + kind: "nonzero-exit", + elapsedMs: expect.any(Number), }); });