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
2 changes: 1 addition & 1 deletion packages/loopover-engine/src/config-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const MERGE_READINESS_SUB_GATES: ReadonlyArray<{ field: "linkedIssue" | "duplica
];

function mergeReadinessCompositeWarnings(gate: FocusManifestGateConfig): string[] {
if (gate.mergeReadiness === null) return [];
if (gate.mergeReadiness === null || gate.mergeReadiness === "off") return [];
const explicit = MERGE_READINESS_SUB_GATES.filter(({ field }) => gate[field] !== null).map(({ label }) => label);
if (explicit.length === 0) return [];
return [
Expand Down
7 changes: 5 additions & 2 deletions src/review/linked-issue-label-propagation-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,11 @@ async function resolveIssueLabelsForPropagation(
// a fail-safe degrade, not a silent bypass: "no evidence this PR addresses the issue" must never be
// conflated with "confirmed addressed." This check does NOT apply to a non-reward relaxed label (plain
// `trustMaintainerAuthoredIssue`, e.g. bug/feature mirroring) or to the direct-ownership path above --
// both keep their existing behavior unchanged.
const rewardCandidates = kept.filter((label) => rewardTrustLabels.has(label.toLowerCase()));
// both keep their existing behavior unchanged. `isDirectOwnershipMatch` excludes `relaxedRewardLabels`
// entirely from this check even though both land in `kept`: a reward label the direct-match branch
// already required maintainerAuthored for (#9161's own stricter fall-through) is genuine involvement,
// not a bare citation, so it never needs this additional evidence bar.
const rewardCandidates = isDirectOwnershipMatch ? [] : kept.filter((label) => rewardTrustLabels.has(label.toLowerCase()));
if (rewardCandidates.length > 0) {
const verdict =
args.prNumber !== undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
"predicted": true,
"basis": "public_config",
"pack": "gittensor",
"conclusion": "failure",
"title": "LoopOver Orb Review Agent: No linked issue detected",
"summary": "No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.",
"conclusion": "success",
"title": "LoopOver Orb Review Agent passed",
"summary": "No configured hard blocker was found. Advisory findings, if any, stay advisory.",
"readinessScore": 80,
"blockers": [
"blockers": [],
"warnings": [
{
"code": "missing_linked_issue",
"title": "No linked issue detected",
"detail": "No closing reference or linked issue number was found in the PR metadata/body.",
"action": "If this PR is intended to solve an issue, link it explicitly in the PR body."
}
],
"warnings": [],
"funnel": null,
"note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring)."
}
4 changes: 2 additions & 2 deletions test/fixtures/engine-parity/predicted-gate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cleanPassGittensor from "./clean-pass-gittensor";
import cleanPassOssAntiSlop from "./clean-pass-oss-anti-slop";
import guardrailHold from "./guardrail-hold";
import mergeReadinessCompositeBlock from "./merge-readiness-composite-block";
import mergeReadinessCompositePreservesExplicitBlock from "./merge-readiness-composite-preserves-explicit-block";
import mergeReadinessCompositeDemotesExplicitBlock from "./merge-readiness-composite-demotes-explicit-block";
import selfAuthoredLinkedIssueBlock from "./self-authored-linked-issue-block";
import duplicatePrBlock from "./duplicate-pr-block";
import manifestBlockedPath from "./manifest-blocked-path";
Expand All @@ -27,7 +27,7 @@ export const predictedGateFixtures = [
selfAuthoredLinkedIssueBlock,
guardrailHold,
mergeReadinessCompositeBlock,
mergeReadinessCompositePreservesExplicitBlock,
mergeReadinessCompositeDemotesExplicitBlock,
claGateModeInert,
aiReviewGateModeInert,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BASE_INPUT, BASE_REPO, definePredictedGateFixture, parseManifest } from "./_shared";

// #9167: the merge-readiness composite always OVERRIDES the sub-gates it covers (linkedIssue/duplicates/
// slop) when set to advisory or block -- including an explicitly-authored stricter sub-gate mode. A
// "fill in only if left unset" variant was considered and reverted: every sub-gate mode is already a
// concrete, DB-defaulted value by the time it reaches GateCheckPolicy (never actually undefined there), so
// that variant could never fire in practice -- "explicitly authored" vs. "resolved to the shipped default"
// is indistinguishable at that layer. The demotion this fixture exercises is made VISIBLE instead, via
// config-lint.ts's mergeReadinessCompositeWarnings, which operates on the raw pre-default manifest where
// "unset" is a real, meaningful state.
export default definePredictedGateFixture({
id: "merge-readiness-composite-demotes-explicit-block",
title: "Composite merge-readiness advisory mode demotes an explicitly-configured block sub-gate",
branch: "missing_linked_issue does not block when gate.mergeReadiness=advisory overrides an explicit gate.linkedIssue=block",
input: { ...BASE_INPUT, body: "No linked issue yet", linkedIssues: [] },
manifest: parseManifest({ gate: { mergeReadiness: "advisory", linkedIssue: "block" } }),
repo: BASE_REPO,
issues: [],
pullRequests: [],
expected: {
conclusion: "success",
pack: "gittensor",
blockerCodes: [],
warningCodes: ["missing_linked_issue"],
funnelPresent: false,
},
});

This file was deleted.

2 changes: 1 addition & 1 deletion test/unit/loopover-engine-scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("loopover-engine package scaffold", () => {
});

it("publishes only the build output and changelog, not source", () => {
expect(enginePkg.files).toEqual(["dist", "CHANGELOG.md"]);
expect(enginePkg.files).toEqual(["dist/**/*.js", "dist/**/*.d.ts", "CHANGELOG.md"]);
expect(enginePkg.files).not.toContain("src");
});

Expand Down
5 changes: 4 additions & 1 deletion test/unit/predicted-gate-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1555,7 +1555,10 @@ describe("predicted-gate engine branch coverage (#2283)", () => {
expect(gateAdvisoryInternals.gateMode("off")).toBe("off");
expect(gateAdvisoryInternals.gateMode("block")).toBe("block");
expect(gateAdvisoryInternals.gateMode("advisory")).toBe("advisory");
expect(gateAdvisoryInternals.gateMode(undefined)).toBe("advisory");
// #9167: gateMode() fails CLOSED on a non-off/block/advisory value (including undefined) -- every
// legitimate caller supplies its own `?? "advisory"` default before reaching here, so this bare
// pass-through of `undefined` exercises the "truly malformed" fallback, not a normal call site.
expect(gateAdvisoryInternals.gateMode(undefined)).toBe("block");
expect(gateAdvisoryInternals.gatePolicyBlocks("advisory", "advisory")).toBe(false);
expect(gateAdvisoryInternals.gatePolicyBlocks("block", "advisory")).toBe(true);
expect(gateAdvisoryInternals.gatePolicyBlocks(undefined, "off")).toBe(false);
Expand Down
8 changes: 6 additions & 2 deletions test/unit/queue-5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6656,9 +6656,13 @@ describe("queue processors", () => {
},
});

// #9161: gittensor:priority is an ADDITIVE mapping here (removeOtherTypeLabels: false), so it is
// reward-semantic -- and the issue author here ("contributor") is the SAME login as the PR author, a
// direct-ownership match, who is NOT a repo maintainer. The reward label is correctly withheld
// (bug/feature-style type labels are unaffected); only gittensor:feature propagates.
expect(seen.issueFetches).toBe(1);
expect(seen.posted.sort()).toEqual(["gittensor:feature", "gittensor:priority"]);
expect(seen.removed).toEqual(["gittensor:bug"]);
expect(seen.posted.sort()).toEqual(["gittensor:feature"]);
expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]);
});

it("REGRESSION (#regression-safe-propagation, was: 'fails open to the normal title-based label'): skips the label decision entirely — never falls back to title — when the linked issue's fetch fails, leaving existing labels untouched", async () => {
Expand Down
60 changes: 40 additions & 20 deletions test/unit/reputation-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,45 @@ import { evaluateGateCheck } from "../../src/rules/advisory";
import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader";
import type { Advisory, RepositorySettings } from "../../src/types";
import { createTestEnv } from "../helpers/d1";
import { upsertRepositoryFromGitHub } from "../../src/db/repositories";

// Seeds one terminal review_targets row -- the raw table getSubmitterReputation(AcrossInstall) reads from
// (not part of the Drizzle schema; migrations/0050 is the source of truth for these columns).
import { upsertRepositoryFromGitHub, upsertPullRequestFromGitHub } from "../../src/db/repositories";

// #9136: seeds one terminal outcome via the LIVE ledger getSubmitterReputation(AcrossInstall)/
// listSubmitterCohortRows now read (review_audit's pr_outcome + gate_decision rows, joined against
// pull_requests) -- review_targets was frozen by the 2026-06-22 convergence cutover and is no longer a
// live writer for this signal (see submitter-reputation.ts's own PR_OUTCOME_JOIN/REASON_CODE_SUBQUERY
// fragments, which this mirrors exactly). `status` becomes the pr_outcome row's `decision`; `reasonCode`
// becomes a paired gate_decision row's `summary`, read back via the latest-gate_decision-for-this-target
// subquery -- matching how a real close/merge webhook actually populates both rows in production.
let seedReviewTargetCounter = 0;
async function seedReviewTarget(
env: Env,
args: { project: string; repo: string; number: number; installationId: number; submitter: string; status: string; reasonCode?: string | null },
) {
await upsertPullRequestFromGitHub(env, args.repo, {
number: args.number,
title: "synthetic PR",
state: args.status === "merged" ? "closed" : "closed",
user: { login: args.submitter },
head: { sha: `sha${args.number}` },
labels: [],
merged_at: args.status === "merged" ? new Date().toISOString() : null,
} as unknown as Parameters<typeof upsertPullRequestFromGitHub>[2]);
const targetId = `${args.project}#${args.number}`;
const uniq = `${Date.now()}-${(seedReviewTargetCounter += 1)}`;
await env.DB.prepare(
`INSERT INTO review_targets (id, project, kind, repo, number, installation_id, submitter, status, decision_json, terminal_at)
VALUES (?, ?, 'pull_request', ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
`INSERT INTO review_audit (id, project, target_id, event_type, decision, source, head_sha, summary, created_at)
VALUES (?, ?, ?, 'pr_outcome', ?, 'gittensory-native', NULL, NULL, CURRENT_TIMESTAMP)`,
)
.bind(
`${args.project}:pull_request:${args.repo}#${args.number}`,
args.project,
args.repo,
args.number,
args.installationId,
args.submitter,
args.status,
args.reasonCode === undefined ? null : JSON.stringify({ reasonCode: args.reasonCode }),
)
.bind(`pr_outcome:${targetId}:${uniq}`, args.project, targetId, args.status)
.run();
if (args.reasonCode !== undefined && args.reasonCode !== null) {
await env.DB.prepare(
`INSERT INTO review_audit (id, project, target_id, event_type, decision, source, head_sha, summary, created_at)
VALUES (?, ?, ?, 'gate_decision', NULL, 'gittensory-native', NULL, ?, CURRENT_TIMESTAMP)`,
)
.bind(`gate_decision:${targetId}:${uniq}`, args.project, targetId, args.reasonCode)
.run();
}
}

// A submitter who FLOODED the project with submissions but landed almost none — the burst anti-abuse pattern.
Expand Down Expand Up @@ -368,9 +384,10 @@ describe("ORB/AMS reputation bridge wiring (#6801)", () => {
reasonCode: "dual_review_declined",
});
}
// Keep the quality outcomes recent while making their submission cadence old enough not to trip the
// independent 24-hour machine-paced check after AMS upgrades the quality signal.
await env.DB.prepare("UPDATE review_targets SET created_at = '2026-01-01T00:00:00.000Z' WHERE submitter = ?").bind(submitter).run();
// Keep the quality outcomes (review_audit) recent while making their submission cadence old enough not
// to trip the independent 24-hour machine-paced check after AMS upgrades the quality signal. #9015
// repointed getSubmitterCadence to pull_requests.created_at (review_targets is no longer a live source).
await env.DB.prepare("UPDATE pull_requests SET created_at = '2026-01-01T00:00:00.000Z' WHERE author_login = ?").bind(submitter).run();
}

it("REGRESSION (#6801): feature on upgrades a low local signal and changes the end-to-end gate decision", async () => {
Expand Down Expand Up @@ -472,8 +489,11 @@ describe("getEffectiveSubmitterReputation (#4513, install-wide for a confirmed m
await upsertRepositoryFromGitHub(env, { name: "repo-a", full_name: "org/repo-a", owner: { login: "org" } }, 999);
// Only 1 sample on repo-a itself -- per-repo signal stays "neutral" (below minSample).
await seedReviewTarget(env, { project: "org/repo-a", repo: "org/repo-a", number: 1, installationId: 999, submitter: "farmer99", status: "closed", reasonCode: "dual_review_declined" });
// But spread across OTHER repos in the SAME install, farmer99 has a clear serial-decline pattern.
// But spread across OTHER repos in the SAME install, farmer99 has a clear serial-decline pattern. Each
// repo must be registered (getSubmitterReputationAcrossInstall joins review_audit against `repositories`
// by installation_id, #9136) for its seeded outcome to actually count toward the install-wide tally.
for (let i = 0; i < 7; i++) {
await upsertRepositoryFromGitHub(env, { name: `repo-${i}`, full_name: `org/repo-${i}`, owner: { login: "org" } }, 999);
await seedReviewTarget(env, { project: `org/repo-${i}`, repo: `org/repo-${i}`, number: i + 10, installationId: 999, submitter: "farmer99", status: "closed", reasonCode: "dual_review_declined" });
}
vi.stubGlobal("fetch", stubMinerFetch("farmer99"));
Expand Down
Loading