From bacfa5aa0c4da4e43206761be25c565a6bb02d85 Mon Sep 17 00:00:00 2001 From: Jeff Dutton Date: Sun, 15 Feb 2026 00:15:16 -0500 Subject: [PATCH 1/2] feat: Add pytest error extractor for Python test output Adds a new pytest extractor plugin that parses Python pytest output into structured errors with file/line/message. Fixes false-positive where the jasmine extractor was matching pytest output at 90% confidence and reporting 0 errors despite exit code 1. - Detect pytest output at 95/90/85% confidence tiers - Parse FAILURES section (assertion errors with tracebacks) - Parse ERRORS section (collection/import errors) - Fallback to short test summary parsing - Add `.py::` to jasmine forbidden hints (belt-and-suspenders) - Add .gitignore patterns for TypeScript compilation artifacts - 12 tests built via TDD red-green-refactor cycles Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 + packages/extractors/src/extractor-registry.ts | 20 +- .../src/extractors/jasmine/index.ts | 1 + .../src/extractors/pytest/index.test.ts | 223 ++++++++++ .../extractors/src/extractors/pytest/index.ts | 399 ++++++++++++++++++ .../test/registry-integration.test.ts | 11 +- 6 files changed, 650 insertions(+), 9 deletions(-) create mode 100644 packages/extractors/src/extractors/pytest/index.test.ts create mode 100644 packages/extractors/src/extractors/pytest/index.ts diff --git a/.gitignore b/.gitignore index 3e9b492e..4f5fb348 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,11 @@ build/ *.tsbuildinfo *.tgz +# TypeScript compilation artifacts (outside dist/) +*.d.ts +*.d.ts.map +*.js.map + # Turborepo cache .turbo/ diff --git a/packages/extractors/src/extractor-registry.ts b/packages/extractors/src/extractor-registry.ts index 015775cb..5062a418 100644 --- a/packages/extractors/src/extractor-registry.ts +++ b/packages/extractors/src/extractor-registry.ts @@ -25,6 +25,7 @@ import mavenCompilerPlugin from './extractors/maven-compiler/index.js'; import mavenSurefirePlugin from './extractors/maven-surefire/index.js'; import mochaPlugin from './extractors/mocha/index.js'; import playwrightPlugin from './extractors/playwright/index.js'; +import pytestPlugin from './extractors/pytest/index.js'; import tapPlugin from './extractors/tap/index.js'; import typescriptPlugin from './extractors/typescript/index.js'; import vitestPlugin from './extractors/vitest/index.js'; @@ -74,10 +75,11 @@ export interface ExtractorDescriptor { * 2. JUnit (Priority 100) - XML format is unique * 3. TypeScript (Priority 95) - Very specific error codes * 4. Playwright (Priority 95) - .spec.ts files with › separator - * 5. Jest (Priority 90) - Must check before Mocha - * 6. Vitest (Priority 90) - Secondary fallback patterns - * 7. ESLint (Priority 85) - Distinctive format - * 8. Jasmine (Priority 85) - Distinctive "Failures:" header + * 5. Pytest (Priority 92) - Python pytest with .py:: paths + * 6. Jest (Priority 90) - Must check before Mocha + * 7. Vitest (Priority 90) - Secondary fallback patterns + * 8. ESLint (Priority 85) - Distinctive format + * 9. Jasmine (Priority 85) - Distinctive "Failures:" header * 9. Ava (Priority 82) - Ava v6+ format with ✘ markers * 10. Mocha (Priority 80) - Generic "passing/failing" patterns * 11. TAP (Priority 78) - TAP version 13 protocol @@ -141,6 +143,16 @@ export const EXTRACTOR_REGISTRY: ExtractorDescriptor[] = [ extract: playwrightPlugin.extract, }, + // Pytest - Python pytest with distinctive .py:: test paths + { + name: pytestPlugin.metadata.name, + priority: pytestPlugin.priority, + trust: 'full', // Built-in trusted code + detect: pytestPlugin.detect, + extract: pytestPlugin.extract, + hints: pytestPlugin.hints, + }, + // Jest - Must check BEFORE Mocha to avoid false positives { name: jestPlugin.metadata.name, diff --git a/packages/extractors/src/extractors/jasmine/index.ts b/packages/extractors/src/extractors/jasmine/index.ts index 06876e98..8211b73b 100644 --- a/packages/extractors/src/extractors/jasmine/index.ts +++ b/packages/extractors/src/extractors/jasmine/index.ts @@ -292,6 +292,7 @@ const jasminePlugin: ExtractorPlugin = { hints: { required: ['spec'], anyOf: ['Failures:'], + forbidden: ['.py::'], }, priority: 90, // Updated to match highest confidence detect, diff --git a/packages/extractors/src/extractors/pytest/index.test.ts b/packages/extractors/src/extractors/pytest/index.test.ts new file mode 100644 index 00000000..64da65c7 --- /dev/null +++ b/packages/extractors/src/extractors/pytest/index.test.ts @@ -0,0 +1,223 @@ +/** + * Pytest Error Extractor - TDD Tests + * + * Built test-first following Red-Green-Refactor cycles. + * + * @package @vibe-validate/extractors + */ + +import { describe, it, expect } from 'vitest'; + +import { expectExtractionResult, expectSamplesParseSuccessfully } from '../../test/helpers/extractor-test-helpers.js'; + +import pytestPlugin from './index.js'; + +describe('pytest extractor plugin', () => { + describe('detect', () => { + it('should detect pytest via short summary + .py paths at 90% confidence', () => { + // No platform line — just short summary with .py paths + const output = `=========================== short test summary info ============================ +FAILED tests/test_calc.py::TestCalc::test_divide - ZeroDivisionError: division by zero +========================= 1 failed, 4 passed in 0.45s =========================`; + + const result = pytestPlugin.detect(output); + expect(result.confidence).toBe(90); + expect(result.patterns).toContain('short test summary'); + }); + + it('should detect pytest output with platform line at 95% confidence', () => { + const output = `============================= test session starts ============================== +platform darwin -- Python 3.9.6, pytest-8.4.1, pluggy-1.6.0 +rootdir: /Users/dev/project +collected 5 items + +tests/test_calc.py F. [ 40%] + +=========================== short test summary info ============================ +FAILED tests/test_calc.py::TestCalc::test_divide - ZeroDivisionError: division by zero +========================= 1 failed, 4 passed in 0.45s =========================`; + + const result = pytestPlugin.detect(output); + expect(result.confidence).toBe(95); + expect(result.patterns).toContain('pytest platform line'); + expect(result.reason).toContain('pytest'); + }); + + it('should detect pytest via summary format + .py paths at 85% confidence', () => { + // No platform line, no short test summary — just pytest-style counts + .py paths + const output = `FAILED tests/test_calc.py::test_divide +========================= 1 failed, 4 passed in 0.45s =========================`; + + const result = pytestPlugin.detect(output); + expect(result.confidence).toBe(85); + expect(result.patterns).toContain('pytest summary format'); + }); + + it('should NOT detect non-pytest output', () => { + const output = `src/index.ts:42:5 - error TS2322: Type 'string' is not assignable to type 'number'.`; + const result = pytestPlugin.detect(output); + expect(result.confidence).toBe(0); + }); + + it('should NOT detect Jasmine output (regression guard)', () => { + const output = `Started +F + +Failures: +1) Test Suite should work + Message: + Expected 4 to equal 5. + Stack: + at + at UserContext. (test.js:9:17) + +1 spec, 1 failure +Finished in 0.037 seconds`; + + const result = pytestPlugin.detect(output); + expect(result.confidence).toBe(0); + }); + }); + + describe('extract', () => { + describe('FAILURES section (assertion errors)', () => { + it('should extract assertion failures from FAILURES section', () => { + const output = `============================= test session starts ============================== +platform linux -- Python 3.11.0, pytest-7.4.0, pluggy-1.3.0 +rootdir: /home/dev/project +collected 5 items + +tests/test_calc.py F. [ 40%] +tests/test_utils.py .F. [ 100%] + +================================== FAILURES =================================== +___________________________ TestCalc.test_divide ___________________________ + + def test_divide(self): +> assert divide(10, 0) == float('inf') +E ZeroDivisionError: division by zero + +tests/test_calc.py:15: ZeroDivisionError +___________________________ TestUtils.test_parse ___________________________ + + def test_parse(self): +> assert parse("abc") == 123 +E AssertionError: assert None == 123 +E + where None = parse('abc') + +tests/test_utils.py:22: AssertionError +=========================== short test summary info ============================ +FAILED tests/test_calc.py::TestCalc::test_divide - ZeroDivisionError: division by zero +FAILED tests/test_utils.py::TestUtils::test_parse - AssertionError: assert None == 123 +========================= 2 failed, 3 passed in 0.45s =========================`; + + const result = pytestPlugin.extract(output); + expectExtractionResult(result, { errorCount: 2 }); + + // First failure: ZeroDivisionError with file:line + expect(result.errors[0].file).toBe('tests/test_calc.py'); + expect(result.errors[0].line).toBe(15); + expect(result.errors[0].message).toContain('ZeroDivisionError'); + + // Second failure: AssertionError with file:line + expect(result.errors[1].file).toBe('tests/test_utils.py'); + expect(result.errors[1].line).toBe(22); + expect(result.errors[1].message).toContain('AssertionError'); + }); + }); + + describe('ERRORS section (collection errors)', () => { + it('should extract collection errors from ERRORS section', () => { + // Real-world scenario: lfa-cc-marketplace powerpoint plugin failure + const output = `============================= test session starts ============================== +platform darwin -- Python 3.9.6, pytest-8.4.1, pluggy-1.6.0 +rootdir: /Users/dev/project +collected 10 items / 2 errors + +==================================== ERRORS ==================================== +________ ERROR collecting tests/test_foo.py ________ +tests/test_foo.py:9: in + from mymodule import MyClass +mymodule.py:50: in + class MyClass: +mymodule.py:55: in MyClass + def method(self, arg: str | None = None) -> dict: +E TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' +________ ERROR collecting tests/test_bar.py ________ +tests/test_bar.py:3: in + from missing_module import func +E ModuleNotFoundError: No module named 'missing_module' +=========================== short test summary info ============================ +ERROR tests/test_foo.py - TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' +ERROR tests/test_bar.py - ModuleNotFoundError: No module named 'missing_module' +========================= 2 errors in 0.50s =========================`; + + const result = pytestPlugin.extract(output); + expectExtractionResult(result, { errorCount: 2 }); + + // First error: TypeError from traceback + expect(result.errors[0].file).toBe('mymodule.py'); + expect(result.errors[0].line).toBe(55); + expect(result.errors[0].message).toContain('TypeError'); + + // Second error: ModuleNotFoundError + expect(result.errors[1].message).toContain('ModuleNotFoundError'); + }); + }); + + describe('Short summary fallback', () => { + it('should extract from short summary when no FAILURES/ERRORS sections', () => { + const output = `=========================== short test summary info ============================ +FAILED tests/test_calc.py::TestCalc::test_divide - ZeroDivisionError: division by zero +ERROR tests/test_bar.py - ModuleNotFoundError: No module named 'missing_module' +========================= 1 failed, 1 error in 0.50s =========================`; + + const result = pytestPlugin.extract(output); + expectExtractionResult(result, { errorCount: 2 }); + + expect(result.errors[0].file).toBe('tests/test_calc.py'); + expect(result.errors[0].message).toContain('ZeroDivisionError'); + expect(result.errors[1].file).toBe('tests/test_bar.py'); + expect(result.errors[1].message).toContain('ModuleNotFoundError'); + }); + }); + + describe('Edge cases', () => { + it('should return 0 errors for all-passing output', () => { + const output = `============================= test session starts ============================== +platform darwin -- Python 3.9.6, pytest-8.4.1, pluggy-1.6.0 +rootdir: /Users/dev/project +collected 10 items + +tests/test_calc.py .......... [100%] + +============================== 10 passed in 0.50s ==============================`; + + const result = pytestPlugin.extract(output); + expectExtractionResult(result, { errorCount: 0 }); + }); + }); + }); + + describe('metadata', () => { + it('should have correct plugin metadata', () => { + expect(pytestPlugin.metadata.name).toBe('pytest'); + expect(pytestPlugin.priority).toBe(92); + expect(pytestPlugin.hints?.anyOf).toContain('pytest'); + expect(pytestPlugin.hints?.anyOf).toContain('.py::'); + expect(pytestPlugin.hints?.forbidden).toContain('at '); + expect(pytestPlugin.metadata.tags).toContain('python'); + expect(pytestPlugin.metadata.tags).toContain('testing'); + }); + }); + + describe('samples', () => { + it('should have at least 2 sample test cases', () => { + expect(pytestPlugin.samples.length).toBeGreaterThanOrEqual(2); + }); + + it('should successfully parse all sample inputs', () => { + expectSamplesParseSuccessfully(pytestPlugin); + }); + }); +}); diff --git a/packages/extractors/src/extractors/pytest/index.ts b/packages/extractors/src/extractors/pytest/index.ts new file mode 100644 index 00000000..9bc4f7fe --- /dev/null +++ b/packages/extractors/src/extractors/pytest/index.ts @@ -0,0 +1,399 @@ +/** + * Pytest Error Extractor Plugin + * + * @package @vibe-validate/extractors + */ + +import type { + ExtractorPlugin, + ErrorExtractorResult, + DetectionResult, + ExtractorSample, +} from '../../types.js'; +import { extractErrorType } from '../../utils/parser-utils.js'; +import { processTestFailures, type TestFailureInfo } from '../../utils/test-framework-utils.js'; + +const PLATFORM_RE = /platform\s+\S+\s+--\s+Python\s+[\d.]+,\s+pytest-[\d.]+/; +const PY_TEST_PATHS_RE = /(?:FAILED|ERROR)\s+\S+\.py/; +const SHORT_SUMMARY_KEYWORD = 'short test summary'; +// eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest summary line (controlled output), not user input +const PASSED_RE = /\d+\s+passed/; +// eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest summary line (controlled output), not user input +const FAILED_SUMMARY_RE = /\d+\s+failed/; +// eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest summary line (controlled output), not user input +const ERROR_SUMMARY_RE = /\d+\s+error/; + +function detect(output: string): DetectionResult { + if (PLATFORM_RE.test(output)) { + return { + confidence: 95, + patterns: ['pytest platform line'], + reason: 'Python pytest output detected (platform line with pytest version)', + }; + } + + const hasShortSummary = output.includes(SHORT_SUMMARY_KEYWORD); + const hasPyTestPaths = PY_TEST_PATHS_RE.test(output); + + if (hasShortSummary && hasPyTestPaths) { + return { + confidence: 90, + patterns: [SHORT_SUMMARY_KEYWORD, 'FAILED/ERROR .py paths'], + reason: 'Python pytest output detected (short test summary with .py paths)', + }; + } + + // Weaker signal: pytest-style summary line with .py paths + const hasPytestSummary = PASSED_RE.test(output) && ( + FAILED_SUMMARY_RE.test(output) || + ERROR_SUMMARY_RE.test(output) || + output.includes('= FAILURES =') || + output.includes('= ERRORS =') + ); + + if (hasPytestSummary && hasPyTestPaths) { + return { + confidence: 85, + patterns: ['pytest summary format', 'FAILED/ERROR .py paths'], + reason: 'Possible pytest output (summary format with .py paths)', + }; + } + + return { confidence: 0, patterns: [], reason: '' }; +} + +/** Parse E-prefixed error lines: "E message" */ +const E_LINE_RE = /^E\s{3}(.+)$/; + +/** Parse file:line:ErrorType from FAILURES block location line */ +const LOCATION_RE = /^(\S[^:]*\.py):(\d+):\s*(\w+(?:Error|Exception|Warning)?)/; + +/** Parse file:line from traceback "in " lines */ +const TRACE_RE = /^(\S[^:]*\.py):(\d+):\s*in\s+/; + +/** Check if a line is a pytest section header (=== ... ===) */ +function isSectionHeader(line: string, excludeSection: string): boolean { + return /^={3,}\s/.test(line) && !line.includes(excludeSection); +} + +/** Check if a line is a pytest block header (___ ... ___) */ +// eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest output (controlled output), not user input +const BLOCK_HEADER_RE = /^_{3,}\s+(.+?)\s+_{3,}$/; + +/** Check if a line starts a new block or section */ +function isBlockOrSectionBoundary(line: string, excludeSection: string): boolean { + return BLOCK_HEADER_RE.test(line) || isSectionHeader(line, excludeSection); +} + +/** + * Scan a FAILURES block for E-prefixed errors and file:line location. + */ +function scanBlockForErrors( + lines: string[], + startIndex: number, + excludeSection: string, +): { eLines: string[]; file?: string; lineNumber?: number; errorType?: string; nextIndex: number } { + const eLines: string[] = []; + let file: string | undefined; + let lineNumber: number | undefined; + let errorType: string | undefined; + + let j = startIndex; + while (j < lines.length) { + const blockLine = lines[j]; + + if (isBlockOrSectionBoundary(blockLine, excludeSection)) { + break; + } + + const eMatch = E_LINE_RE.exec(blockLine); + if (eMatch) { + eLines.push(eMatch[1]); + } + + const locationMatch = LOCATION_RE.exec(blockLine); + if (locationMatch) { + file = locationMatch[1]; + lineNumber = Number.parseInt(locationMatch[2], 10); + errorType ??= locationMatch[3] || undefined; + } + + j++; + } + + return { eLines, file, lineNumber, errorType, nextIndex: j }; +} + +/** + * Parse failures from the FAILURES section + */ +function parseFailuresSection(output: string): TestFailureInfo[] { + const failures: TestFailureInfo[] = []; + const failuresStart = output.indexOf('= FAILURES ='); + if (failuresStart === -1) return failures; + + const lines = output.slice(failuresStart).split('\n'); + + let i = 1; + while (i < lines.length) { + const line = lines[i]; + + if (i > 1 && isSectionHeader(line, 'FAILURES')) { + break; + } + + const blockMatch = BLOCK_HEADER_RE.exec(line); + if (blockMatch) { + const testName = blockMatch[1]; + const scan = scanBlockForErrors(lines, i + 1, 'FAILURES'); + + let errorMessage: string | undefined; + let { errorType } = scan; + if (scan.eLines.length > 0) { + errorMessage = scan.eLines.join(' ').trim(); + errorType ??= extractErrorType(errorMessage); + } + + failures.push({ + testName, + message: errorMessage ?? 'Test failed', + errorType, + file: scan.file, + line: scan.lineNumber, + }); + + i = scan.nextIndex; + } else { + i++; + } + } + + return failures; +} + +/** + * Scan an ERRORS block for traceback file:line and E-prefixed errors. + * Keeps last traceback location (closest to error source). + */ +function scanErrorBlockForErrors( + lines: string[], + startIndex: number, + excludeSection: string, +): { errorMessage?: string; errorType?: string; file?: string; lineNumber?: number; nextIndex: number } { + let errorMessage: string | undefined; + let errorType: string | undefined; + let file: string | undefined; + let lineNumber: number | undefined; + + let j = startIndex; + while (j < lines.length) { + const blockLine = lines[j]; + + if (isBlockOrSectionBoundary(blockLine, excludeSection)) { + break; + } + + const traceMatch = TRACE_RE.exec(blockLine); + if (traceMatch) { + file = traceMatch[1]; + lineNumber = Number.parseInt(traceMatch[2], 10); + } + + const eMatch = E_LINE_RE.exec(blockLine); + if (eMatch) { + errorMessage = eMatch[1].trim(); + errorType = extractErrorType(errorMessage); + } + + j++; + } + + return { errorMessage, errorType, file, lineNumber, nextIndex: j }; +} + +/** + * Parse errors from the ERRORS section (collection/import errors) + */ +function parseErrorsSection(output: string): TestFailureInfo[] { + const errors: TestFailureInfo[] = []; + const errorsStart = output.indexOf('= ERRORS ='); + if (errorsStart === -1) return errors; + + const lines = output.slice(errorsStart).split('\n'); + // eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest section headers (controlled output), not user input + const errorCollectingRe = /^_{3,}\s+ERROR\s+collecting\s+(.+?)\s+_{3,}$/; + + let i = 1; + while (i < lines.length) { + const line = lines[i]; + + if (i > 1 && isSectionHeader(line, 'ERRORS')) { + break; + } + + const blockMatch = errorCollectingRe.exec(line); + if (blockMatch) { + const testFile = blockMatch[1]; + const scan = scanErrorBlockForErrors(lines, i + 1, 'ERRORS'); + + errors.push({ + testName: `ERROR collecting ${testFile}`, + message: scan.errorMessage ?? 'Collection error', + errorType: scan.errorType, + file: scan.file ?? testFile, + line: scan.lineNumber, + }); + + i = scan.nextIndex; + } else { + i++; + } + } + + return errors; +} + +/** Parse FAILED line from short test summary */ +// eslint-disable-next-line sonarjs/slow-regex, security/detect-unsafe-regex -- Safe: only parses pytest summary lines (controlled output), not user input +const FAILED_RE = /^FAILED\s+(\S+\.py)(?:::(\S+))?\s+-\s+(.+)$/; + +/** Parse ERROR line from short test summary */ +// eslint-disable-next-line sonarjs/slow-regex -- Safe: only parses pytest summary lines (controlled output), not user input +const ERROR_RE = /^ERROR\s+(\S+\.py)\s+-\s+(.+)$/; + +/** + * Parse the short test summary info section (fallback) + */ +function parseShortSummary(output: string): TestFailureInfo[] { + const failures: TestFailureInfo[] = []; + + for (const line of output.split('\n')) { + const failedMatch = FAILED_RE.exec(line); + if (failedMatch) { + const errorType = extractErrorType(failedMatch[3]); + failures.push({ + testName: failedMatch[2] ?? failedMatch[1], + message: failedMatch[3].trim(), + errorType, + file: failedMatch[1], + }); + continue; + } + + const errorMatch = ERROR_RE.exec(line); + if (errorMatch) { + const errorType = extractErrorType(errorMatch[2]); + failures.push({ + testName: `ERROR collecting ${errorMatch[1]}`, + message: errorMatch[2].trim(), + errorType, + file: errorMatch[1], + }); + } + } + + return failures; +} + +function extract(output: string): ErrorExtractorResult { + const failuresFromSection = parseFailuresSection(output); + const errorsFromSection = parseErrorsSection(output); + const allFromSections = [...failuresFromSection, ...errorsFromSection]; + + if (allFromSections.length > 0) { + return processTestFailures(allFromSections, 95); + } + + const summaryFailures = parseShortSummary(output); + if (summaryFailures.length > 0) { + return processTestFailures(summaryFailures, 90); + } + + return processTestFailures([], 95); +} + +const samples: ExtractorSample[] = [ + { + name: 'collection-errors', + description: 'Pytest collection errors (import failures)', + input: `============================= test session starts ============================== +platform darwin -- Python 3.9.6, pytest-8.4.1, pluggy-1.6.0 +rootdir: /Users/dev/project +collected 10 items / 2 errors + +==================================== ERRORS ==================================== +________ ERROR collecting tests/test_foo.py ________ +tests/test_foo.py:9: in + from mymodule import MyClass +mymodule.py:50: in + class MyClass: +mymodule.py:55: in MyClass + def method(self, arg: str | None = None) -> dict: +E TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' +________ ERROR collecting tests/test_bar.py ________ +tests/test_bar.py:3: in + from missing_module import func +E ModuleNotFoundError: No module named 'missing_module' +=========================== short test summary info ============================ +ERROR tests/test_foo.py - TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' +ERROR tests/test_bar.py - ModuleNotFoundError: No module named 'missing_module' +========================= 2 errors in 0.50s =========================`, + expectedErrors: 2, + expectedPatterns: ['TypeError', 'ModuleNotFoundError'], + }, + { + name: 'assertion-failures', + description: 'Pytest assertion failures in test functions', + input: `============================= test session starts ============================== +platform linux -- Python 3.11.0, pytest-7.4.0, pluggy-1.3.0 +rootdir: /home/dev/project +collected 5 items + +tests/test_calc.py F. [ 40%] +tests/test_utils.py .F. [ 100%] + +================================== FAILURES =================================== +___________________________ TestCalc.test_divide ___________________________ + + def test_divide(self): +> assert divide(10, 0) == float('inf') +E ZeroDivisionError: division by zero + +tests/test_calc.py:15: ZeroDivisionError +___________________________ TestUtils.test_parse ___________________________ + + def test_parse(self): +> assert parse("abc") == 123 +E AssertionError: assert None == 123 +E + where None = parse('abc') + +tests/test_utils.py:22: AssertionError +=========================== short test summary info ============================ +FAILED tests/test_calc.py::TestCalc::test_divide - ZeroDivisionError: division by zero +FAILED tests/test_utils.py::TestUtils::test_parse - AssertionError: assert None == 123 +========================= 2 failed, 3 passed in 0.45s =========================`, + expectedErrors: 2, + expectedPatterns: ['ZeroDivisionError', 'AssertionError'], + }, +]; + +const pytestPlugin: ExtractorPlugin = { + metadata: { + name: 'pytest', + version: '1.0.0', + author: 'vibe-validate', + description: 'Extracts Python pytest test framework errors', + repository: 'https://github.com/jdutton/vibe-validate', + tags: ['pytest', 'testing', 'python'], + }, + hints: { + anyOf: ['pytest', '.py::'], + forbidden: ['at ', 'at Context.'], + }, + priority: 92, + detect, + extract, + samples, +}; + +export default pytestPlugin; diff --git a/packages/extractors/test/registry-integration.test.ts b/packages/extractors/test/registry-integration.test.ts index e3b52a9f..7426c4fe 100644 --- a/packages/extractors/test/registry-integration.test.ts +++ b/packages/extractors/test/registry-integration.test.ts @@ -32,6 +32,7 @@ describe('Extractor Registry - Trust Level Integration', () => { 'mocha', 'jasmine', 'playwright', + 'pytest', 'junit', 'maven-compiler', 'maven-checkstyle', @@ -373,15 +374,15 @@ src/example.ts(15,10): error TS2304: Cannot find name 'foo'. }); describe('Complete coverage of trust field', () => { - it('should have exactly 15 extractors with trust levels', () => { + it('should have exactly 16 extractors with trust levels', () => { // Count unique extractor names const uniqueNames = new Set(EXTRACTOR_REGISTRY.map(e => e.name)); - // Should have 14 unique extractors (vitest appears twice with different priorities) - expect(uniqueNames.size).toBe(14); + // Should have 15 unique extractors (vitest appears twice with different priorities) + expect(uniqueNames.size).toBe(15); - // All 15+ registry entries should have trust field - expect(EXTRACTOR_REGISTRY.length).toBeGreaterThanOrEqual(15); + // All 16+ registry entries should have trust field + expect(EXTRACTOR_REGISTRY.length).toBeGreaterThanOrEqual(16); expect(EXTRACTOR_REGISTRY.every(e => e.trust === 'full')).toBe(true); }); From 917d7f8f81bad2ab53bf2cf9b1a02684cacd951f Mon Sep 17 00:00:00 2001 From: Jeff Dutton Date: Sun, 15 Feb 2026 12:09:25 -0500 Subject: [PATCH 2/2] chore: Prepare v0.19.0-rc.12 Co-Authored-By: Claude Opus 4.6 --- docs/skill/SKILL.md | 2 +- package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/test/bin/wrapper.test.ts | 2 +- packages/config/package.json | 2 +- packages/core/package.json | 2 +- packages/extractors/package.json | 2 +- packages/git/package.json | 2 +- packages/history/package.json | 2 +- packages/utils/package.json | 2 +- packages/vibe-validate/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index adf10c9e..e05b6770 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: vibe-validate -version: 0.19.0-rc.11 # Tracks vibe-validate package version +version: 0.19.0-rc.12 # Tracks vibe-validate package version description: Expert guidance for vibe-validate, an LLM-optimized validation orchestration tool. Use when working with vibe-validate commands, configuration, pre-commit workflows, or validation orchestration in TypeScript projects. model: claude-sonnet-4-5 tools: diff --git a/package.json b/package.json index ae5605d5..d259bd6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibe-validate", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "type": "module", "private": true, "description": "Git-aware validation orchestration for vibe coding (LLM-assisted development)", diff --git a/packages/cli/package.json b/packages/cli/package.json index 978ac1e5..38e30743 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/cli", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Command-line interface for vibe-validate validation framework", "type": "module", "main": "./dist/index.js", diff --git a/packages/cli/test/bin/wrapper.test.ts b/packages/cli/test/bin/wrapper.test.ts index 89a1b4c2..da7fb754 100644 --- a/packages/cli/test/bin/wrapper.test.ts +++ b/packages/cli/test/bin/wrapper.test.ts @@ -15,7 +15,7 @@ import { executeWrapperSync, type WrapperResultSync } from '../helpers/test-comm */ // Test constants -const EXPECTED_VERSION = '0.19.0-rc.11'; // BUMP_VERSION_UPDATE +const EXPECTED_VERSION = '0.19.0-rc.12'; // BUMP_VERSION_UPDATE const REPO_ROOT = join(__dirname, '../../../..'); const PACKAGES_CORE = join(__dirname, '../../../core'); diff --git a/packages/config/package.json b/packages/config/package.json index 442caec1..520e7adc 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/config", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Configuration system for vibe-validate with TypeScript-first design and config templates", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 6e73bf9c..e6d6e7cd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/core", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Core validation orchestration engine for vibe-validate", "type": "module", "main": "./dist/index.js", diff --git a/packages/extractors/package.json b/packages/extractors/package.json index 080d707e..54834e83 100644 --- a/packages/extractors/package.json +++ b/packages/extractors/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/extractors", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "LLM-optimized error extractors for validation output", "type": "module", "main": "./dist/index.js", diff --git a/packages/git/package.json b/packages/git/package.json index be32cc85..ce2e880d 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/git", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Git utilities for vibe-validate - tree hash calculation, branch sync, and post-merge cleanup", "type": "module", "main": "./dist/index.js", diff --git a/packages/history/package.json b/packages/history/package.json index 858b2445..0dae32c5 100644 --- a/packages/history/package.json +++ b/packages/history/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/history", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Validation history tracking via git notes for vibe-validate", "type": "module", "main": "./dist/index.js", diff --git a/packages/utils/package.json b/packages/utils/package.json index 099f4ecb..68a8bd17 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/utils", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Common utilities for vibe-validate packages (command execution, path normalization)", "type": "module", "main": "./dist/index.js", diff --git a/packages/vibe-validate/package.json b/packages/vibe-validate/package.json index f37595c9..04e817cf 100644 --- a/packages/vibe-validate/package.json +++ b/packages/vibe-validate/package.json @@ -1,6 +1,6 @@ { "name": "vibe-validate", - "version": "0.19.0-rc.11", + "version": "0.19.0-rc.12", "description": "Git-aware validation orchestration for vibe coding (LLM-assisted development) - umbrella package", "type": "module", "bin": {