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
5 changes: 5 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 37 additions & 2 deletions test/unit/routes-ai-byok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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", () => {
Expand Down
Loading