diff --git a/packages/loopover-miner/lib/idea-feasibility-cli.js b/packages/loopover-miner/lib/idea-feasibility-cli.js index 8350656830..c93896d733 100644 --- a/packages/loopover-miner/lib/idea-feasibility-cli.js +++ b/packages/loopover-miner/lib/idea-feasibility-cli.js @@ -33,7 +33,9 @@ export function parseIdeaFeasibilityArgs(args) { } if (token === "--hint") { const value = args[i + 1]; - if (value === undefined || value.startsWith("-")) { + // A whitespace-only hint is as empty as a missing one (#6766): it declares no testable success signal, so + // it gets the same rejection rather than sailing through as a real objective signal. + if (value === undefined || value.startsWith("-") || value.trim() === "") { return { error: "--hint requires a value." }; } options.acceptanceHints.push(value); diff --git a/packages/loopover-miner/lib/idea-feasibility.js b/packages/loopover-miner/lib/idea-feasibility.js index 0c6d24263f..0fed105d9c 100644 --- a/packages/loopover-miner/lib/idea-feasibility.js +++ b/packages/loopover-miner/lib/idea-feasibility.js @@ -36,7 +36,11 @@ export function deriveIdeaIssueStatus(idea, resolved) { // Out of the loop's scope: the idea does not resolve to a repo the loop can act on. if (!resolved.targetResolvable) return "missing"; // Impossible to evaluate objectively: no declared success signal, so the loop could never test its own output. - const objectiveSignals = idea.acceptanceHints?.length ?? 0; + // Count CONTENT, not array length (#6766): a blank/whitespace-only hint declares nothing testable, so it must + // not pass as an objective signal just by occupying a slot. + const objectiveSignals = (idea.acceptanceHints ?? []).filter( + (hint) => typeof hint === "string" && hint.trim() !== "", + ).length; if (objectiveSignals === 0) return "invalid"; return "ready"; } diff --git a/test/unit/miner-idea-feasibility-cli.test.ts b/test/unit/miner-idea-feasibility-cli.test.ts index 6ff0f96aa7..99ff4e7278 100644 --- a/test/unit/miner-idea-feasibility-cli.test.ts +++ b/test/unit/miner-idea-feasibility-cli.test.ts @@ -71,6 +71,15 @@ describe("parseIdeaFeasibilityArgs (#5671)", () => { }); }); + it("REGRESSION (#6766): rejects a whitespace-only --hint the same way as a missing one", () => { + // A blank hint declares no testable success signal, so it must not sail through as a real one. + for (const blank of [" ", "\t", " \n "]) { + expect(parseIdeaFeasibilityArgs(["unclaimed", "none", "--hint", blank])).toEqual({ + error: "--hint requires a value.", + }); + } + }); + it("rejects --hint whose value looks like another flag", () => { expect(parseIdeaFeasibilityArgs(["unclaimed", "none", "--hint", "--json"])).toEqual({ error: "--hint requires a value.", diff --git a/test/unit/miner-idea-feasibility.test.ts b/test/unit/miner-idea-feasibility.test.ts index 3eaaa352ea..61f194d541 100644 --- a/test/unit/miner-idea-feasibility.test.ts +++ b/test/unit/miner-idea-feasibility.test.ts @@ -28,6 +28,17 @@ describe("deriveIdeaIssueStatus (#5671)", () => { expect(deriveIdeaIssueStatus({ acceptanceHints: [] }, { targetResolvable: true })).toBe("invalid"); }); + it("REGRESSION (#6766): returns 'invalid' for blank/whitespace-only hints — a slot is not a signal", () => { + // The count used to be array LENGTH, so a whitespace-only hint declared nothing testable yet passed as + // "ready", contradicting the module's own "no objective success signal is invalid" contract. + expect(deriveIdeaIssueStatus({ acceptanceHints: [" "] }, { targetResolvable: true })).toBe("invalid"); + expect(deriveIdeaIssueStatus({ acceptanceHints: ["", "\t", "\n "] }, { targetResolvable: true })).toBe("invalid"); + }); + + it("REGRESSION (#6766): a real hint alongside blank ones still counts as an objective signal", () => { + expect(deriveIdeaIssueStatus({ acceptanceHints: [" ", "retries on 5xx"] }, { targetResolvable: true })).toBe("ready"); + }); + it("returns 'ready' when the idea resolves and carries at least one objective success signal", () => { expect(deriveIdeaIssueStatus({ acceptanceHints: ["uploads retry on 5xx"] }, { targetResolvable: true })).toBe("ready"); });