Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14906,7 +14906,18 @@
"type": "string"
},
"targetRepo": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {
"nullable": true
}
}
]
},
"constraints": {
"type": "array",
Expand Down Expand Up @@ -14998,7 +15009,18 @@
"type": "string"
},
"targetRepo": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {},
"additionalProperties": {
"nullable": true
}
}
]
},
"constraints": {
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion packages/loopover-contract/src/api-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export const intakeIdeaSchema = z.object({
id: z.string().optional(),
title: z.string().optional(),
body: z.string().optional(),
targetRepo: z.string().optional(),
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
constraints: z.array(z.string()).max(50).optional(),
acceptanceHints: z.array(z.string()).max(50).optional(),
priority: z.string().optional(),
Expand Down
2 changes: 1 addition & 1 deletion packages/loopover-contract/src/tools/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const IntakeIdeaInput = z.object({
id: z.string().optional(),
title: z.string().optional(),
body: z.string().optional(),
targetRepo: z.string().optional(),
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
constraints: z.array(z.string()).max(50).optional(),
acceptanceHints: z.array(z.string()).max(50).optional(),
priority: z.string().optional(),
Expand Down
9 changes: 6 additions & 3 deletions packages/loopover-engine/test/idea-intake.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";

import { validateIdeaSubmission } from "../dist/index.js";
import { buildClaimPlan, buildTaskGraph, validateIdeaSubmission } from "../dist/index.js";

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

test("accepts a provision object", () => {
test("accepts a provision object and buildClaimPlan carries an empty targetRepo", () => {
const r = validateIdeaSubmission(rawIdea({ kind: "provision" }));
assert.equal(r.ok, true);
if (r.ok) assert.deepEqual(r.idea.targetRepo, { kind: "provision" });
if (!r.ok) return;
assert.deepEqual(r.idea.targetRepo, { kind: "provision" });
const plan = buildClaimPlan(buildTaskGraph(r.idea), r.idea.targetRepo);
assert.equal(plan.targetRepo, "");
});

test("rejects a malformed slug in both the string and the existing-object form", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2287,7 +2287,7 @@ export const IntakeIdeaRequestSchema = z
id: z.string().optional(),
title: z.string().optional(),
body: z.string().optional(),
targetRepo: z.string().optional(),
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
constraints: z.array(z.string()).max(50).optional(),
acceptanceHints: z.array(z.string()).max(50).optional(),
priority: z.string().optional(),
Expand Down Expand Up @@ -2322,7 +2322,7 @@ export const PlanIdeaClaimsRequestSchema = z
id: z.string().optional(),
title: z.string().optional(),
body: z.string().optional(),
targetRepo: z.string().optional(),
targetRepo: z.union([z.string(), z.looseObject({})]).optional(),
constraints: z.array(z.string()).max(50).optional(),
acceptanceHints: z.array(z.string()).max(50).optional(),
priority: z.string().optional(),
Expand Down
13 changes: 13 additions & 0 deletions test/unit/contract-api-requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SCENARIO_MAX_LINKED_ISSUE_NUMBERS,
SCENARIO_MAX_REPO_FULL_NAME_CHARS,
} from "@loopover/contract";
import { validateIdeaSubmission } from "@loopover/engine";
import { MAX_NOTIFICATION_DELIVERY_ID_LENGTH as SRC_DELIVERY_ID, MAX_NOTIFICATION_MARK_READ_IDS as SRC_MARK_READ } from "../../src/db/repositories";
import { MAX_FOCUS_MANIFEST_BYTES as SRC_MANIFEST_BYTES } from "../../src/signals/focus-manifest";
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";
Expand Down Expand Up @@ -108,6 +109,18 @@ describe("the moved schemas accept and reject what they always did (#9750)", ()
// empty submission must REACH the handler rather than be rejected by the schema.
expect(intakeIdeaSchema.safeParse({}).success).toBe(true);
expect(intakeIdeaSchema.safeParse({ constraints: Array.from({ length: 51 }, () => "c") }).success).toBe(false);
expect(intakeIdeaSchema.safeParse({ targetRepo: { kind: "provision" } }).success).toBe(true);
expect(intakeIdeaSchema.safeParse({ targetRepo: { kind: "existing", repo: "acme/widgets" } }).success).toBe(true);
expect(intakeIdeaSchema.safeParse({ targetRepo: "acme/widgets" }).success).toBe(true);
expect(intakeIdeaSchema.safeParse({ targetRepo: 42 }).success).toBe(false);
});

it("#10064: malformed object targetRepo passes the schema but is rejected by validateIdeaSubmission", () => {
const body = { targetRepo: { kind: "existing" } };
expect(intakeIdeaSchema.safeParse(body).success).toBe(true);
const validated = validateIdeaSubmission(body);
expect(validated.ok).toBe(false);
if (!validated.ok) expect(validated.errors).toContain("target_repo_required");
});

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