From ed147f6470823291343d014f26885639286a7e95 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Sun, 28 Jun 2026 11:59:52 +0200 Subject: [PATCH] extract test output parsing Parsing no longer depends on ui widget --- pi-test-status/extensions/index.ts | 27 ++------- .../parsing/__tests__/vitest-output.test.ts | 60 +++++++++++++++++++ .../parsing/typescript-command-output.ts | 26 +++++++- vitest.config.ts | 10 ++++ 4 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 pi-test-status/extensions/parsing/__tests__/vitest-output.test.ts create mode 100644 vitest.config.ts diff --git a/pi-test-status/extensions/index.ts b/pi-test-status/extensions/index.ts index 9556bc8..39e2166 100644 --- a/pi-test-status/extensions/index.ts +++ b/pi-test-status/extensions/index.ts @@ -1,5 +1,5 @@ import {type ExtensionAPI, isBashToolResult} from "@earendil-works/pi-coding-agent"; -import {isTestCommand} from "./parsing/typescript-command-output"; +import {isTestCommand, parseTestOutput} from "./parsing/typescript-command-output"; // noinspection JSUnusedGlobalSymbols export default function testStatusExtension(pi: ExtensionAPI) { @@ -13,30 +13,15 @@ export default function testStatusExtension(pi: ExtensionAPI) { const output = textContent?.text ?? ""; const theme = ctx.ui.theme; - // Detect pass/fail from vitest summary output - const testsPassed = - /Tests\s+.+passed/.test(output) && !/Tests\s+.+failed/.test(output); - const testsFailed = - /Tests\s+.+failed/.test(output) || event.isError; - - if (testsPassed) { - // Extract summary line for inline detail (e.g., "1 passed (242ms)") - const summaryLine = output - .split("\n") - .find((line: string) => /Test Files\s+.+passed/.test(line)); - const summary = summaryLine?.trim() ?? "All tests passed"; + const testResult = parseTestOutput(output); + if (testResult.status === "Success") { ctx.ui.setWidget("test-status", [ - `${theme.fg("success", "●")} ${theme.fg("text", summary)}`, + `${theme.fg("success", "●")} ${theme.fg("text", testResult.summary)}`, ]); - } else if (testsFailed) { - const summaryLine = output - .split("\n") - .find((line: string) => /Tests\s+.+failed/.test(line)); - const summary = summaryLine?.trim() ?? "Tests failed"; - + } else if (testResult.status === "Error" || event.isError) { ctx.ui.setWidget("test-status", [ - `${theme.fg("error", "●")} ${theme.fg("text", summary)}`, + `${theme.fg("error", "●")} ${theme.fg("text", testResult.summary)}`, ]); } }); diff --git a/pi-test-status/extensions/parsing/__tests__/vitest-output.test.ts b/pi-test-status/extensions/parsing/__tests__/vitest-output.test.ts new file mode 100644 index 0000000..40ae035 --- /dev/null +++ b/pi-test-status/extensions/parsing/__tests__/vitest-output.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { parseTestOutput } from "../typescript-command-output"; + +describe("parseTestOutput", () => { + it("parses a passing vitest run", () => { + const output = ` + RAN v3.2.6 /path + + ✓ src/greet.test.ts (1 test) 1ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at 11:12:54 + Duration 308ms +`; + + const result = parseTestOutput(output); + + expect(result.status).toBe("Success"); + expect(result.summary).toContain("1 passed"); + }); + + it("parses a failing vitest run", () => { + const output = ` + RAN v3.2.6 /path + + ✓ src/greet.test.ts (1 test) 1ms + ✗ src/foo.test.ts (1 test) 2ms + + Test Files 1 failed (1) + Tests 1 failed (1) + Start at 11:15:00 + Duration 400ms +`; + + const result = parseTestOutput(output); + + expect(result.status).toBe("Error"); + expect(result.summary).toContain("1 failed"); + }); + + it("returns Error and a fallback summary for unrecognised output", () => { + const result = parseTestOutput("some random output"); + + expect(result.status).toBe("Error"); + expect(result.summary).toBe("Tests failed"); + }); + + it("returns Success with a fallback summary when passed but no detail line", () => { + const output = ` + ✓ src/greet.test.ts (1 test) 1ms + Tests 1 passed (242ms) +`; + + const result = parseTestOutput(output); + + expect(result.status).toBe("Success"); + expect(result.summary).toBe("All tests passed"); + }); +}); diff --git a/pi-test-status/extensions/parsing/typescript-command-output.ts b/pi-test-status/extensions/parsing/typescript-command-output.ts index 017050e..1fc94bc 100644 --- a/pi-test-status/extensions/parsing/typescript-command-output.ts +++ b/pi-test-status/extensions/parsing/typescript-command-output.ts @@ -1,3 +1,25 @@ +export type TestResult = + | { status: "Success"; summary: string } + | { status: "Error"; summary: string }; + export const isTestCommand = (command: string) => { - return command.includes("npm test") || command.includes("vitest") -} \ No newline at end of file + return command.includes("npm test") || command.includes("vitest"); +}; + +export function parseTestOutput(output: string): TestResult { + const testsPassed = + /Tests\s+.+passed/.test(output) && !/Tests\s+.+failed/.test(output); + const testsFailed = /Tests\s+.+failed/.test(output); + + if (testsPassed && !testsFailed) { + const line = output + .split("\n") + .find((l) => /Test Files\s+.+passed/.test(l)); + return { status: "Success", summary: line?.trim() ?? "All tests passed" }; + } + + const line = output + .split("\n") + .find((l) => /Tests\s+.+failed/.test(l)); + return { status: "Error", summary: line?.trim() ?? "Tests failed" }; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d898be4 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: [ + "src/**/*.test.ts", + "pi-test-status/extensions/**/*.test.ts", + ], + }, +});