diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index a1837633db..784a424b7a 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -45,6 +45,17 @@ export type LocalBranchValidation = { exitCode?: number | undefined; }; +/** + * A local validation command counts as passing test evidence when it ran green — either a full run + * (`"passed"`) or a focused subset (`"focused"`, e.g. `vitest run path/to/file.test.ts`). Single-sourced + * so every evidence surface (the PR-packet validation summary, the freshness evidence list, the + * `validation_as_test_evidence` finding, and the v2 workspace-intelligence count) agrees on what passing + * means instead of drifting apart. + */ +export function isPassingValidation(entry: LocalBranchValidation): boolean { + return entry.status === "passed" || entry.status === "focused"; +} + export type LocalBranchScorer = { mode: "metadata_only" | "external_command" | "gittensor_root"; activeModel?: string | undefined; @@ -841,7 +852,7 @@ function buildLocalFindings( }, ] : []), - ...((input.validation ?? []).some((entry) => entry.status === "passed") && !changedFiles.some((file) => isTestFile(file.path)) + ...((input.validation ?? []).some(isPassingValidation) && !changedFiles.some((file) => isTestFile(file.path)) ? [ { code: "validation_as_test_evidence", @@ -1209,7 +1220,7 @@ function renderPrPacketMarkdown(title: string, sections: Array<{ heading: string function summarizeValidation(validation: LocalBranchValidation[]): LocalBranchAnalysis["prPacket"]["validationSummary"] { return { - passed: validation.filter((entry) => entry.status === "passed" || entry.status === "focused").length, + passed: validation.filter(isPassingValidation).length, failed: validation.filter((entry) => entry.status === "failed").length, notRun: validation.filter((entry) => entry.status === "not_run" || entry.status === "skipped" || entry.status === "unknown").length, commands: validation, @@ -1218,7 +1229,7 @@ function summarizeValidation(validation: LocalBranchValidation[]): LocalBranchAn function validationEvidence(validation: LocalBranchValidation[] | undefined): string[] { return (validation ?? []) - .filter((entry) => entry.status === "passed" || entry.status === "focused") + .filter(isPassingValidation) .map((entry) => entry.command); } diff --git a/src/signals/local-workspace-intelligence.ts b/src/signals/local-workspace-intelligence.ts index 40c7320d98..07f326a48d 100644 --- a/src/signals/local-workspace-intelligence.ts +++ b/src/signals/local-workspace-intelligence.ts @@ -1,3 +1,4 @@ +import { isPassingValidation } from "./local-branch"; import type { LocalBranchAnalysis, LocalBranchAnalysisInput, LocalBranchChangedFile, LocalBranchValidation } from "./local-branch"; import { isTestPath } from "./test-evidence"; import { sanitizeLocalScorerWarnings } from "./local-scorer-diagnostics"; @@ -55,7 +56,7 @@ export function buildLocalWorkspaceIntelligence(args: { }): LocalWorkspaceIntelligence { const validation = args.input.validation ?? []; const testFileCount = args.changedFiles.filter((file) => isTestPath(file.path)).length; - const passedValidationCount = validation.filter((entry) => entry.status === "passed").length; + const passedValidationCount = validation.filter(isPassingValidation).length; const hasTestFiles = testFileCount > 0; const hasValidation = passedValidationCount > 0; const testEvidenceLevel = hasTestFiles && hasValidation ? "both" : hasTestFiles ? "test_files" : hasValidation ? "validation_commands" : "none"; diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index be7bfb47ab..9fc52aa8c1 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { SCENARIO_MAX_BRANCH_REF_CHARS, SCENARIO_MAX_LINKED_ISSUE_NUMBERS } from "../../src/scenarios/input-model"; -import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../../src/signals/local-branch"; +import { buildLocalBranchAnalysis, findCurrentBranchPullRequest, isPassingValidation } from "../../src/signals/local-branch"; import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../../src/signals/local-scorer-diagnostics"; import type { ContributorOutcomeHistory, ContributorProfile, ContributorScoringProfile, IssueQualityReport } from "../../src/signals/engine"; import type { RepositoryRecord, ScoringModelSnapshotRecord } from "../../src/types"; @@ -1117,6 +1117,46 @@ describe("local branch analysis", () => { expect(analysis.recommendedRerunCondition).toMatch(/git fetch origin/i); }); + it("treats a focused validation run as passing test evidence across every surface (regression)", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + baseRef: "origin/main", + baseSha: "base", + headSha: "head", + remoteTrackingSha: "base", + body: "Fixes #7", + changedFiles: [{ path: "internal/entity/model.go", additions: 10, deletions: 2, status: "modified" }], + // A focused subset run (`vitest run path`) is green evidence; summarizeValidation/validationEvidence + // already count it, so the finding and the v2 workspace count must agree instead of dropping it. + validation: [{ command: "vitest run internal/entity/model.test.ts", status: "focused", summary: "focused subset passed" }], + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Entity model edge case", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.prPacket.validationSummary.passed).toBe(1); + expect(analysis.baseFreshness.passedValidationCount).toBe(1); + expect(analysis.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "validation_as_test_evidence" })])); + expect(analysis.workspaceIntelligence.testEvidence.passedValidationCount).toBe(1); + expect(analysis.workspaceIntelligence.testEvidence.level).toBe("validation_commands"); + expect(analysis.preflight.findings.map((finding) => finding.code)).not.toContain("local_diff_missing_tests"); + }); + + it("counts only green validation statuses (passed or focused) as passing evidence", () => { + expect(isPassingValidation({ command: "npm test", status: "passed" })).toBe(true); + expect(isPassingValidation({ command: "vitest run x", status: "focused" })).toBe(true); + for (const status of ["failed", "not_run", "skipped", "unknown"] as const) { + expect(isPassingValidation({ command: "npm test", status })).toBe(false); + } + }); + it("treats focused validation as evidence and failed validation as actionable", () => { const analysis = buildLocalBranchAnalysis({ input: { diff --git a/test/unit/local-workspace-intelligence.test.ts b/test/unit/local-workspace-intelligence.test.ts index 22675ae806..5b2511d3a9 100644 --- a/test/unit/local-workspace-intelligence.test.ts +++ b/test/unit/local-workspace-intelligence.test.ts @@ -136,6 +136,40 @@ describe("local workspace intelligence v2", () => { expect(preflight.findings.map((finding) => finding.code)).not.toContain("local_diff_missing_tests"); }); + it("treats a focused validation run as test evidence, matching the PR-packet summary", () => { + const intelligence = buildLocalWorkspaceIntelligence({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + changedFiles: [{ path: "internal/entity/model.go", status: "modified" }], + validation: [{ command: "vitest run internal/entity/model.test.ts", status: "focused", summary: "focused subset passed" }], + }, + analysis: { + baseFreshness: { status: "fresh", changedFileCount: 1, testFileCount: 0, passedValidationCount: 1, warnings: [] }, + branchQualityBlockers: [], + accountStateBlockers: [], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + prPacket: { + titleSuggestion: "Entity model fix", + markdown: "# Entity model fix\n", + bodySections: [], + reviewerNotes: [], + validationSummary: { + passed: 1, + failed: 0, + notRun: 0, + commands: [{ command: "vitest run internal/entity/model.test.ts", status: "focused", summary: "focused subset passed" }], + }, + publicSafeWarnings: [], + }, + }, + changedFiles: [{ path: "internal/entity/model.go", status: "modified" }], + }); + + expect(intelligence.testEvidence.passedValidationCount).toBe(1); + expect(intelligence.testEvidence.level).toBe("validation_commands"); + }); + it("records metadata-only scorer diagnostics when no external scorer is configured", () => { const intelligence = buildLocalWorkspaceIntelligence({ input: {