From e72b012f1bf500a6dbb1f6c00d7977cd3d0b8dba Mon Sep 17 00:00:00 2001 From: galuis116 Date: Sun, 19 Jul 2026 08:35:02 -0400 Subject: [PATCH] fix(engine): cap idea-intake acceptanceHints entry length acceptanceHints is a renter-supplied string[] of the same shape as constraints, which already enforces IDEA_CONSTRAINT_MAX_CHARS per entry. acceptanceHints had no equivalent bound, so an unbounded entry could dominate the public acceptance-criteria surface it's later folded into, contradicting the module's own stated invariant that every renter freeform text field is length-capped. Closes #7243 --- packages/loopover-engine/src/idea-intake.ts | 5 ++-- test/unit/idea-intake-bridge.test.ts | 29 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/loopover-engine/src/idea-intake.ts b/packages/loopover-engine/src/idea-intake.ts index 0f3b98f361..a5f8d14eb6 100644 --- a/packages/loopover-engine/src/idea-intake.ts +++ b/packages/loopover-engine/src/idea-intake.ts @@ -102,8 +102,9 @@ export function validateIdeaSubmission(raw: unknown): IdeaValidationResult { else if (constraints.some((c) => c.length > IDEA_CONSTRAINT_MAX_CHARS)) errors.push("constraint_too_long"); } const acceptanceHints = input.acceptanceHints; - if (acceptanceHints !== undefined && (!Array.isArray(acceptanceHints) || !acceptanceHints.every((h) => typeof h === "string"))) { - errors.push("acceptance_hints_invalid"); + if (acceptanceHints !== undefined) { + if (!Array.isArray(acceptanceHints) || !acceptanceHints.every((h) => typeof h === "string")) errors.push("acceptance_hints_invalid"); + else if (acceptanceHints.some((h) => h.length > IDEA_CONSTRAINT_MAX_CHARS)) errors.push("acceptance_hint_too_long"); } const priority = input.priority; if (priority !== undefined && priority !== "normal" && priority !== "high") errors.push("priority_invalid"); diff --git a/test/unit/idea-intake-bridge.test.ts b/test/unit/idea-intake-bridge.test.ts index c8f0d4875d..c057f8453f 100644 --- a/test/unit/idea-intake-bridge.test.ts +++ b/test/unit/idea-intake-bridge.test.ts @@ -73,6 +73,35 @@ describe("validateIdeaSubmission", () => { expect(validateIdeaSubmission(validIdea({ priority: "urgent" as unknown as IdeaSubmission["priority"] })).ok).toBe(false); expect(validateIdeaSubmission(validIdea({ priority: "normal" })).ok).toBe(true); }); + + it("caps acceptanceHints entry length at IDEA_CONSTRAINT_MAX_CHARS, same bound as constraints (#7243)", () => { + const atCap = validateIdeaSubmission(validIdea({ acceptanceHints: ["h".repeat(IDEA_CONSTRAINT_MAX_CHARS)] })); + expect(atCap.ok).toBe(true); + + const overCap = validateIdeaSubmission(validIdea({ acceptanceHints: ["h".repeat(IDEA_CONSTRAINT_MAX_CHARS + 1)] })); + expect(overCap.ok).toBe(false); + if (!overCap.ok) expect(overCap.errors).toContain("acceptance_hint_too_long"); + }); + + it("does not raise acceptance_hint_too_long for a malformed (non-array) acceptanceHints — shape error only", () => { + const r = validateIdeaSubmission(validIdea({ acceptanceHints: "x".repeat(IDEA_CONSTRAINT_MAX_CHARS + 1) as unknown as string[] })); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.errors).toContain("acceptance_hints_invalid"); + expect(r.errors).not.toContain("acceptance_hint_too_long"); + } + }); + + it("collects an over-length acceptanceHints entry alongside other unrelated errors in one pass", () => { + const r = validateIdeaSubmission({ + title: "t", + body: "b", + targetRepo: "o/n", + acceptanceHints: ["h".repeat(IDEA_CONSTRAINT_MAX_CHARS + 1)], + }); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.errors).toEqual(expect.arrayContaining(["id_required", "acceptance_hint_too_long"])); + }); }); describe("buildTaskGraph — spec §4 Example A (simple idea → one issue → go)", () => {