feat: add M5 test runner backend - #74
Conversation
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughAdds a new internal/testrunner package for workspace check detection and parsing (Go, Bun, Node, pytest, Cargo); integrates testrunner into internal/verify (Check metadata and conditional TestSummary parsing); updates CLI formatting and redaction to render and sanitize parsed test summaries; and adds tests for detection, parsing, integration, and redaction. ChangesTest discovery and parsing
Verify system integration
CLI output formatting and redaction
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/testrunner/testrunner.go`:
- Around line 193-206: The detectPackageManager function currently defaults to
Bun which causes projects without lockfiles or a packageManager field to be
treated as bun; change the default return in detectPackageManager to
packageManager{Name: "npm", IDPrefix: "npm", Framework: FrameworkNode} so plain
Node workspaces fall back to npm (keep the existing declaredName handling and
other cases intact). Ensure the packageManager struct construction and Framework
references (FrameworkNode) are used in the default branch instead of Bun.
- Around line 224-260: parseGoSummary currently sets sawDetailedTest when
goFailPattern matches, which causes package-only "ok ..." lines to be ignored in
mixed verbose/non-verbose runs; update parseGoSummary so that encountering
goFailPattern does not alone set sawDetailedTest (or only sets it when a real
detailed test line is seen), and ensure goPackageOK matches (using goPackageOK)
still increments packagePasses and that summary.Passed is augmented by
packagePasses when detailed tests were not seen; modify the logic around the
goFailPattern case and the sawDetailedTest flag handling in parseGoSummary to
preserve counting of ok packages while still recording failures into
summary.Failures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 681bf6d4-0c40-48c3-82aa-31f25a0371d3
📒 Files selected for processing (6)
internal/cli/workflow_test.gointernal/cli/workflows.gointernal/testrunner/testrunner.gointernal/testrunner/testrunner_test.gointernal/verify/verify.gointernal/verify/verify_test.go
anandh8x
left a comment
There was a problem hiding this comment.
What's good
internal/testrunneris the right shared home for detection and parsing.Detectreturns checks in a stable, deterministic order (Go, then per-script package checks, then pytest, then cargo) andParseSummarydispatches by framework. Pulling this out ofinternal/verifyremoves the Bun-only hardcoding inDetectPlanand unblocks pytest/cargo forzero verify.- Framework coverage is pragmatic and reasonably tested.
parseGoSummaryhandles verbose--- PASS/FAIL/SKIP, package-onlyok, the mixed non-verbose+verbose case, and correctly avoids double-counting;parseBunSummary,parseNodeSummary,parsePytestSummary, andparseCargoSummaryall sit on a sharedmergeSummaryCountshelper so plain "N pass / N fail / N skip" summary lines contribute. Failure location extraction for Go uses the next line when present, plus a back-fill pass for the case where the location appears after a blank. - Redaction order is now correct.
verify.Runappliesredaction.RedactStringto stdout/stderr beforeParseSummarysees them, so test summary counts and failure messages (name, file, message) operate on redacted content.redactVerifyReportin the CLI now copiesResultsfirst and copies nested slices (OutputSummary.Lines,TestSummary.Failures) before mutating, so a JSON report passed in is not mutated in place — fixes the in-place mutation concern from #59. - CLI formatting shows the most useful information without being noisy. The new
formatVerifyTestSummaryproduces a single "tests: N total, N passed, N failed" line (skip count when non-zero) and follows with per-failurefailure: Name at Filelines only when a name is present. JSON output gets the structuredtestSummaryblock. - Detection defaults to npm for plain Node workspaces.
detectPackageManagernow falls through tonpm/FrameworkNodewhen no lockfile and no declared manager are present; the prior Bun default is gone, andTestDetectDefaultsPlainNodeWorkspacesToNPMlocks that behavior in. shouldParseTestSummarykeeps parse-on-test intact even for legacy plans. New plans get a populatedKind, but for plans constructed by older code (or external callers) the kind fallbacks (id contains.test/pytest, or command containstest/pytest) are conservative and only match real test scripts.- Defensive copies are consistent.
Plan.Checksis built viamake(..., 0, len(detected))and eachCommandslice is copied withappend([]string{}, check.Command...)before being passed to the runner and toParseSummary. The testrunnerSummaryandFailureslices are also handled by reference-only, with redaction handled in the CLI layer.
Observations (non-blocking)
-
shouldParseTestSummaryid-substring heuristic can false-positive. It checksstrings.Contains(id, ".test") || strings.HasSuffix(id, "test") || strings.Contains(id, "pytest")and any command part equal totestorpytest. In current codeDetectsetsKindexplicitly, so this only matters for external callers. Still, ids likebun.pretestor a hypotheticale2e.attestscript would be treated as a test. AKind != ""short-circuit already short-circuits totrueforKindTest; aKind != ""short-circuit tofalsefor other kinds is the missing twin. Right now aKind == KindTypecheckcheck still falls through to the id heuristic — fine, but worth noting. -
parseGoSummarysecond pass has a dead guard.if location == nil || index == 0 { continue }—index == 0cannot occur because the first pass already established thatlinesis non-empty only when the input had content, andlen(lines) > 0is guaranteed. The guard is harmless but reads like it was guarding against a case that is no longer possible aftersplitLinesfilters empty lines. -
parseNodeSummaryhas noSkippedtracking in the TAP path.ok N - nameincrementsPassed,not ok N - nameincrementsFailedand records the name, but# skip Nsummary lines are the only way skips enter viamergeSummaryCounts. A TAP test that prints# SKIP name(TAP "directives") is not captured, only aggregate counts are. -
mergeSummaryCountsusesmaxIntso later, larger counts win. That is the right choice for repeating summary lines, but it means a parser that prints both an old stale line and a new accurate one will keep the larger value. In practice summary lines appear once at the end, so this is fine; worth a comment in the helper. -
Framework summary regex is greedy on whitespace boundaries.
summaryCountPrefixmatches(\d+)\s+(pass|passes|...); for an input likepanic: 1 failedthe1andfailedare two tokens and the regex needs the\s+between them — which exists, so it matches. That is correct, but the same pattern will also match a line likefatal: saw 0 passed assertionsin some other tool's output. Not a real problem; just noting the parser is permissive on purpose. -
redactVerifyReportwalks the copiedResultsby index rather than range. Usingfor index := range report.Resultsand thenreport.Results[index]is fine because the loop is read-only, but afor _, result := range report.Resultsplus a smallcloneResulthelper would read more cleanly. Cosmetic. -
No stdout/stderr size cap before
ParseSummary. A pathological runner that emits megabytes of test output still gets fully redacted and parsed on the main goroutine. For localzero verifythis is acceptable, but a future follow-up could cap the buffer fed toParseSummary(e.g., last 1 MB) so a runaway test cannot stall the agent loop. The existingOutputSummaryalready truncates viasummarizeOutputfor display. -
Test
TestRunParsesStructuredFailureSummaryredaction assertion is on the formatted text, not the JSONTestSummary. The newif got := report.Results[0].TestSummary.Failures[0]; !strings.Contains(got.Message, "[REDACTED]")assertion does cover the JSON, which is good. Worth a follow-up that the message stored on theTestSummaryis itself redaction-safe even if a caller decides to re-redact via a different policy — currently it relies onRedactStringbeing applied at the source. -
Cargo test names with spaces are split by the regex.
cargoTestLinematchestest (.+) \.\.\. (ok|FAILED|ignored)$, sotest runs::zero test ... okbecomesName: "runs::zero test", which is fine; just noting that names can include spaces and that gets recorded verbatim into theFailure.Nameand through the CLI line. -
Pytest
Messageuses everything after-.pytestFailureLine^FAILED\s+(\S+)(?:\s+-\s*(.*))?$records the dash and tail asMessage. For aFAILED tests/test_cli.py::test_stream - AssertionError: boomit storesAssertionError: boom, which is what you want, but the leading-is stripped bysafeSubmatch'sTrimSpace— the downstreamFailure.Messagewill beAssertionError: boom(good). -
parseGoSummaryTotalfromsummary.Total++only on=== RUNlines. For a non-verbose run that still records package status withgo test -vdisabled,summary.Totalstays at 0 untilnormalizeTotalssets it toPassed + Failed + Skipped. That is correct, but meanssummary.Totalreflects distinct pass/fail/skip sources; if a verbose run mixed withoklines,summary.Totalincludes RUNs andsummary.Passedis incremented per PASS plus package passes, which already-matches-a-verbose-test would not double-count becausesawVerbosePerTestOutputis true. This is the intended invariant, but a regression test forTotal == Passed + Failedin mixed scenarios is the one that would catch any future change. -
No parallel test for
TestRunSkipsStructuredSummaryForNonTestChecksvia the JSON path. The test assertsreport.Results[0].TestSummary == nilfor a typecheck check, which is the right guard. A follow-up that feeds typecheck output with "tests:" in it (e.g., a tsc error that says "Failed 0 tests") would currently be ignored as expected. No action needed; just noting theshouldParseTestSummarykind-first rule is the right defense.
No blockers. The slice is well-scoped, tests cover the prior CodeRabbit nits (npm default, mixed non-verbose Go counting), the integration with zero verify is clean, and the nested redaction in redactVerifyReport is now mutation-safe. The two follow-ups I would prioritize next: (1) the shouldParseTestSummary id heuristic tightening once Kind is required for plans, and (2) a buffer cap before ParseSummary for long-running test output.
Summary
zero verifyJSON and text output, with nested redaction for output summaries and failure details.Validation
git diff --check origin/main...HEADgo test -count=1 -p 1 ./...bun install --frozen-lockfilebun run typecheckbun test ./tests --timeout 15000(217 pass, 0 fail)bun run buildbun run smoke:buildbun run smoke:go./zero verify --json --only go.test --timeout-ms 120000 | rg '"testSummary"|"framework"|"total"|"passed"|"failed"' -nReviewers: @Vasanthdev2004 @anandh8x
Summary by CodeRabbit