diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 6becd7e0a1..43e01d3cc9 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -7940,6 +7940,9 @@ "qualityGateMinScore": { "type": "number", "nullable": true + }, + "firstTimeContributorGrace": { + "type": "boolean" } }, "required": [ @@ -7950,6 +7953,7 @@ "checkRunMode", "checkRunDetailLevel", "gateCheckMode", + "firstTimeContributorGrace", "linkedIssueGateMode", "duplicatePrGateMode", "qualityGateMode", diff --git a/migrations/0026_first_time_contributor_grace.sql b/migrations/0026_first_time_contributor_grace.sql new file mode 100644 index 0000000000..5a571f3886 --- /dev/null +++ b/migrations/0026_first_time_contributor_grace.sql @@ -0,0 +1 @@ +ALTER TABLE repository_settings ADD COLUMN first_time_contributor_grace INTEGER NOT NULL DEFAULT 0; diff --git a/src/api/routes.ts b/src/api/routes.ts index aa8ceff4ab..75a0a96287 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -504,6 +504,7 @@ const repositorySettingsSchema = z.object({ checkRunMode: z.enum(["off", "enabled"]).default("off"), checkRunDetailLevel: z.enum(["minimal", "standard", "deep"]).default("standard"), gateCheckMode: z.enum(["off", "enabled"]).default("off"), + firstTimeContributorGrace: z.boolean().default(false), linkedIssueGateMode: z.enum(["off", "advisory", "block"]).default("block"), duplicatePrGateMode: z.enum(["off", "advisory", "block"]).default("block"), qualityGateMode: z.enum(["off", "advisory", "block"]).default("advisory"), @@ -2440,6 +2441,7 @@ export function createApp() { checkRunMode: parsed.data.checkRunMode, checkRunDetailLevel: parsed.data.checkRunDetailLevel, gateCheckMode: parsed.data.gateCheckMode, + firstTimeContributorGrace: parsed.data.firstTimeContributorGrace, linkedIssueGateMode: parsed.data.linkedIssueGateMode, duplicatePrGateMode: parsed.data.duplicatePrGateMode, qualityGateMode: parsed.data.qualityGateMode, diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 6776329817..eee61eb84e 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -386,6 +386,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "block", duplicatePrGateMode: "block", qualityGateMode: "advisory", @@ -409,6 +410,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise checkRunMode: parseCheckRunMode(row.checkRunMode), checkRunDetailLevel: parseCheckRunDetailLevel(row.checkRunDetailLevel), gateCheckMode: parseGateCheckMode(row.gateCheckMode), + firstTimeContributorGrace: row.firstTimeContributorGrace, linkedIssueGateMode: parseGateRuleMode(row.linkedIssueGateMode), duplicatePrGateMode: parseGateRuleMode(row.duplicatePrGateMode), qualityGateMode: parseGateRuleMode(row.qualityGateMode), @@ -436,6 +438,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial>>; + repoFullName: string; + repoIssues: Awaited>; + repoPullRequests: PullRequestRecord[]; + }, +) { + const base = gateCheckPolicy(settings, readinessScore); + const normalizedAuthor = args.authorLogin.toLowerCase(); + const authorPullRequests = args.repoPullRequests.filter((entry) => entry.authorLogin?.toLowerCase() === normalizedAuthor); + const authorIssues = args.repoIssues.filter((entry) => entry.authorLogin?.toLowerCase() === normalizedAuthor); + const profile = buildContributorProfile( + args.authorLogin, + { login: args.authorLogin, topLanguages: [], source: "github" }, + authorPullRequests, + authorIssues, + ); + const roleContext = buildRoleContext({ + login: args.authorLogin, + repo: args.repo, + repoFullName: args.repoFullName, + pullRequests: authorPullRequests, + issues: authorIssues, + profile, + }); + const outcomeHistory = buildContributorOutcomeHistory({ + login: args.authorLogin, + profile, + repositories: [args.repo], + pullRequests: authorPullRequests, + issues: authorIssues, + repoStats: [], + }); + const repoOutcome = outcomeHistory.repoOutcomes.find((entry) => entry.repoFullName === args.repoFullName); + return resolveContributorGraceGatePolicy(base, settings, { roleContext, repoOutcome }); +} + function linkedIssueDuplicatePullRequestsForGate(pr: PullRequestRecord, pullRequests: PullRequestRecord[]): number[] { const linkedIssues = new Set(pr.linkedIssues); if (linkedIssues.size === 0) return []; @@ -955,9 +997,20 @@ async function maybePublishPrPublicSurface( scopedOverlapCount: unionScopedOverlapClusters(collisions, pr, preflight.collisions).length, }); - const gateEvaluation = settings.gateCheckMode === "enabled" ? evaluateGateCheck(advisory, gateCheckPolicy(settings, readiness.total)) : undefined; + const gatePolicy = + author && repo && settings.firstTimeContributorGrace + ? resolveGateCheckPolicy(settings, readiness.total, { + authorLogin: author, + repo, + repoFullName, + repoIssues, + repoPullRequests, + }) + : gateCheckPolicy(settings, readiness.total); + + const gateEvaluation = settings.gateCheckMode === "enabled" ? evaluateGateCheck(advisory, gatePolicy) : undefined; if (gateEnabled) { - const gateCheckResult = await createOrUpdateGateCheckRun(env, installationId, repoFullName, advisory, gateCheckPolicy(settings, readiness.total), { + const gateCheckResult = await createOrUpdateGateCheckRun(env, installationId, repoFullName, advisory, gatePolicy, { checkRunId: pendingGateCheckRunId, }); if (gateCheckResult?.kind === "permission_missing") { diff --git a/src/rules/gate-grace.ts b/src/rules/gate-grace.ts new file mode 100644 index 0000000000..f4b40f86b9 --- /dev/null +++ b/src/rules/gate-grace.ts @@ -0,0 +1,51 @@ +import type { GateRuleMode } from "../types"; +import type { ContributorOutcomeHistory, RoleContext } from "../signals/engine"; +import type { GateCheckPolicy } from "./advisory"; + +export function isRepeatClosedUnmergedAuthor( + repoOutcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined, +): boolean { + if (!repoOutcome) return false; + return repoOutcome.closedPullRequests > 0 && repoOutcome.mergedPullRequests === 0 && repoOutcome.pullRequests >= 2; +} + +export function shouldGrantFirstTimeContributorGrace(args: { + enabled: boolean; + roleContext: RoleContext; + repoOutcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined; +}): boolean { + if (!args.enabled) return false; + if (args.roleContext.maintainerLane) return false; + if (isRepeatClosedUnmergedAuthor(args.repoOutcome)) return false; + if (!args.repoOutcome) return true; + if (args.repoOutcome.pullRequests <= 1) return true; + return args.repoOutcome.mergedPullRequests === 0 && args.repoOutcome.closedPullRequests === 0; +} + +export function applyFirstTimeContributorGrace(policy: GateCheckPolicy): GateCheckPolicy { + return { + ...policy, + linkedIssueGateMode: downgradeBlockToAdvisory(policy.linkedIssueGateMode), + duplicatePrGateMode: downgradeBlockToAdvisory(policy.duplicatePrGateMode), + qualityGateMode: downgradeBlockToAdvisory(policy.qualityGateMode), + }; +} + +export function resolveContributorGraceGatePolicy( + base: GateCheckPolicy, + settings: { firstTimeContributorGrace: boolean }, + context: { + roleContext: RoleContext; + repoOutcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined; + }, +): GateCheckPolicy { + if (!settings.firstTimeContributorGrace) return base; + if (!shouldGrantFirstTimeContributorGrace({ enabled: true, roleContext: context.roleContext, repoOutcome: context.repoOutcome })) { + return base; + } + return applyFirstTimeContributorGrace(base); +} + +function downgradeBlockToAdvisory(mode: GateRuleMode | undefined): GateRuleMode | undefined { + return mode === "block" ? "advisory" : mode; +} diff --git a/src/types.ts b/src/types.ts index 4f405e81a8..3f4c782302 100644 --- a/src/types.ts +++ b/src/types.ts @@ -363,6 +363,7 @@ export type RepositorySettings = { checkRunMode: "off" | "enabled"; checkRunDetailLevel: "minimal" | "standard" | "deep"; gateCheckMode: "off" | "enabled"; + firstTimeContributorGrace: boolean; linkedIssueGateMode: GateRuleMode; duplicatePrGateMode: GateRuleMode; qualityGateMode: GateRuleMode; diff --git a/test/unit/gate-grace.test.ts b/test/unit/gate-grace.test.ts new file mode 100644 index 0000000000..25fdfbe261 --- /dev/null +++ b/test/unit/gate-grace.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it } from "vitest"; +import { evaluateGateCheck } from "../../src/rules/advisory"; +import { + applyFirstTimeContributorGrace, + isRepeatClosedUnmergedAuthor, + resolveContributorGraceGatePolicy, + shouldGrantFirstTimeContributorGrace, +} from "../../src/rules/gate-grace"; +import type { ContributorOutcomeHistory, RoleContext } from "../../src/signals/engine"; + +const maintainerRoleContext: RoleContext = { + login: "maintainer", + repoFullName: "JSONbored/gittensory", + generatedAt: "2026-06-12T00:00:00.000Z", + role: "collaborator", + maintainerLane: true, + normalContributorEvidenceAllowed: false, + source: "github_association", + association: "MEMBER", + reasons: [], + guidance: "Maintainer lane", +}; + +const newcomerOutcome: ContributorOutcomeHistory["repoOutcomes"][number] = { + repoFullName: "JSONbored/gittensory", + role: "outside_contributor", + lane: "direct_pr", + maintainerLane: false, + pullRequests: 1, + mergedPullRequests: 0, + openPullRequests: 1, + closedPullRequests: 0, + closedPullRequestRate: 0, + issues: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + credibility: 1, + issueCredibility: 1, + isEligible: true, + successLevel: "weak", + strengths: [], + risks: [], +}; + +const repeatOffenderOutcome: ContributorOutcomeHistory["repoOutcomes"][number] = { + ...newcomerOutcome, + pullRequests: 3, + openPullRequests: 1, + closedPullRequests: 2, + closedPullRequestRate: 2 / 3, +}; + +describe("first-time contributor gate grace", () => { + it("detects repeat closed-unmerged authors", () => { + expect(isRepeatClosedUnmergedAuthor(repeatOffenderOutcome)).toBe(true); + expect(isRepeatClosedUnmergedAuthor(newcomerOutcome)).toBe(false); + expect(isRepeatClosedUnmergedAuthor(undefined)).toBe(false); + }); + + it("grants grace for first-time contributors but not repeat offenders or maintainers", () => { + const contributorRole = { ...maintainerRoleContext, maintainerLane: false, role: "outside_contributor" as const }; + + expect(shouldGrantFirstTimeContributorGrace({ enabled: false, roleContext: contributorRole, repoOutcome: newcomerOutcome })).toBe(false); + expect(shouldGrantFirstTimeContributorGrace({ enabled: true, roleContext: contributorRole, repoOutcome: undefined })).toBe(true); + expect(shouldGrantFirstTimeContributorGrace({ enabled: true, roleContext: contributorRole, repoOutcome: newcomerOutcome })).toBe(true); + expect(shouldGrantFirstTimeContributorGrace({ enabled: true, roleContext: maintainerRoleContext, repoOutcome: newcomerOutcome })).toBe(false); + expect(shouldGrantFirstTimeContributorGrace({ enabled: true, roleContext: contributorRole, repoOutcome: repeatOffenderOutcome })).toBe(false); + }); + + it("grants grace for low-history contributors with multiple open attempts but no closed-unmerged pattern", () => { + const contributorRole = { ...maintainerRoleContext, maintainerLane: false, role: "outside_contributor" as const }; + expect( + shouldGrantFirstTimeContributorGrace({ + enabled: true, + roleContext: contributorRole, + repoOutcome: { + ...newcomerOutcome, + pullRequests: 2, + openPullRequests: 2, + closedPullRequests: 0, + }, + }), + ).toBe(true); + expect( + shouldGrantFirstTimeContributorGrace({ + enabled: true, + roleContext: contributorRole, + repoOutcome: { + ...newcomerOutcome, + pullRequests: 3, + openPullRequests: 3, + mergedPullRequests: 0, + closedPullRequests: 0, + }, + }), + ).toBe(true); + expect( + shouldGrantFirstTimeContributorGrace({ + enabled: true, + roleContext: contributorRole, + repoOutcome: { + ...newcomerOutcome, + pullRequests: 3, + openPullRequests: 2, + mergedPullRequests: 1, + closedPullRequests: 0, + }, + }), + ).toBe(false); + }); + + it("resolves contributor grace gate policy from base policy and contributor context", () => { + const contributorRole = { ...maintainerRoleContext, maintainerLane: false, role: "outside_contributor" as const }; + const base = { + linkedIssueGateMode: "block" as const, + duplicatePrGateMode: "block" as const, + qualityGateMode: "block" as const, + qualityGateMinScore: 90, + readinessScore: 10, + }; + + expect(resolveContributorGraceGatePolicy(base, { firstTimeContributorGrace: false }, { roleContext: contributorRole, repoOutcome: newcomerOutcome })).toEqual(base); + expect( + resolveContributorGraceGatePolicy(base, { firstTimeContributorGrace: true }, { roleContext: contributorRole, repoOutcome: newcomerOutcome }), + ).toEqual(applyFirstTimeContributorGrace(base)); + expect( + resolveContributorGraceGatePolicy(base, { firstTimeContributorGrace: true }, { roleContext: maintainerRoleContext, repoOutcome: newcomerOutcome }), + ).toEqual(base); + expect( + resolveContributorGraceGatePolicy(base, { firstTimeContributorGrace: true }, { roleContext: contributorRole, repoOutcome: repeatOffenderOutcome }), + ).toEqual(base); + }); + + it("preserves non-block gate modes when applying grace", () => { + expect( + applyFirstTimeContributorGrace({ + linkedIssueGateMode: "off", + duplicatePrGateMode: "advisory", + qualityGateMode: "block", + }), + ).toEqual({ + linkedIssueGateMode: "off", + duplicatePrGateMode: "advisory", + qualityGateMode: "advisory", + }); + }); + + it("downgrades configured block gates to advisory for newcomers with borderline findings", () => { + const advisory = { + targetType: "pull_request" as const, + targetKey: "JSONbored/gittensory#12", + repoFullName: "JSONbored/gittensory", + pullNumber: 12, + conclusion: "neutral" as const, + severity: "warning" as const, + title: "Pull request advisory generated.", + summary: "Pull request advisory generated.", + findings: [ + { + code: "missing_linked_issue", + severity: "warning" as const, + title: "No linked issue", + detail: "Private detail", + }, + ], + generatedAt: "2026-06-12T00:00:00.000Z", + id: "advisory:test", + }; + + const strictGate = evaluateGateCheck(advisory, { + linkedIssueGateMode: "block", + duplicatePrGateMode: "block", + qualityGateMode: "block", + qualityGateMinScore: 90, + readinessScore: 10, + }); + expect(strictGate.conclusion).toBe("failure"); + + const gracedGate = evaluateGateCheck( + advisory, + applyFirstTimeContributorGrace({ + linkedIssueGateMode: "block", + duplicatePrGateMode: "block", + qualityGateMode: "block", + qualityGateMinScore: 90, + readinessScore: 10, + }), + ); + expect(gracedGate.conclusion).toBe("success"); + }); +}); diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index d06c9a46e7..447baec23f 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -62,6 +62,7 @@ function settingsFor(repoFullName: string, overrides: Partial { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, requireLinkedIssue: true, }); const calls = { minerList: 0, gateChecks: 0 }; @@ -750,6 +751,61 @@ describe("queue processors", () => { expect(calls).toEqual({ minerList: 0, gateChecks: 2 }); }); + it("applies first-time contributor gate grace for a newcomer's borderline PR", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", + firstTimeContributorGrace: true, + requireLinkedIssue: true, + linkedIssueGateMode: "block", + }); + const calls = { gateChecks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/grace123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") { + calls.gateChecks += 1; + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && (init?.method ?? "GET") === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "completed", conclusion: "success", output: { title: "Gittensory Gate passed" } }); + calls.gateChecks += 1; + return Response.json({ id: 901 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-grace-newcomer", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 43, title: "First contribution", state: "open", user: { login: "newcontributor" }, head: { sha: "grace123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ gateChecks: 2 }); + }); + it("publishes an enabled gate when bot PR public output is skipped", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await persistRegistrySnapshot( @@ -768,6 +824,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, linkedIssueGateMode: "block", }); const calls = { gateChecks: 0, comments: 0, minerList: 0 }; @@ -838,6 +895,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, linkedIssueGateMode: "block", }); const calls = { minerList: 0, gateChecks: 0, comments: 0 }; @@ -907,6 +965,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, requireLinkedIssue: true, }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { @@ -960,6 +1019,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, }); const calls = { gateWrites: 0, commentGets: 0, commentPosts: 0 }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { @@ -1017,6 +1077,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", + firstTimeContributorGrace: false, }); let commentGets = 0; vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { @@ -1067,6 +1128,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", + firstTimeContributorGrace: false, includeMaintainerAuthors: true, commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer"] } }, }); @@ -1236,6 +1298,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", + firstTimeContributorGrace: false, includeMaintainerAuthors: true, }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { @@ -1484,6 +1547,7 @@ describe("queue processors", () => { autoLabelEnabled: true, checkRunMode: "enabled", gateCheckMode: "enabled", + firstTimeContributorGrace: false, }); let publicCalls = 0; vi.stubGlobal("fetch", async () => { @@ -1852,6 +1916,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "enabled", gateCheckMode: "off", + firstTimeContributorGrace: false, }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); @@ -1906,6 +1971,7 @@ describe("queue processors", () => { autoLabelEnabled: false, checkRunMode: "enabled", gateCheckMode: "off", + firstTimeContributorGrace: false, }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); diff --git a/test/unit/registration-readiness.test.ts b/test/unit/registration-readiness.test.ts index 65f385721c..b5033ba437 100644 --- a/test/unit/registration-readiness.test.ts +++ b/test/unit/registration-readiness.test.ts @@ -42,6 +42,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin checkRunMode: "enabled", checkRunDetailLevel: "standard", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/self-dogfood-registration-pack.test.ts b/test/unit/self-dogfood-registration-pack.test.ts index f59258b5d9..c2740c505b 100644 --- a/test/unit/self-dogfood-registration-pack.test.ts +++ b/test/unit/self-dogfood-registration-pack.test.ts @@ -55,6 +55,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin checkRunMode: "off", checkRunDetailLevel: "standard", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 854bedbd86..ada6e0a48b 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1503,6 +1503,7 @@ function repoSettings(repoFullName: string): RepositorySettings { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 16ec9a2437..35677f4a54 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -1613,6 +1613,7 @@ describe("v2 signal builders", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index 8e84970595..908742f986 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -324,6 +324,7 @@ describe("world-class backend signals", () => { checkRunMode: "off" as const, checkRunDetailLevel: "minimal" as const, gateCheckMode: "off" as const, + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory" as const, duplicatePrGateMode: "advisory" as const, qualityGateMode: "advisory" as const, @@ -373,6 +374,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", @@ -457,6 +459,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", @@ -516,6 +519,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + firstTimeContributorGrace: false, linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory",