diff --git a/src/api/routes.ts b/src/api/routes.ts index 3db0c2aa8b..f8334b3b84 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -621,6 +621,7 @@ const repositorySettingsSchema = z.object({ aiReviewProvider: z.enum(["anthropic", "openai"]).nullable().optional(), aiReviewModel: z.string().trim().min(1).max(120).nullable().optional(), aiReviewAllAuthors: z.boolean().default(false), + closeOwnerAuthors: z.boolean().default(false), autoLabelEnabled: z.boolean().default(true), gittensorLabel: z.string().trim().min(1).max(50).default("gittensor"), blacklistLabel: z.string().trim().min(1).max(50).default("slop"), @@ -675,6 +676,7 @@ const maintainerSettingsSchema = z blacklistLabel: z.string().trim().min(1).max(50), createMissingLabel: z.boolean(), includeMaintainerAuthors: z.boolean(), + closeOwnerAuthors: z.boolean(), requireLinkedIssue: z.boolean(), badgeEnabled: z.boolean(), agentPaused: z.boolean(), @@ -713,6 +715,7 @@ const repositoryAiReviewSchema = z.object({ provider: z.enum(["anthropic", "openai"]).nullable().optional(), model: z.string().trim().min(1).max(120).nullable().optional(), allAuthors: z.boolean().default(false), + closeOwnerAuthors: z.boolean().default(false), }); const contributorIssueDraftGenerateSchema = z.object({ @@ -2250,6 +2253,7 @@ export function createApp() { aiReviewProvider: parsed.data.provider, aiReviewModel: parsed.data.model, aiReviewAllAuthors: parsed.data.allAuthors, + closeOwnerAuthors: parsed.data.closeOwnerAuthors, }); // getRepositorySettings normalizes these to a concrete value or null (never undefined). return c.json({ @@ -3387,6 +3391,7 @@ export function createApp() { aiReviewProvider: parsed.data.aiReviewProvider, aiReviewModel: parsed.data.aiReviewModel, aiReviewAllAuthors: parsed.data.aiReviewAllAuthors, + closeOwnerAuthors: parsed.data.closeOwnerAuthors, autoLabelEnabled: parsed.data.autoLabelEnabled, gittensorLabel: parsed.data.gittensorLabel, blacklistLabel: parsed.data.blacklistLabel, diff --git a/test/unit/routes-ai-byok.test.ts b/test/unit/routes-ai-byok.test.ts index ca75d028ec..5fe0dcde82 100644 --- a/test/unit/routes-ai-byok.test.ts +++ b/test/unit/routes-ai-byok.test.ts @@ -35,19 +35,33 @@ describe("maintainer AI-review config route", () => { await upsertRepositorySettings(env, { repoFullName: REPO, gateCheckMode: "enabled", gittensorLabel: "custom-label", blacklistLabel: "abuse" }); const res = await app.request( `/v1/repos/${REPO}/ai-review`, - { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ mode: "block", byok: true, provider: "anthropic", model: "claude-3-5-sonnet-latest", allAuthors: true }) }, + { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ mode: "block", byok: true, provider: "anthropic", model: "claude-3-5-sonnet-latest", allAuthors: true, closeOwnerAuthors: true }) }, env, ); expect(res.status).toBe(200); - expect(await res.json()).toMatchObject({ aiReviewMode: "block", aiReviewByok: true, aiReviewProvider: "anthropic", aiReviewModel: "claude-3-5-sonnet-latest", aiReviewAllAuthors: true , closeOwnerAuthors: false}); + expect(await res.json()).toMatchObject({ aiReviewMode: "block", aiReviewByok: true, aiReviewProvider: "anthropic", aiReviewModel: "claude-3-5-sonnet-latest", aiReviewAllAuthors: true, closeOwnerAuthors: true }); const settings = await getRepositorySettings(env, REPO); expect(settings.aiReviewMode).toBe("block"); expect(settings.aiReviewAllAuthors).toBe(true); // persisted + read back (DB column round-trip) + expect(settings.closeOwnerAuthors).toBe(true); // persisted + read back (DB column round-trip) expect(settings.gateCheckMode).toBe("enabled"); // preserved expect(settings.gittensorLabel).toBe("custom-label"); // preserved expect(settings.blacklistLabel).toBe("abuse"); // #1425 round-trips through the DB }); + it("defaults closeOwnerAuthors off when the AI-review config omits it", async () => { + const app = createApp(); + const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); + const res = await app.request( + `/v1/repos/${REPO}/ai-review`, + { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ mode: "block", byok: true, provider: "anthropic", model: "claude-3-5-sonnet-latest", allAuthors: true }) }, + env, + ); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ closeOwnerAuthors: false }); + expect((await getRepositorySettings(env, REPO)).closeOwnerAuthors).toBe(false); + }); + it("accepts a config without provider/model (stored as null)", async () => { const app = createApp(); const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); @@ -62,6 +76,27 @@ describe("maintainer AI-review config route", () => { const res = await app.request(`/v1/repos/${REPO}/ai-review`, { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ mode: "loud" }) }, env); expect(res.status).toBe(400); }); + + it("lets maintainer settings set closeOwnerAuthors without resetting unrelated fields", async () => { + const app = createApp(); + const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); + await upsertRepositorySettings(env, { repoFullName: REPO, gateCheckMode: "enabled", gittensorLabel: "custom-label" }); + const res = await app.request(`/v1/repos/${REPO}/settings`, { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ closeOwnerAuthors: true }) }, env); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ closeOwnerAuthors: true, gateCheckMode: "enabled", gittensorLabel: "custom-label" }); + const settings = await getRepositorySettings(env, REPO); + expect(settings.closeOwnerAuthors).toBe(true); + expect(settings.gateCheckMode).toBe("enabled"); + }); + + it("lets the internal full settings route persist closeOwnerAuthors", async () => { + const app = createApp(); + const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET }); + const res = await app.request(`/v1/internal/repos/${REPO}/settings`, { method: "POST", headers: { authorization: `Bearer ${env.INTERNAL_JOB_TOKEN}`, "content-type": "application/json" }, body: JSON.stringify({ closeOwnerAuthors: true }) }, env); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ closeOwnerAuthors: true }); + expect((await getRepositorySettings(env, REPO)).closeOwnerAuthors).toBe(true); + }); }); describe("maintainer BYOK key route", () => {