diff --git a/packages/loopover-engine/src/focus-manifest/guidance.ts b/packages/loopover-engine/src/focus-manifest/guidance.ts index d3303f0f73..64fccf3b17 100644 --- a/packages/loopover-engine/src/focus-manifest/guidance.ts +++ b/packages/loopover-engine/src/focus-manifest/guidance.ts @@ -3,6 +3,7 @@ import type { FocusManifestFinding, FocusManifestGuidance, } from "../types/predicted-gate-types.js"; +import { isCodeFile } from "../signals/path-matchers.js"; const FOCUS_MANIFEST_TERMS = /\b(reward\w*|score\w*|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|farming|payouts?|rankings?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|private[-\s]?reviewability|reviewability(?:[-\s]?internals?)?|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)s?|estimated[-\s]?scores?|score[-\s]?(?:estimate|prediction|preview)s?)\b/i; const FOCUS_MANIFEST_LOCAL_PATH_PATTERN = new RegExp(String.raw`/Users/|/home/|/root/|/var/|/opt/|/tmp/|/private/|[A-Za-z]:[\\/]Users[\\/]|[A-Za-z]:[\\/]Program Files[\\/]`, "i"); @@ -124,6 +125,7 @@ export function buildFocusManifestGuidance(args: { const linkedIssueCount = Math.max(0, args.linkedIssueCount ?? 0); const testFileCount = Math.max(0, args.testFileCount ?? 0); const passedValidationCount = Math.max(0, args.passedValidationCount ?? 0); + const codeFileCount = changedPaths.filter(isCodeFile).length; const matchedWantedPaths = matchedPatterns(changedPaths, manifest.wantedPaths); const preferredLabelHits = manifest.preferredLabels.filter((label) => labels.includes(label.toLowerCase())); @@ -201,7 +203,7 @@ export function buildFocusManifestGuidance(args: { publicNextSteps.push("Link a tracked issue if one exists; the maintainer prefers linked issues."); } - if (manifest.testExpectations.length > 0 && testFileCount === 0 && passedValidationCount === 0) { + if (manifest.testExpectations.length > 0 && codeFileCount > 0 && testFileCount === 0 && passedValidationCount === 0) { const safeExpectations = manifest.testExpectations.filter(isFocusManifestPublicSafe).slice(0, 3); const expectationDetail = safeExpectations.length > 0 ? ` Expected evidence: ${safeExpectations.join("; ")}.` : ""; findings.push({ diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index fcdeee392d..8b648145a0 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -85,7 +85,7 @@ import { DEFAULT_LINKED_ISSUE_HARD_RULES } from "../review/linked-issue-hard-rul import { DEFAULT_UNLINKED_ISSUE_GUARDRAIL } from "../review/unlinked-issue-guardrail-config"; import { DEFAULT_ADVISORY_AI_ROUTING } from "../review/advisory-ai-routing-config"; import { DEFAULT_SCREENSHOT_TABLE_GATE } from "../review/screenshot-table-gate"; -import { classifyChangedFile } from "./path-matchers"; +import { classifyChangedFile, isCodeFile } from "./path-matchers"; import { EMPTY_AUTO_REVIEW_CONFIG, EMPTY_MAX_FINDINGS_CONFIG, @@ -658,6 +658,7 @@ export function buildFocusManifestGuidance(args: { const hasNoIssueRationale = args.hasNoIssueRationale ?? false; const testFileCount = Math.max(0, args.testFileCount ?? 0); const passedValidationCount = Math.max(0, args.passedValidationCount ?? 0); + const codeFileCount = changedPaths.filter(isCodeFile).length; const matchedWantedPaths = matchedPatterns(changedPaths, manifest.wantedPaths); const preferredLabelHits = manifest.preferredLabels.filter((label) => labels.includes(label.toLowerCase())); @@ -735,7 +736,7 @@ export function buildFocusManifestGuidance(args: { publicNextSteps.push("Link a tracked issue if one exists; the maintainer prefers linked issues."); } - if (manifest.testExpectations.length > 0 && testFileCount === 0 && passedValidationCount === 0) { + if (manifest.testExpectations.length > 0 && codeFileCount > 0 && testFileCount === 0 && passedValidationCount === 0) { const safeExpectations = manifest.testExpectations.filter(isFocusManifestPublicSafe).slice(0, 3); const expectationDetail = safeExpectations.length > 0 ? ` Expected evidence: ${safeExpectations.join("; ")}.` : ""; findings.push({ diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 6dc50e0aca..b3415a473a 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -660,6 +660,25 @@ describe("buildFocusManifestGuidance", () => { expect(guidance.findings.some((finding) => finding.code === "manifest_missing_tests")).toBe(false); }); + it("REGRESSION (#manifest-missing-tests-docs-only-false-positive): does not require tests when the change touches no code files", () => { + // A content/docs-only PR (e.g. a catalog registry .mdx entry) has nothing a test could meaningfully cover, + // so it must not trip manifest_missing_tests just because no test file or validation evidence exists -- + // mirrors the codeFileCount > 0 guard local_diff_missing_tests already applies (predicted-gate-engine.ts). + const guidance = buildFocusManifestGuidance({ manifest: wanted, changedPaths: ["docs/readme.md"], linkedIssueCount: 1, testFileCount: 0, passedValidationCount: 0 }); + expect(guidance.findings.some((finding) => finding.code === "manifest_missing_tests")).toBe(false); + }); + + it("still requires tests when the change mixes code with docs and neither tests nor validation evidence exist", () => { + const guidance = buildFocusManifestGuidance({ + manifest: wanted, + changedPaths: ["docs/readme.md", "src/x.ts"], + linkedIssueCount: 1, + testFileCount: 0, + passedValidationCount: 0, + }); + expect(guidance.findings.some((finding) => finding.code === "manifest_missing_tests")).toBe(true); + }); + it("notes when issue-discovery is discouraged", () => { const guidance = buildFocusManifestGuidance({ manifest: wanted, changedPaths: ["src/x.ts"], labels: ["bug"], linkedIssueCount: 1, testFileCount: 1 }); expect(guidance.findings.some((finding) => finding.code === "manifest_issue_discovery_discouraged")).toBe(true); diff --git a/test/unit/predicted-gate-engine-coverage.test.ts b/test/unit/predicted-gate-engine-coverage.test.ts index c1e15ab476..f2aa5ad999 100644 --- a/test/unit/predicted-gate-engine-coverage.test.ts +++ b/test/unit/predicted-gate-engine-coverage.test.ts @@ -821,6 +821,32 @@ describe("predicted-gate engine module coverage (#2283)", () => { }); expect(missingTests.findings.some((f) => f.code === "manifest_missing_tests")).toBe(true); expect(missingTests.findings.find((f) => f.code === "manifest_missing_tests")?.detail).not.toContain("wallet"); + + // REGRESSION (#manifest-missing-tests-docs-only-false-positive): a docs/content-only change has nothing a + // test could cover, so it must not trip manifest_missing_tests just because no test file or validation + // evidence exists. + const docsOnlyNoFalsePositive = buildFocusManifestGuidance({ + manifest: { + present: true, + source: "repo_file", + wantedPaths: [], + preferredLabels: [], + linkedIssuePolicy: "optional", + testExpectations: ["paste your wallet hotkey here"], + issueDiscoveryPolicy: "neutral", + maintainerNotes: [], + publicNotes: [], + gate: { present: true } as FocusManifest["gate"], + settings: {}, + review: { present: true, preMergeChecks: [] }, + warnings: [], + }, + changedPaths: ["content/registry/new-entry.mdx"], + linkedIssueCount: 1, + testFileCount: 0, + passedValidationCount: 0, + }); + expect(docsOnlyNoFalsePositive.findings.some((f) => f.code === "manifest_missing_tests")).toBe(false); }); it("covers remaining codecov patch branch arms in ported engine modules", () => { diff --git a/test/unit/queue-2.test.ts b/test/unit/queue-2.test.ts index 6306762608..6ca9e1a43e 100644 --- a/test/unit/queue-2.test.ts +++ b/test/unit/queue-2.test.ts @@ -4028,7 +4028,11 @@ describe("queue processors", () => { await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 47, - path: "README.md", + // A code file, deliberately -- this test's subject is whether an ignored-bot author still trips the + // deterministic manifestPolicyGateMode gate, not manifest_missing_tests' own code-vs-docs trigger + // (see the codeFileCount > 0 guard in buildFocusManifestGuidance). A docs-only path would no longer + // trip the finding at all and silently stop testing this test's actual subject. + path: "src/example.ts", status: "modified", additions: 1, deletions: 1,