Skip to content

Commit 55d1e4b

Browse files
authored
intake(idea): accept the IdeaTarget object shapes validateIdeaSubmission already supports (#10070)
Fixes #10064 Co-authored-by: phamngocquy <phamngocquy@users.noreply.github.com>
1 parent 582aa25 commit 55d1e4b

6 files changed

Lines changed: 47 additions & 9 deletions

File tree

apps/loopover-ui/public/openapi.json

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14906,7 +14906,18 @@
1490614906
"type": "string"
1490714907
},
1490814908
"targetRepo": {
14909-
"type": "string"
14909+
"anyOf": [
14910+
{
14911+
"type": "string"
14912+
},
14913+
{
14914+
"type": "object",
14915+
"properties": {},
14916+
"additionalProperties": {
14917+
"nullable": true
14918+
}
14919+
}
14920+
]
1491014921
},
1491114922
"constraints": {
1491214923
"type": "array",
@@ -14998,7 +15009,18 @@
1499815009
"type": "string"
1499915010
},
1500015011
"targetRepo": {
15001-
"type": "string"
15012+
"anyOf": [
15013+
{
15014+
"type": "string"
15015+
},
15016+
{
15017+
"type": "object",
15018+
"properties": {},
15019+
"additionalProperties": {
15020+
"nullable": true
15021+
}
15022+
}
15023+
]
1500215024
},
1500315025
"constraints": {
1500415026
"type": "array",

packages/loopover-contract/src/api-requests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export const intakeIdeaSchema = z.object({
165165
id: z.string().optional(),
166166
title: z.string().optional(),
167167
body: z.string().optional(),
168-
targetRepo: z.string().optional(),
168+
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
169169
constraints: z.array(z.string()).max(50).optional(),
170170
acceptanceHints: z.array(z.string()).max(50).optional(),
171171
priority: z.string().optional(),

packages/loopover-contract/src/tools/agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const IntakeIdeaInput = z.object({
3030
id: z.string().optional(),
3131
title: z.string().optional(),
3232
body: z.string().optional(),
33-
targetRepo: z.string().optional(),
33+
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
3434
constraints: z.array(z.string()).max(50).optional(),
3535
acceptanceHints: z.array(z.string()).max(50).optional(),
3636
priority: z.string().optional(),

packages/loopover-engine/test/idea-intake.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test } from "node:test";
22
import assert from "node:assert/strict";
33

4-
import { validateIdeaSubmission } from "../dist/index.js";
4+
import { buildClaimPlan, buildTaskGraph, validateIdeaSubmission } from "../dist/index.js";
55

66
// Engine-suite (node:test) coverage for validateIdeaSubmission's targetRepo resolution (#9609) so the
77
// `engine` Codecov flag credits the changed lines, mirroring test/unit/idea-intake-bridge.test.ts.
@@ -21,10 +21,13 @@ test("accepts the canonical { kind: 'existing', repo } object it returns (round-
2121
if (r.ok) assert.deepEqual(r.idea.targetRepo, { kind: "existing", repo: "acme/widgets" });
2222
});
2323

24-
test("accepts a provision object", () => {
24+
test("accepts a provision object and buildClaimPlan carries an empty targetRepo", () => {
2525
const r = validateIdeaSubmission(rawIdea({ kind: "provision" }));
2626
assert.equal(r.ok, true);
27-
if (r.ok) assert.deepEqual(r.idea.targetRepo, { kind: "provision" });
27+
if (!r.ok) return;
28+
assert.deepEqual(r.idea.targetRepo, { kind: "provision" });
29+
const plan = buildClaimPlan(buildTaskGraph(r.idea), r.idea.targetRepo);
30+
assert.equal(plan.targetRepo, "");
2831
});
2932

3033
test("rejects a malformed slug in both the string and the existing-object form", () => {

src/openapi/schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,7 +2287,7 @@ export const IntakeIdeaRequestSchema = z
22872287
id: z.string().optional(),
22882288
title: z.string().optional(),
22892289
body: z.string().optional(),
2290-
targetRepo: z.string().optional(),
2290+
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
22912291
constraints: z.array(z.string()).max(50).optional(),
22922292
acceptanceHints: z.array(z.string()).max(50).optional(),
22932293
priority: z.string().optional(),
@@ -2322,7 +2322,7 @@ export const PlanIdeaClaimsRequestSchema = z
23222322
id: z.string().optional(),
23232323
title: z.string().optional(),
23242324
body: z.string().optional(),
2325-
targetRepo: z.string().optional(),
2325+
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
23262326
constraints: z.array(z.string()).max(50).optional(),
23272327
acceptanceHints: z.array(z.string()).max(50).optional(),
23282328
priority: z.string().optional(),

test/unit/contract-api-requests.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SCENARIO_MAX_LINKED_ISSUE_NUMBERS,
2828
SCENARIO_MAX_REPO_FULL_NAME_CHARS,
2929
} from "@loopover/contract";
30+
import { validateIdeaSubmission } from "@loopover/engine";
3031
import { MAX_NOTIFICATION_DELIVERY_ID_LENGTH as SRC_DELIVERY_ID, MAX_NOTIFICATION_MARK_READ_IDS as SRC_MARK_READ } from "../../src/db/repositories";
3132
import { MAX_FOCUS_MANIFEST_BYTES as SRC_MANIFEST_BYTES } from "../../src/signals/focus-manifest";
3233
import { MAX_LOCAL_SCORER_WARNING_CHARS as SRC_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT as SRC_WARNING_COUNT } from "../../src/signals/local-scorer-diagnostics";
@@ -108,6 +109,18 @@ describe("the moved schemas accept and reject what they always did (#9750)", ()
108109
// empty submission must REACH the handler rather than be rejected by the schema.
109110
expect(intakeIdeaSchema.safeParse({}).success).toBe(true);
110111
expect(intakeIdeaSchema.safeParse({ constraints: Array.from({ length: 51 }, () => "c") }).success).toBe(false);
112+
expect(intakeIdeaSchema.safeParse({ targetRepo: { kind: "provision" } }).success).toBe(true);
113+
expect(intakeIdeaSchema.safeParse({ targetRepo: { kind: "existing", repo: "acme/widgets" } }).success).toBe(true);
114+
expect(intakeIdeaSchema.safeParse({ targetRepo: "acme/widgets" }).success).toBe(true);
115+
expect(intakeIdeaSchema.safeParse({ targetRepo: 42 }).success).toBe(false);
116+
});
117+
118+
it("#10064: malformed object targetRepo passes the schema but is rejected by validateIdeaSubmission", () => {
119+
const body = { targetRepo: { kind: "existing" } };
120+
expect(intakeIdeaSchema.safeParse(body).success).toBe(true);
121+
const validated = validateIdeaSubmission(body);
122+
expect(validated.ok).toBe(false);
123+
if (!validated.ok) expect(validated.errors).toContain("target_repo_required");
111124
});
112125

113126
it("leaves check-before-start entirely optional — the repository is the path param, not the body", () => {

0 commit comments

Comments
 (0)