From ed354d5e3d8df284f1d2ef21d961445b6f7c84fd Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:54:19 +0200 Subject: [PATCH 1/2] feat(config): audit and wire repository-settings flag enforcement Promote requireLinkedIssue into linkedIssueGateMode when unset, resolve effective settings for the settings preview API, include aiReview fields in preview output, and add an enforcement test matrix for the audited flags. Fixes #797 Co-authored-by: Cursor --- src/api/routes.ts | 3 +- src/queue/processors.ts | 13 +- src/services/maintainer-activation.ts | 2 +- src/settings/repository-settings.ts | 13 ++ src/signals/focus-manifest.ts | 5 + src/signals/settings-preview.ts | 4 + test/unit/focus-manifest.test.ts | 8 + test/unit/maintainer-activation.test.ts | 2 +- .../repository-settings-enforcement.test.ts | 139 ++++++++++++++++++ 9 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/settings/repository-settings.ts create mode 100644 test/unit/repository-settings-enforcement.test.ts diff --git a/src/api/routes.ts b/src/api/routes.ts index 8db71ad86c..69ce9f5030 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -221,6 +221,7 @@ import { loadGatePrecisionReport } from "../services/gate-precision"; import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard"; import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics"; import { compileFocusManifestPolicy } from "../signals/focus-manifest"; +import { resolveRepositorySettings } from "../settings/repository-settings"; import { loadRepoFocusManifest, upsertRepoFocusManifest } from "../signals/focus-manifest-loader"; import { buildRepoOnboardingPackPreviewForRepo } from "../services/repo-onboarding-pack"; import { generateContributorIssueDrafts } from "../services/contributor-issue-draft"; @@ -2174,7 +2175,7 @@ export function createApp() { if (unauthorized) return unauthorized; } const [settings, issues, pullRequests] = await Promise.all([ - getRepositorySettings(c.env, fullName), + resolveRepositorySettings(c.env, fullName), listIssues(c.env, fullName), listPullRequests(c.env, fullName), ]); diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 6739305784..16610cd62a 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -143,8 +143,9 @@ import { import { buildIssueSlopAssessment, buildSlopAssessment, type SlopBand } from "../signals/slop"; import { runGittensoryAiSlopAdvisory } from "../services/ai-slop"; import { decidePublicSurface } from "../signals/settings-preview"; +import { buildFocusManifestGuidance } from "../signals/focus-manifest"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; -import { buildFocusManifestGuidance, resolveEffectiveSettings } from "../signals/focus-manifest"; +import { resolveRepositorySettings } from "../settings/repository-settings"; import type { LocalBranchAnalysisInput } from "../signals/local-branch"; import { runGittensoryAiReview } from "../services/ai-review"; import type { AdvisoryFinding, ContributorEvidenceRecord, DetectedNotificationEvent, GitHubWebhookPayload, JobMessage, JsonValue, PullRequestRecord, RepositorySettings } from "../types"; @@ -1066,16 +1067,6 @@ export function gateCheckPolicy( }; } -/** - * Effective repository settings for webhook handling: the DB-backed settings overlaid with the repo's - * `.gittensory.yml` (config-as-code). This single resolver is why EVERYTHING — gate on/off, all blocker - * modes, comments, labels, surface, audience — is controllable from the repo's config file. - */ -async function resolveRepositorySettings(env: Env, repoFullName: string): Promise { - const [dbSettings, manifest] = await Promise.all([getRepositorySettings(env, repoFullName), loadRepoFocusManifest(env, repoFullName)]); - return resolveEffectiveSettings(dbSettings, manifest); -} - /** Build a bounded unified-diff string from cached PR files for the AI reviewer. Caps total size so a * huge PR cannot blow the model context or the neuron budget; each file's patch is taken from the raw * GitHub file payload when present. */ diff --git a/src/services/maintainer-activation.ts b/src/services/maintainer-activation.ts index d239475fdb..0b2c723070 100644 --- a/src/services/maintainer-activation.ts +++ b/src/services/maintainer-activation.ts @@ -58,7 +58,7 @@ export function buildMaintainerActivationPreview(args: { const samples: MaintainerActivationSample[] = recent.map((pr) => { const advisory = buildPullRequestAdvisory(args.repo, pr, { otherOpenPullRequests: args.pullRequests.filter((other) => other.number !== pr.number), - requireLinkedIssue: true, + requireLinkedIssue: args.settings.requireLinkedIssue || args.settings.linkedIssueGateMode !== "off", }); for (const finding of advisory.findings) codeCounts.set(finding.code, (codeCounts.get(finding.code) ?? 0) + 1); return { diff --git a/src/settings/repository-settings.ts b/src/settings/repository-settings.ts new file mode 100644 index 0000000000..ddba01e790 --- /dev/null +++ b/src/settings/repository-settings.ts @@ -0,0 +1,13 @@ +import { getRepositorySettings } from "../db/repositories"; +import { resolveEffectiveSettings } from "../signals/focus-manifest"; +import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; +import type { RepositorySettings } from "../types"; + +/** Effective repository settings: DB values overlaid with `.gittensory.yml` (config-as-code). */ +export async function resolveRepositorySettings(env: Env, repoFullName: string): Promise { + const [dbSettings, manifest] = await Promise.all([ + getRepositorySettings(env, repoFullName), + loadRepoFocusManifest(env, repoFullName), + ]); + return resolveEffectiveSettings(dbSettings, manifest); +} diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index aa9709d2a2..ae70886b2f 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -527,6 +527,11 @@ export function resolveEffectiveSettings(dbSettings: RepositorySettings, manifes if (gate.mergeReadiness !== null) effective.mergeReadinessGateMode = gate.mergeReadiness; if (gate.manifestPolicy !== null) effective.manifestPolicyGateMode = gate.manifestPolicy; if (gate.firstTimeContributorGrace !== null) effective.firstTimeContributorGrace = gate.firstTimeContributorGrace; + // The dashboard "Require linked issue" toggle must not silently diverge from gate blocking: when the + // boolean is on but linkedIssueGateMode is still off, treat it as a block requirement (#797). + if (effective.requireLinkedIssue && effective.linkedIssueGateMode === "off") { + effective.linkedIssueGateMode = "block"; + } return effective; } diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts index 91ab43a1b3..6ad09a6ab2 100644 --- a/src/signals/settings-preview.ts +++ b/src/signals/settings-preview.ts @@ -316,6 +316,10 @@ export function buildRepoSettingsPreview(args: { includeMaintainerAuthors: settings.includeMaintainerAuthors, requireLinkedIssue: settings.requireLinkedIssue, badgeEnabled: settings.badgeEnabled ?? false, + aiReviewMode: settings.aiReviewMode, + aiReviewByok: settings.aiReviewByok, + aiReviewProvider: settings.aiReviewProvider ?? null, + aiReviewModel: settings.aiReviewModel ?? null, commandAuthorization: summarizeCommandAuthorizationPolicy(settings.commandAuthorization), }, commandAuthorizationPreview, diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 0af4c1db32..5241c676d6 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -951,6 +951,14 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () = expect(eff.aiReviewMode).toBe("block"); expect(eff.aiReviewByok).toBe(true); }); + + it("promotes requireLinkedIssue to linkedIssueGateMode block when the gate mode is still off (#797)", () => { + const eff = resolveEffectiveSettings( + { requireLinkedIssue: true, linkedIssueGateMode: "off" } as RepositorySettings, + parseFocusManifest(null), + ); + expect(eff.linkedIssueGateMode).toBe("block"); + }); }); describe("parseFocusManifest review config", () => { diff --git a/test/unit/maintainer-activation.test.ts b/test/unit/maintainer-activation.test.ts index 6ccbab7e03..311f3fd22a 100644 --- a/test/unit/maintainer-activation.test.ts +++ b/test/unit/maintainer-activation.test.ts @@ -72,7 +72,7 @@ describe("buildMaintainerActivationPreview", () => { const preview = buildMaintainerActivationPreview({ repoFullName: repo.fullName, repo, - settings: settings(), + settings: settings({ requireLinkedIssue: true }), pullRequests: [pr(1, { linkedIssues: [] }), pr(2, { linkedIssues: [5] })], generatedAt: "2026-06-14T00:00:00.000Z", }); diff --git a/test/unit/repository-settings-enforcement.test.ts b/test/unit/repository-settings-enforcement.test.ts new file mode 100644 index 0000000000..baa5580414 --- /dev/null +++ b/test/unit/repository-settings-enforcement.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; +import { gateCheckPolicy } from "../../src/queue/processors"; +import { evaluateGateCheck } from "../../src/rules/advisory"; +import { runAiReviewForAdvisory } from "../../src/queue/processors"; +import { buildMaintainerActivationPreview } from "../../src/services/maintainer-activation"; +import { decidePublicSurface } from "../../src/signals/settings-preview"; +import { parseFocusManifest, resolveEffectiveSettings } from "../../src/signals/focus-manifest"; +import type { Advisory, PullRequestRecord, RepositoryRecord, RepositorySettings } from "../../src/types"; + +function settings(over: Partial = {}): RepositorySettings { + return { + repoFullName: "owner/repo", + commentMode: "detected_contributors_only", + publicAudienceMode: "oss_maintainer", + publicSignalLevel: "standard", + checkRunMode: "off", + checkRunDetailLevel: "standard", + gateCheckMode: "enabled", + gatePack: "gittensor", + linkedIssueGateMode: "off", + duplicatePrGateMode: "advisory", + qualityGateMode: "advisory", + slopGateMode: "off", + mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", + firstTimeContributorGrace: false, + slopAiAdvisory: false, + qualityGateMinScore: null, + autoLabelEnabled: true, + gittensorLabel: "gittensor", + createMissingLabel: true, + publicSurface: "comment_and_label", + includeMaintainerAuthors: false, + requireLinkedIssue: false, + backfillEnabled: true, + privateTrustEnabled: true, + aiReviewMode: "off", + aiReviewByok: false, + aiReviewProvider: null, + aiReviewModel: null, + ...over, + }; +} + +function missingIssueAdvisory(): Advisory { + return { + id: "advisory-policy", + targetType: "pull_request", + targetKey: "owner/repo#7", + repoFullName: "owner/repo", + pullNumber: 7, + headSha: "sha7", + conclusion: "neutral", + severity: "warning", + title: "Gittensory advisory available", + summary: "1 advisory finding generated.", + findings: [{ code: "missing_linked_issue", title: "No linked issue detected", severity: "warning", detail: "No closing reference.", action: "Link the issue." }], + generatedAt: "2026-06-13T00:00:00.000Z", + }; +} + +describe("repository settings enforcement audit (#797)", () => { + it("maps requireLinkedIssue to linkedIssueGateMode block when the gate mode is off", () => { + const effective = resolveEffectiveSettings(settings({ requireLinkedIssue: true, linkedIssueGateMode: "off" }), parseFocusManifest(null)); + expect(effective.linkedIssueGateMode).toBe("block"); + }); + + it("blocks confirmed contributors when requireLinkedIssue is enabled via the boolean alone", () => { + const effective = resolveEffectiveSettings(settings({ requireLinkedIssue: true, linkedIssueGateMode: "off" }), parseFocusManifest(null)); + const policy = gateCheckPolicy(effective); + const result = evaluateGateCheck(missingIssueAdvisory(), policy, { confirmedContributor: true }); + expect(result.conclusion).toBe("failure"); + }); + + it("skips maintainer-authored public surfaces when includeMaintainerAuthors is false", () => { + const decision = decidePublicSurface({ + settings: settings({ includeMaintainerAuthors: false }), + authorLogin: "owner", + authorType: "User", + authorAssociation: "OWNER", + minerStatus: "confirmed", + }); + expect(decision).toMatchObject({ skipped: true, skipReason: "maintainer_author" }); + }); + + it("no-ops AI review when aiReviewMode is off", async () => { + const advisory = missingIssueAdvisory(); + const notes = await runAiReviewForAdvisory({} as Env, { + settings: settings({ aiReviewMode: "off" }), + advisory, + repoFullName: "owner/repo", + pr: { number: 7, title: "Test" }, + author: "miner", + confirmedContributor: true, + }); + expect(notes).toBeUndefined(); + expect(advisory.findings.some((finding) => finding.code === "ai_consensus_defect")).toBe(false); + }); + + it("uses the repo requireLinkedIssue setting in maintainer activation previews", () => { + const repo: RepositoryRecord = { + fullName: "owner/repo", + owner: "owner", + name: "repo", + isInstalled: true, + isRegistered: true, + isPrivate: false, + registryConfig: { repo: "owner/repo", emissionShare: 0.01, issueDiscoveryShare: 0, maintainerCut: 0, labelMultipliers: {}, raw: {} }, + }; + const pull: PullRequestRecord = { + repoFullName: repo.fullName, + number: 1, + title: "No issue link", + state: "open", + authorLogin: "miner", + body: "No closing reference.", + labels: [], + linkedIssues: [], + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + }; + const withRequirement = buildMaintainerActivationPreview({ + repoFullName: repo.fullName, + repo, + settings: settings({ requireLinkedIssue: true }), + pullRequests: [pull], + generatedAt: "2026-06-01T00:00:00.000Z", + }); + const withoutRequirement = buildMaintainerActivationPreview({ + repoFullName: repo.fullName, + repo, + settings: settings({ requireLinkedIssue: false, linkedIssueGateMode: "off" }), + pullRequests: [pull], + generatedAt: "2026-06-01T00:00:00.000Z", + }); + expect(withRequirement.samples[0]?.findings.some((finding) => finding.code === "missing_linked_issue")).toBe(true); + expect(withoutRequirement.samples[0]?.findings.some((finding) => finding.code === "missing_linked_issue")).toBe(false); + }); +}); From 413098872c80e27088787aad845638774ef03c49 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:58:25 +0200 Subject: [PATCH 2/2] fix(config): resolve typecheck failures in settings preview tests Add aiReview fields to RepoSettingsPreview and fix gateCheckPolicy usage in the repository-settings enforcement test matrix. Co-authored-by: Cursor --- src/signals/settings-preview.ts | 4 ++++ test/unit/repository-settings-enforcement.test.ts | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts index 6ad09a6ab2..ab24ae5ab3 100644 --- a/src/signals/settings-preview.ts +++ b/src/signals/settings-preview.ts @@ -198,6 +198,10 @@ export type RepoSettingsPreview = { includeMaintainerAuthors: boolean; requireLinkedIssue: boolean; badgeEnabled: boolean; + aiReviewMode: RepositorySettings["aiReviewMode"]; + aiReviewByok: boolean; + aiReviewProvider: string | null; + aiReviewModel: string | null; commandAuthorization: { defaultAllowed: CommandAuthorizationRole[]; commandOverrides: Array<{ command: string; allowedRoles: CommandAuthorizationRole[] }>; diff --git a/test/unit/repository-settings-enforcement.test.ts b/test/unit/repository-settings-enforcement.test.ts index baa5580414..5c2044b2c9 100644 --- a/test/unit/repository-settings-enforcement.test.ts +++ b/test/unit/repository-settings-enforcement.test.ts @@ -67,8 +67,7 @@ describe("repository settings enforcement audit (#797)", () => { it("blocks confirmed contributors when requireLinkedIssue is enabled via the boolean alone", () => { const effective = resolveEffectiveSettings(settings({ requireLinkedIssue: true, linkedIssueGateMode: "off" }), parseFocusManifest(null)); - const policy = gateCheckPolicy(effective); - const result = evaluateGateCheck(missingIssueAdvisory(), policy, { confirmedContributor: true }); + const result = evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(effective, null, true)); expect(result.conclusion).toBe("failure"); });