From 0c2dc682b691c827ed23236612fc0fbc0b31f854 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:28:52 +0000 Subject: [PATCH 1/4] Initial plan From 23cd3cd517e410efaa7bdfad6a4c65028cba0f73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:36:20 +0000 Subject: [PATCH 2/4] feat: add color differentiation for actual/expected values in test failures - Parse @hapi/lab failure output to extract actual and expected values - Set TestMessage actualOutput and expectedOutput properties to enable VS Code's diff view - Add comprehensive tests for parseErrorMessage function - Export parseErrorMessage for testing purposes Co-authored-by: mtharrison <916064+mtharrison@users.noreply.github.com> --- src/testRunner.ts | 85 +++++++++++++++++-- test/testRunner.test.ts | 179 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 test/testRunner.test.ts diff --git a/src/testRunner.ts b/src/testRunner.ts index 304c703..6a9aac9 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -42,6 +42,19 @@ export interface TestResult { output?: string; } +/** + * Parsed failure information from lab output. + * + * @property message - The full error message + * @property actualOutput - The actual output value (for diff view) + * @property expectedOutput - The expected output value (for diff view) + */ +interface FailureInfo { + message: string; + actualOutput?: string; + expectedOutput?: string; +} + /** * Executes a single @hapi/lab test case or describe block. * @@ -204,19 +217,40 @@ export async function runLabTest( } } } else { - const message = - parseErrorMessage(output + errorOutput) || "Test failed"; - run.failed(testItem, new vscode.TestMessage(message), duration); + const failureInfo = + parseErrorMessage(output + errorOutput) || { message: "Test failed" }; + + const testMessage = new vscode.TestMessage(failureInfo.message); + + // If we have actual/expected values, set them to enable VS Code's diff view + if (failureInfo.actualOutput && failureInfo.expectedOutput) { + testMessage.actualOutput = failureInfo.actualOutput; + testMessage.expectedOutput = failureInfo.expectedOutput; + } + + run.failed(testItem, testMessage, duration); + // Mark failed descendants detected in real-time with actual error message for (const [descendant, descendantDuration] of failedDescendants) { markedDescendants.add(descendant); - run.failed(descendant, new vscode.TestMessage(message), descendantDuration); + const descendantMessage = new vscode.TestMessage(failureInfo.message); + if (failureInfo.actualOutput && failureInfo.expectedOutput) { + descendantMessage.actualOutput = failureInfo.actualOutput; + descendantMessage.expectedOutput = failureInfo.expectedOutput; + } + run.failed(descendant, descendantMessage, descendantDuration); } + // Mark remaining descendants as failed if (descendants) { for (const descendant of descendants) { if (!markedDescendants.has(descendant)) { - run.failed(descendant, new vscode.TestMessage(message), duration); + const descendantMessage = new vscode.TestMessage(failureInfo.message); + if (failureInfo.actualOutput && failureInfo.expectedOutput) { + descendantMessage.actualOutput = failureInfo.actualOutput; + descendantMessage.expectedOutput = failureInfo.expectedOutput; + } + run.failed(descendant, descendantMessage, duration); } } } @@ -240,7 +274,21 @@ export async function runLabTest( */ const FAILURE_START_PATTERN = /^\s*\d+\)\s+.+:/; -function parseErrorMessage(output: string): string | undefined { +/** + * Pattern to extract actual and expected values from lab's error messages. + * Matches: "Expected to equal specified value: " + * Captures: [1] actual value (quoted or unquoted), [2] expected value (quoted or unquoted) + */ +const EXPECTED_PATTERN = /Expected (.+?) to equal specified value:\s*(.+?)(?:\s*at\s|$)/s; + +/** + * Parses lab's failure output to extract error message and actual/expected values. + * Exported for testing purposes. + * + * @param output - Raw test output from lab + * @returns Parsed failure information with message and optional actual/expected values + */ +export function parseErrorMessage(output: string): FailureInfo | undefined { // Strip ANSI escape codes for cleaner parsing const cleanOutput = output.replace(ANSI_ESCAPE_PATTERN, ""); const lines = cleanOutput.split("\n"); @@ -272,5 +320,28 @@ function parseErrorMessage(output: string): string | undefined { errorLines.pop(); } - return errorLines.length > 0 ? errorLines.join("\n") : undefined; + if (errorLines.length === 0) { + return undefined; + } + + const message = errorLines.join("\n"); + + // Try to extract actual and expected values for assertion failures + const match = EXPECTED_PATTERN.exec(message); + if (match) { + const [, actualStr, expectedStr] = match; + + // Clean up the values by removing quotes and trimming + const cleanValue = (val: string): string => { + return val.trim().replace(/^['"]|['"]$/g, ""); + }; + + return { + message, + actualOutput: cleanValue(actualStr), + expectedOutput: cleanValue(expectedStr), + }; + } + + return { message }; } diff --git a/test/testRunner.test.ts b/test/testRunner.test.ts new file mode 100644 index 0000000..845c14d --- /dev/null +++ b/test/testRunner.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect } from 'vitest'; +import { parseErrorMessage } from '../src/testRunner'; + +describe('testRunner', () => { + describe('parseErrorMessage', () => { + it('should parse simple string comparison failure', () => { + const output = ` +Failed tests: + + 1) Example test should fail with actual/expected mismatch: + + actual expected + + 'aababa' + + Expected 'aaba' to equal specified value: 'ba' + + at /tmp/lab-test-example/test.js:11:27 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain("Expected 'aaba' to equal specified value: 'ba'"); + expect(result?.actualOutput).toBe('aaba'); + expect(result?.expectedOutput).toBe('ba'); + }); + + it('should parse number comparison failure', () => { + const output = ` +Failed tests: + + 1) Example test should fail with number mismatch: + + actual expected + + 42100 + + Expected 42 to equal specified value: 100 + + at /tmp/lab-test-example/test.js:15:23 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain('Expected 42 to equal specified value: 100'); + expect(result?.actualOutput).toBe('42'); + expect(result?.expectedOutput).toBe('100'); + }); + + it('should parse object comparison failure', () => { + const output = ` +Failed tests: + + 1) Example test should fail with object mismatch: + + actual expected + + { + baz: 123456, + foo: 'bar' + } + + Expected { foo: 'bar', baz: 123 } to equal specified value: { foo: 'bar', baz: 456 } + + at /tmp/lab-test-example/test.js:19:45 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain("Expected { foo: 'bar', baz: 123 } to equal specified value: { foo: 'bar', baz: 456 }"); + expect(result?.actualOutput).toBe("{ foo: 'bar', baz: 123 }"); + expect(result?.expectedOutput).toBe("{ foo: 'bar', baz: 456 }"); + }); + + it('should handle errors without expected values', () => { + const output = ` +Failed tests: + + 1) Example test should throw error: + + Error: Something went wrong + + at /tmp/lab-test-example/test.js:23:15 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain('Error: Something went wrong'); + expect(result?.actualOutput).toBeUndefined(); + expect(result?.expectedOutput).toBeUndefined(); + }); + + it('should handle generic AssertionError', () => { + const output = ` +Failed tests: + + 1) Example test should fail: + + AssertionError: expected value to be truthy + + at /tmp/lab-test-example/test.js:27:10 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain('AssertionError: expected value to be truthy'); + expect(result?.actualOutput).toBeUndefined(); + expect(result?.expectedOutput).toBeUndefined(); + }); + + it('should strip ANSI escape codes', () => { + // ANSI codes for red and reset + const output = ` +Failed tests: + + 1) Example test: + + \u001b[31mExpected 'foo' to equal specified value: 'bar'\u001b[0m + + at /tmp/test.js:10:20 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain("Expected 'foo' to equal specified value: 'bar'"); + expect(result?.message).not.toContain('\u001b[31m'); + expect(result?.message).not.toContain('\u001b[0m'); + expect(result?.actualOutput).toBe('foo'); + expect(result?.expectedOutput).toBe('bar'); + }); + + it('should return undefined for empty output', () => { + const result = parseErrorMessage(''); + expect(result).toBeUndefined(); + }); + + it('should return undefined for output without errors', () => { + const output = ` +Example test + ✓ 1) should pass (10 ms) + +1 of 1 tests passed +`; + + const result = parseErrorMessage(output); + expect(result).toBeUndefined(); + }); + + it('should handle multiline actual/expected values', () => { + const output = ` +Failed tests: + + 1) Example test: + + Expected { + foo: 'bar', + nested: { a: 1 } + } to equal specified value: { + foo: 'baz', + nested: { a: 2 } + } + + at /tmp/test.js:10:20 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.actualOutput).toContain('foo'); + expect(result?.expectedOutput).toContain('baz'); + }); + }); +}); From af3ee599ac89058550d348ddf4347519518e37df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:38:48 +0000 Subject: [PATCH 3/4] fix: improve quote removal to match only paired quotes - Update cleanValue function to only remove quotes when they appear at both start and end - Prevents incorrect quote removal from mismatched or single-sided quotes - Addresses code review feedback Co-authored-by: mtharrison <916064+mtharrison@users.noreply.github.com> --- src/testRunner.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index 6a9aac9..b0416fb 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -331,9 +331,15 @@ export function parseErrorMessage(output: string): FailureInfo | undefined { if (match) { const [, actualStr, expectedStr] = match; - // Clean up the values by removing quotes and trimming + // Clean up the values by removing surrounding quotes (if both start and end match) and trimming const cleanValue = (val: string): string => { - return val.trim().replace(/^['"]|['"]$/g, ""); + const trimmed = val.trim(); + // Remove quotes only if they match at both ends + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + return trimmed.slice(1, -1); + } + return trimmed; }; return { From 81c4dc679f8caaeeaf4e64bb25c11785e2ae60af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:28:47 +0000 Subject: [PATCH 4/4] feat: support include and contain assertion patterns - Update regex to match "to include" and "to contain" patterns in addition to "to equal" - Add tests for include and contain assertion failures - Fixes color differentiation for @hapi/code assertions like "expect(x).to.include(y)" Co-authored-by: mtharrison <916064+mtharrison@users.noreply.github.com> --- src/testRunner.ts | 9 ++++++--- test/testRunner.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/testRunner.ts b/src/testRunner.ts index b0416fb..b346e0c 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -276,10 +276,13 @@ const FAILURE_START_PATTERN = /^\s*\d+\)\s+.+:/; /** * Pattern to extract actual and expected values from lab's error messages. - * Matches: "Expected to equal specified value: " - * Captures: [1] actual value (quoted or unquoted), [2] expected value (quoted or unquoted) + * Matches: + * - "Expected to equal specified value: " + * - "Expected to include " + * - "Expected to contain " + * Captures: [1] actual value, [2] expected value */ -const EXPECTED_PATTERN = /Expected (.+?) to equal specified value:\s*(.+?)(?:\s*at\s|$)/s; +const EXPECTED_PATTERN = /Expected (.+?) to (?:equal specified value:|include|contain)\s*(.+?)(?:\s*at\s|$)/s; /** * Parses lab's failure output to extract error message and actual/expected values. diff --git a/test/testRunner.test.ts b/test/testRunner.test.ts index 845c14d..407dd60 100644 --- a/test/testRunner.test.ts +++ b/test/testRunner.test.ts @@ -26,6 +26,44 @@ Failed tests: expect(result?.expectedOutput).toBe('ba'); }); + it('should parse include assertion failure', () => { + const output = ` +Failed tests: + + 1) Various assertion types should fail with include assertion: + + Expected { a: 1 } to include { b: 1 } + + at /tmp/lab-test-include/test.js:9:29 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain('Expected { a: 1 } to include { b: 1 }'); + expect(result?.actualOutput).toBe('{ a: 1 }'); + expect(result?.expectedOutput).toBe('{ b: 1 }'); + }); + + it('should parse contain assertion failure', () => { + const output = ` +Failed tests: + + 1) Various assertion types should fail with contain assertion: + + Expected 'hello' to include 'world' + + at /tmp/lab-test-include/test.js:13:28 +`; + + const result = parseErrorMessage(output); + + expect(result).toBeDefined(); + expect(result?.message).toContain("Expected 'hello' to include 'world'"); + expect(result?.actualOutput).toBe('hello'); + expect(result?.expectedOutput).toBe('world'); + }); + it('should parse number comparison failure', () => { const output = ` Failed tests: