From d6c00491f2b4520fa99aa1121abc5baf174950e5 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:21:52 +0200 Subject: [PATCH 01/11] feat(mcp): add gittensory_remediation_plan tool Turn local branch blocker lists into an ordered, deduplicated public-safe remediation checklist with per-item rerun conditions. Co-authored-by: Cursor --- packages/gittensory-mcp/bin/gittensory-mcp.js | 14 ++ src/api/routes.ts | 54 ++++++ src/mcp/server.ts | 37 ++++ src/services/remediation-plan.ts | 162 ++++++++++++++++++ test/integration/api.test.ts | 1 + test/unit/mcp-output-schemas.test.ts | 1 + test/unit/remediation-plan.test.ts | 93 ++++++++++ 7 files changed, 362 insertions(+) create mode 100644 src/services/remediation-plan.ts create mode 100644 test/unit/remediation-plan.test.ts diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index e73eac857d..a7e095c058 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -501,6 +501,20 @@ server.registerTool( }, ); +server.registerTool( + "gittensory_remediation_plan", + { + description: "Analyze the current git branch and return an ordered public-safe remediation checklist with rerun conditions.", + inputSchema: currentBranchShape, + }, + async (input) => { + const workspaceInput = await withClientWorkspaceRoots(input); + const payload = buildBranchAnalysisPayload({ ...workspaceInput, cwd: resolveWorkspaceCwd(workspaceInput).cwd }); + const { localScorerStatus: _localScorerStatus, ...body } = payload; + return toolResult("Gittensory remediation plan.", await apiPost("/v1/local/remediation-plan", body)); + }, +); + server.registerTool( "gittensory_prepare_pr_packet", { diff --git a/src/api/routes.ts b/src/api/routes.ts index 31bb834609..4370d6b39a 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -126,6 +126,7 @@ import { preflightBranchWithAgent, startAgentRun, } from "../services/agent-orchestrator"; +import { buildRemediationPlan } from "../services/remediation-plan"; import { buildMcpClientTelemetry } from "../services/client-telemetry"; import { buildAndPersistContributorDecisionPack, @@ -1996,6 +1997,59 @@ export function createApp() { return c.json(response); }); + app.post("/v1/local/remediation-plan", async (c) => { + const body = await c.req.json().catch(() => null); + const parsed = localBranchAnalysisSchema.safeParse(body); + if (!parsed.success) return c.json({ error: "invalid_local_branch_analysis_request", issues: parsed.error.issues }, 400); + const unauthorized = await requireContributorAccess(c, parsed.data.login); + if (unauthorized) return unauthorized; + const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality, repoManifest] = await Promise.all([ + loadContributorFastContext(c.env, parsed.data.login), + getRepository(c.env, parsed.data.repoFullName), + listIssues(c.env, parsed.data.repoFullName), + listPullRequests(c.env, parsed.data.repoFullName), + listRecentMergedPullRequests(c.env, parsed.data.repoFullName), + listBountiesByRepo(c.env, parsed.data.repoFullName), + getOrCreateScoringModelSnapshot(c.env), + loadOrComputeIssueQualityResponse(c.env, parsed.data.repoFullName), + loadRepoFocusManifest(c.env, parsed.data.repoFullName), + ]); + const fit = buildContributorFit(context.profile, context.repositories, [], [], context.syncStates, context.repoStats); + const scoringProfile = buildContributorScoringProfile({ login: parsed.data.login, fit, scoringSnapshot: snapshot }); + const checkSummaries = await loadCheckSummariesForPullRequests(c.env, parsed.data.repoFullName, parsed.data, pullRequests); + const analysisInput = parsed.data.focusManifest !== undefined || !repoManifest.present + ? parsed.data + : { ...parsed.data, focusManifest: repoManifest as unknown }; + const analysis = buildLocalBranchAnalysis({ + input: analysisInput, + repo, + issues, + pullRequests, + contributorPullRequests: context.contributorPullRequests, + recentMergedPullRequests, + bounties, + repositories: context.repositories, + checkSummaries, + profile: context.profile, + outcomeHistory: context.outcomeHistory, + scoringSnapshot: snapshot, + scoringProfile, + issueQuality: issueQuality?.report, + gittensorSnapshot: context.gittensorSnapshot, + }); + return c.json( + buildRemediationPlan({ + login: analysis.login, + repoFullName: analysis.repoFullName, + branchQualityBlockers: analysis.branchQualityBlockers, + accountStateBlockers: analysis.accountStateBlockers, + scoreBlockers: analysis.scoreBlockers, + recommendedRerunCondition: analysis.recommendedRerunCondition, + localFindings: analysis.localFindings, + }), + ); + }); + app.post("/v1/agent/runs", async (c) => { const body = await c.req.json().catch(() => null); const parsed = agentRunSchema.safeParse(body); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 65cb99cfef..09e12900b0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -44,6 +44,7 @@ import { } from "../services/agent-orchestrator"; import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../services/decision-pack"; import { buildPublicPrBodyDraft } from "../services/pr-body-draft"; +import { buildRemediationPlan } from "../services/remediation-plan"; import { loadOrComputeIssueQualityResponse } from "../services/issue-quality"; import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast"; import { buildMcpClientTelemetry } from "../services/client-telemetry"; @@ -415,6 +416,14 @@ const checkBeforeStartOutputSchema = { report: z.unknown().optional(), }; +const remediationPlanOutputSchema = { + repoFullName: z.string().optional(), + login: z.string().optional(), + summary: z.string().optional(), + recommendedRerunCondition: z.string().optional(), + items: z.unknown().optional(), +}; + export async function handleMcpRequest(c: AppContext): Promise { if (c.req.method === "OPTIONS") return new Response(null, { status: 204 }); const identity = await authenticateMcpRequest(c); @@ -728,6 +737,17 @@ export class GittensoryMcp { async (input) => this.toolResult(await this.localBranchSlice(input, "scoreBlockers")), ); + server.registerTool( + "gittensory_remediation_plan", + { + description: + "Turn local branch blocker lists into an ordered, deduplicated public-safe remediation checklist with rerun conditions. Metadata only.", + inputSchema: localBranchAnalysisShape, + outputSchema: remediationPlanOutputSchema, + }, + async (input) => this.toolResult(await this.remediationPlan(input)), + ); + server.registerTool( "gittensory_prepare_pr_packet", { @@ -1399,6 +1419,23 @@ export class GittensoryMcp { }; } + private async remediationPlan(input: z.infer>): Promise { + const analysis = await this.analyzeLocalBranch(input); + const plan = buildRemediationPlan({ + login: analysis.login, + repoFullName: analysis.repoFullName, + branchQualityBlockers: analysis.branchQualityBlockers, + accountStateBlockers: analysis.accountStateBlockers, + scoreBlockers: analysis.scoreBlockers, + recommendedRerunCondition: analysis.recommendedRerunCondition, + localFindings: analysis.localFindings, + }); + return { + summary: `Gittensory remediation plan for ${analysis.login} in ${analysis.repoFullName}.`, + data: plan as unknown as Record, + }; + } + private async draftPrBody(input: z.infer>): Promise { const analysis = await this.analyzeLocalBranch(input); const draft = buildPublicPrBodyDraft(analysis); diff --git a/src/services/remediation-plan.ts b/src/services/remediation-plan.ts new file mode 100644 index 0000000000..2f457450f1 --- /dev/null +++ b/src/services/remediation-plan.ts @@ -0,0 +1,162 @@ +import { sanitizePublicComment } from "../github/commands"; + +export type RemediationPlanSource = "account_state" | "branch_quality" | "score"; + +export type RemediationPlanItem = { + rank: number; + source: RemediationPlanSource; + step: string; + rerunCondition: string; + impact: "high" | "medium"; +}; + +export type RemediationPlan = { + repoFullName: string; + login: string; + summary: string; + recommendedRerunCondition: string; + items: RemediationPlanItem[]; +}; + +export type RemediationPlanInput = { + login: string; + repoFullName: string; + branchQualityBlockers: string[]; + accountStateBlockers: string[]; + scoreBlockers: string[]; + recommendedRerunCondition: string; + localFindings?: Array<{ + code: string; + severity: "info" | "warning" | "critical"; + title: string; + detail: string; + action?: string | undefined; + }>; +}; + +const FORBIDDEN_PATTERN = + /\b(reward\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i; + +const SOURCE_PRIORITY: Record = { + account_state: 0, + branch_quality: 1, + score: 2, +}; + +function publicSafeText(value: string): string { + const sanitized = sanitizePublicComment(value).trim(); + return sanitized && !FORBIDDEN_PATTERN.test(sanitized) ? sanitized : ""; +} + +function publicSafeRerunCondition(condition: string): string { + const sanitized = publicSafeText(condition); + if (!sanitized) return "Rerun after branch, base, or PR state changes before opening or submitting."; + return /eligibility|multiplier|scoreability|score/i.test(sanitized) + ? "Refresh linked issue and base branch metadata before submission." + : sanitized; +} + +function normalizeKey(value: string): string { + return value.trim().toLowerCase().replace(/\s+/g, " "); +} + +function actionForFinding(findings: RemediationPlanInput["localFindings"], title: string): string | undefined { + const match = findings?.find((finding) => normalizeKey(finding.title) === normalizeKey(title)); + return match?.action ? publicSafeText(match.action) : undefined; +} + +function rerunForAccountBlocker(blocker: string, fallback: string): string { + if (/open PR|concurrent|threshold/i.test(blocker)) { + return publicSafeRerunCondition("Rerun after pending PRs merge/close or open PR count is within the allowance."); + } + if (/credibility|history|maturity/i.test(blocker)) { + return publicSafeRerunCondition("Rerun after account/queue maturity blockers clear."); + } + return publicSafeRerunCondition(fallback); +} + +function rerunForBranchBlocker(blocker: string, fallback: string): string { + if (/stale|fetch origin|base/i.test(blocker)) { + return publicSafeRerunCondition("Run `git fetch origin` and rerun branch analysis against the refreshed base."); + } + if (/validation|test|check/i.test(blocker)) { + return publicSafeRerunCondition("Rerun after fixing branch-quality blockers or adding explicit validation evidence."); + } + if (/linked issue|duplicate|eligibility/i.test(blocker)) { + return publicSafeRerunCondition("Refresh linked issue and base branch metadata before submission."); + } + return publicSafeRerunCondition(fallback); +} + +function stepFromBlocker(source: RemediationPlanSource, blocker: string, findings: RemediationPlanInput["localFindings"]): string { + const findingAction = actionForFinding(findings, blocker); + if (findingAction) return findingAction; + const sanitized = publicSafeText(blocker); + if (sanitized) return sanitized; + if (source === "account_state") return "Clear account or queue maturity blockers before opening more work."; + if (source === "branch_quality") return "Resolve branch-quality findings before submission."; + return "Resolve scoreability blockers before relying on this preview."; +} + +function impactFor(source: RemediationPlanSource, blocker: string): "high" | "medium" { + if (source === "account_state") return "high"; + if (/GitHub checks|validation failed|maintainer-blocked|duplicate|ineligible/i.test(blocker)) return "high"; + return source === "branch_quality" ? "high" : "medium"; +} + +function collectItems(input: RemediationPlanInput): Array> { + const seen = new Set(); + const items: Array> = []; + const push = (source: RemediationPlanSource, blocker: string) => { + const key = normalizeKey(blocker); + if (!key || seen.has(key)) return; + const step = stepFromBlocker(source, blocker, input.localFindings); + if (!step) return; + seen.add(key); + const rerunCondition = + source === "account_state" + ? rerunForAccountBlocker(blocker, input.recommendedRerunCondition) + : source === "branch_quality" + ? rerunForBranchBlocker(blocker, input.recommendedRerunCondition) + : publicSafeRerunCondition(input.recommendedRerunCondition); + items.push({ + source, + step, + rerunCondition, + impact: impactFor(source, blocker), + }); + }; + + for (const blocker of input.accountStateBlockers) push("account_state", blocker); + for (const blocker of input.branchQualityBlockers) push("branch_quality", blocker); + for (const blocker of input.scoreBlockers) push("score", blocker); + + items.sort( + (left, right) => + SOURCE_PRIORITY[left.source] - SOURCE_PRIORITY[right.source] || + Number(right.impact === "high") - Number(left.impact === "high") || + left.step.localeCompare(right.step), + ); + return items; +} + +/** + * Turn local branch blocker lists into an ordered, deduplicated remediation checklist. + * Steps and rerun conditions are public-safe for PR-body reuse. + */ +export function buildRemediationPlan(input: RemediationPlanInput): RemediationPlan { + const ordered = collectItems(input); + const items = ordered.map((item, index) => ({ ...item, rank: index + 1 })); + const summary = + items.length === 0 + ? "No blockers detected; rerun after any branch, base, or PR state changes before opening or submitting." + : `${items.length} remediation step(s) ordered by impact; start with ${items[0]?.step ?? "the first listed item"}.`; + + return { + repoFullName: input.repoFullName, + login: input.login, + summary: publicSafeText(summary), + recommendedRerunCondition: publicSafeRerunCondition(input.recommendedRerunCondition), + items, + }; +} diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index fdfee8ed67..63c04bf09a 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -4373,6 +4373,7 @@ describe("api routes", () => { expect(toolNames).toContain("gittensory_rank_local_next_actions"); expect(toolNames).toContain("gittensory_compare_local_variants"); expect(toolNames).toContain("gittensory_explain_local_blockers"); + expect(toolNames).toContain("gittensory_remediation_plan"); expect(toolNames).toContain("gittensory_prepare_pr_packet"); expect(toolNames).toContain("gittensory_agent_plan_next_work"); expect(toolNames).toContain("gittensory_agent_start_run"); diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index 290e80c330..7c24abbe68 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -23,6 +23,7 @@ const TOOLS_WITH_OUTPUT_SCHEMA = [ "gittensory_get_registry_changes", "gittensory_get_upstream_drift", "gittensory_local_status", + "gittensory_remediation_plan", ]; async function connectTestClient(env: Env = createTestEnv()) { diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts new file mode 100644 index 0000000000..26a07c8b11 --- /dev/null +++ b/test/unit/remediation-plan.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { buildRemediationPlan } from "../../src/services/remediation-plan"; + +const FORBIDDEN = /\b(wallet|hotkey|coldkey|mnemonic|farming|payout|raw[-_\s]?trust)\b/i; + +describe("buildRemediationPlan", () => { + it("returns an ordered, deduplicated checklist with rerun conditions", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["Open PR count exceeds the current allowance (6/2)."], + branchQualityBlockers: ["Local validation failed", "GitHub checks need attention"], + scoreBlockers: ["Local validation failed", "Repo is not registered for Gittensor scoring"], + recommendedRerunCondition: "Rerun after fixing branch-quality blockers or adding explicit validation/linked-context evidence.", + localFindings: [ + { + code: "failed_local_validation", + severity: "warning", + title: "Local validation failed", + detail: "1 validation command failed.", + action: "Fix validation before asking maintainers to review.", + }, + ], + }); + + expect(plan.items.length).toBeGreaterThan(0); + expect(plan.items[0]?.source).toBe("account_state"); + expect(plan.items[0]?.impact).toBe("high"); + const steps = plan.items.map((item) => item.step); + expect(new Set(steps).size).toBe(steps.length); + expect(steps[0]).toBe("Open PR count exceeds the current allowance (6/2)."); + expect(steps).toContain("Fix validation before asking maintainers to review."); + for (const item of plan.items) { + expect(item.rerunCondition.length).toBeGreaterThan(0); + expect(item.rank).toBeGreaterThan(0); + } + expect(JSON.stringify(plan)).not.toMatch(FORBIDDEN); + }); + + it("deduplicates overlapping branch-quality and score blockers", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["Local validation failed", "Local validation failed"], + scoreBlockers: ["Local validation failed", "GitHub checks need attention"], + recommendedRerunCondition: "Rerun after fixing branch-quality blockers or adding explicit validation/linked-context evidence.", + localFindings: [ + { + code: "failed_local_validation", + severity: "warning", + title: "Local validation failed", + detail: "1 validation command failed.", + action: "Fix validation before asking maintainers to review.", + }, + ], + }); + + expect(plan.items).toHaveLength(2); + expect(plan.items[0]?.step).toBe("Fix validation before asking maintainers to review."); + expect(plan.items.map((item) => item.step)).toEqual(["Fix validation before asking maintainers to review.", "GitHub checks need attention"]); + }); + + it("returns a public-safe empty-state plan when no blockers are present", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + branchQualityBlockers: [], + accountStateBlockers: [], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + }); + + expect(plan.items).toEqual([]); + expect(plan.summary).toMatch(/No blockers detected/i); + expect(plan.recommendedRerunCondition).toMatch(/branch, base, or PR state changes/i); + expect(JSON.stringify(plan)).not.toMatch(FORBIDDEN); + }); + + it("sanitizes scoreability language from rerun conditions", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["Branch eligibility blocks linked-issue assumptions"], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after branch/base eligibility metadata confirms eligibility or after linked issue assumptions change.", + }); + + expect(plan.recommendedRerunCondition).toMatch(/linked issue and base branch metadata/i); + expect(plan.recommendedRerunCondition).not.toMatch(/scoreability|multiplier/i); + }); +}); From 7e8bcfdb25a99108b418dba6f116f8ab19ab1ac4 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:26:32 +0200 Subject: [PATCH 02/11] test(mcp): raise remediation plan branch coverage Cover blocker-specific rerun paths and MCP tool-call behavior so CI branch coverage stays above the 97% threshold. Co-authored-by: Cursor --- test/unit/mcp-output-schemas.test.ts | 23 +++++++++++++++++++++++ test/unit/remediation-plan.test.ts | 19 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index 7c24abbe68..5245aa2767 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -176,6 +176,29 @@ describe("MCP tool calls return schema-valid structured content", () => { expect(JSON.stringify(data)).not.toMatch(/hotkey|coldkey|wallet|payout|reward/i); }); + it("gittensory_remediation_plan returns validated structured content", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" }); + const { client } = await connectTestClient(env); + const result = await client.callTool({ + name: "gittensory_remediation_plan", + arguments: { + login: "octo", + repoFullName: "octo/demo", + branchName: "feat/demo", + title: "Demo branch", + changedFiles: [{ path: "src/demo.ts", additions: 10, deletions: 1 }], + validation: [{ command: "npm test", status: "failed" }], + }, + }); + expect(result.isError).toBeFalsy(); + const data = result.structuredContent as Record; + expect(data.repoFullName).toBe("octo/demo"); + expect(data.login).toBe("octo"); + expect(Array.isArray(data.items)).toBe(true); + expect(typeof data.summary).toBe("string"); + }); + it("gittensory_get_repo_outcome_patterns reports not-found, computed, and cached outcomes", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "computed", full_name: "owner/computed", private: false, owner: { login: "owner" }, default_branch: "main" }); diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index 26a07c8b11..5ef8725347 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -90,4 +90,23 @@ describe("buildRemediationPlan", () => { expect(plan.recommendedRerunCondition).toMatch(/linked issue and base branch metadata/i); expect(plan.recommendedRerunCondition).not.toMatch(/scoreability|multiplier/i); }); + + it("maps blocker-specific rerun conditions and fallback steps", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["Open PR count exceeds threshold", "Contributor credibility history is still maturing"], + branchQualityBlockers: ["Local branch base is stale", "Linked issue is duplicate-prone", "GitHub checks need attention"], + scoreBlockers: ["wallet hotkey payout"], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + }); + + expect(plan.items.find((item) => item.source === "account_state" && /Open PR/i.test(item.step))?.rerunCondition).toMatch(/pending PRs merge\/close/i); + expect(plan.items.find((item) => /credibility history/i.test(item.step))?.rerunCondition).toMatch(/account\/queue maturity blockers clear/i); + expect(plan.items.find((item) => /stale/i.test(item.step))?.rerunCondition).toMatch(/git fetch origin/i); + expect(plan.items.find((item) => /duplicate-prone/i.test(item.step))?.rerunCondition).toMatch(/linked issue and base branch metadata/i); + expect(plan.items.find((item) => /GitHub checks/i.test(item.step))?.rerunCondition).toMatch(/validation evidence/i); + expect(plan.items.find((item) => item.source === "score")?.step).toBe("Resolve scoreability blockers before relying on this preview."); + expect(JSON.stringify(plan)).not.toMatch(/\bwallet\b|\bhotkey\b|\bpayout\b/i); + }); }); From 0c915b29b4e6a7679384f0d9ac0770714bdf73a2 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:29:28 +0200 Subject: [PATCH 03/11] fix(mcp): drop sanitized-only remediation steps Treat wallet or score redactions as empty steps so fallback remediation text is used, and align tests with public sanitizer output. Co-authored-by: Cursor --- src/services/remediation-plan.ts | 3 ++- test/unit/remediation-plan.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/remediation-plan.ts b/src/services/remediation-plan.ts index 2f457450f1..2bdd6ed8a8 100644 --- a/src/services/remediation-plan.ts +++ b/src/services/remediation-plan.ts @@ -45,7 +45,8 @@ const SOURCE_PRIORITY: Record = { function publicSafeText(value: string): string { const sanitized = sanitizePublicComment(value).trim(); - return sanitized && !FORBIDDEN_PATTERN.test(sanitized) ? sanitized : ""; + if (!sanitized || FORBIDDEN_PATTERN.test(sanitized) || /^(?:private context\s*)+$/i.test(sanitized)) return ""; + return sanitized; } function publicSafeRerunCondition(condition: string): string { diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index 5ef8725347..21992dff61 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -102,7 +102,7 @@ describe("buildRemediationPlan", () => { }); expect(plan.items.find((item) => item.source === "account_state" && /Open PR/i.test(item.step))?.rerunCondition).toMatch(/pending PRs merge\/close/i); - expect(plan.items.find((item) => /credibility history/i.test(item.step))?.rerunCondition).toMatch(/account\/queue maturity blockers clear/i); + expect(plan.items.find((item) => /maturing/i.test(item.step))?.rerunCondition).toMatch(/account\/queue maturity blockers clear/i); expect(plan.items.find((item) => /stale/i.test(item.step))?.rerunCondition).toMatch(/git fetch origin/i); expect(plan.items.find((item) => /duplicate-prone/i.test(item.step))?.rerunCondition).toMatch(/linked issue and base branch metadata/i); expect(plan.items.find((item) => /GitHub checks/i.test(item.step))?.rerunCondition).toMatch(/validation evidence/i); From 91bb0633e09f765ef49b1651b1094781ee70d653 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:32:02 +0200 Subject: [PATCH 04/11] test(mcp): cover remediation plan fallback branches Co-authored-by: Cursor --- test/unit/remediation-plan.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index 21992dff61..9bdf5f2fdc 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -109,4 +109,33 @@ describe("buildRemediationPlan", () => { expect(plan.items.find((item) => item.source === "score")?.step).toBe("Resolve scoreability blockers before relying on this preview."); expect(JSON.stringify(plan)).not.toMatch(/\bwallet\b|\bhotkey\b|\bpayout\b/i); }); + + it("covers fallback rerun and step branches for sanitized-only input", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["Repository allocation is inactive"], + branchQualityBlockers: ["wallet hotkey payout"], + scoreBlockers: [""], + recommendedRerunCondition: "scoreability multiplier eligibility score preview", + }); + + expect(plan.items.find((item) => item.source === "account_state")?.step).toBe("Repository allocation is inactive"); + expect(plan.items.find((item) => item.source === "branch_quality")?.step).toBe("Resolve branch-quality findings before submission."); + expect(plan.recommendedRerunCondition).toMatch(/linked issue and base branch metadata/i); + expect(plan.summary).toMatch(/2 remediation step/i); + }); + + it("falls back when recommended rerun text is fully redacted", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["Needs cleanup"], + scoreBlockers: [], + recommendedRerunCondition: "wallet hotkey payout reward farming", + }); + + expect(plan.recommendedRerunCondition).toMatch(/branch, base, or PR state changes/i); + }); }); From db9259e57997f5781e90d59f259f373820e960a1 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:34:54 +0200 Subject: [PATCH 05/11] test(mcp): cover remediation plan API route Co-authored-by: Cursor --- test/integration/api.test.ts | 32 ++++++++++++++++++++++++++++++ test/unit/remediation-plan.test.ts | 22 ++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 63c04bf09a..229fe80341 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1185,6 +1185,38 @@ describe("api routes", () => { }); expect(JSON.stringify(localBranchPayload.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i); + const remediationPlan = await app.request( + "/v1/local/remediation-plan", + { + method: "POST", + headers: apiHeaders(env), + body: JSON.stringify({ + login: "oktofeesh1", + repoFullName: "entrius/allways-ui", + baseRef: "origin/test", + headRef: "fix-cache", + branchName: "fix-cache-reconnect", + title: "Fix dashboard cache refresh after reconnect", + body: "Fixes #7", + labels: ["bug"], + changedFiles: [ + { path: "src/cache.ts", additions: 42, deletions: 4, status: "modified" }, + { path: "test/cache.test.ts", additions: 20, deletions: 0, status: "added" }, + ], + validation: [{ command: "npm test -- cache", status: "failed", summary: "cache regression failed" }], + localScorer: { mode: "external_command", sourceTokenScore: 42, totalTokenScore: 66, sourceLines: 44, testTokenScore: 20 }, + branchEligibility: { status: "eligible", source: "github_metadata", checkedAt: "2026-05-30T00:00:00.000Z" }, + }), + }, + env, + ); + expect(remediationPlan.status).toBe(200); + await expect(remediationPlan.json()).resolves.toMatchObject({ + login: "oktofeesh1", + repoFullName: "entrius/allways-ui", + items: expect.arrayContaining([expect.objectContaining({ rank: 1, step: expect.any(String), rerunCondition: expect.any(String) })]), + }); + const localBranchWithMcpToken = await app.request( "/v1/local/branch-analysis", { diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index 9bdf5f2fdc..18681217b3 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -138,4 +138,26 @@ describe("buildRemediationPlan", () => { expect(plan.recommendedRerunCondition).toMatch(/branch, base, or PR state changes/i); }); + + it("uses account-state fallback copy when a blocker is fully redacted", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["wallet hotkey payout"], + branchQualityBlockers: [], + scoreBlockers: ["reward farming score preview"], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + }); + + expect(plan.items).toEqual([ + expect.objectContaining({ + source: "account_state", + step: "Clear account or queue maturity blockers before opening more work.", + }), + expect.objectContaining({ + source: "score", + step: "Resolve scoreability blockers before relying on this preview.", + }), + ]); + }); }); From f4a2c1db94f896d412bed803add85cc01afde34c Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:43:21 +0200 Subject: [PATCH 06/11] fix(deps): override ws and refresh lockfile for npm audit New advisories for ws, tar, and js-yaml caused CI audit step to fail. Pin ws via overrides and refresh the lockfile so npm audit --audit-level=moderate passes. Co-authored-by: Cursor --- package-lock.json | 30 ++++++++++++++++++++---------- package.json | 3 ++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ee2c1584e..bff2cecce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9435,9 +9435,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -12382,9 +12392,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13303,9 +13313,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -13487,7 +13497,7 @@ }, "packages/gittensory-mcp": { "name": "@jsonbored/gittensory-mcp", - "version": "0.5.0", + "version": "0.6.0", "license": "AGPL-3.0-only", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", diff --git a/package.json b/package.json index cd3a1949ce..ba49bb780b 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ }, "vite": { "esbuild": "^0.28.1" - } + }, + "ws": "^8.21.0" }, "main": "index.js", "directories": { From 47b1b774586d8d7d1dac2c69e6637ef622167e3c Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:57:01 +0200 Subject: [PATCH 07/11] test: raise coverage after merging score-breakdown Add explain-breakdown API and MCP tests plus branch coverage for score multipliers so CI stays above the 97% threshold. Co-authored-by: Cursor --- test/integration/api.test.ts | 36 ++++++++++++++++++++++++ test/unit/mcp-output-schemas.test.ts | 11 ++++++++ test/unit/remediation-plan.test.ts | 27 ++++++++++++++++++ test/unit/score-breakdown.test.ts | 41 ++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+) diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index c3b4913abb..60ed790153 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1531,6 +1531,42 @@ describe("api routes", () => { ); expect(noContributorScorePreview.status).toBe(200); + const scoreBreakdown = await app.request( + "/v1/scoring/explain-breakdown", + { + method: "POST", + headers: apiHeaders(env), + body: JSON.stringify({ + repoFullName: "entrius/allways-ui", + contributorLogin: "oktofeesh1", + sourceTokenScore: 42, + totalTokenScore: 60, + sourceLines: 40, + openPrCount: 1, + linkedIssueMode: "standard", + }), + }, + env, + ); + expect(scoreBreakdown.status).toBe(200); + await expect(scoreBreakdown.json()).resolves.toMatchObject({ + repoFullName: "entrius/allways-ui", + components: expect.arrayContaining([expect.objectContaining({ component: expect.any(String), lever: expect.any(String) })]), + highestLeverageLever: expect.objectContaining({ component: expect.any(String), lever: expect.any(String) }), + }); + + const missingContributorBreakdown = await app.request( + "/v1/scoring/explain-breakdown", + { + method: "POST", + headers: apiHeaders(env), + body: JSON.stringify({ repoFullName: "entrius/allways-ui", sourceTokenScore: 42 }), + }, + env, + ); + expect(missingContributorBreakdown.status).toBe(400); + await expect(missingContributorBreakdown.json()).resolves.toMatchObject({ error: "contributor_login_required" }); + for (const [signalType, payload] of [ ["queue-health", { repoFullName: "entrius/allways-ui", signals: { openPullRequests: 2 } }], ["config-quality", { repoFullName: "entrius/allways-ui", notObservedConfiguredLabels: ["refactor"] }], diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index 046c83fe7b..009a15e20f 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -232,6 +232,17 @@ describe("MCP tool calls return schema-valid structured content", () => { expect(data.highestLeverageLever).toBeTruthy(); }); + it("gittensory_explain_score_breakdown requires contributorLogin", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" }); + const { client } = await connectTestClient(env); + const result = await client.callTool({ + name: "gittensory_explain_score_breakdown", + arguments: { repoFullName: "octo/demo", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80 }, + }); + expect(result.isError).toBe(true); + }); + it("gittensory_lint_pr_text returns a deterministic verdict and fixes", async () => { const { client } = await connectTestClient(); const weak = await client.callTool({ name: "gittensory_lint_pr_text", arguments: { commitMessages: ["wip"], prBody: "" } }); diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index 18681217b3..d95184d8d5 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -160,4 +160,31 @@ describe("buildRemediationPlan", () => { }), ]); }); + + it("skips blockers whose finding action and text are fully redacted", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["wallet hotkey payout"], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + localFindings: [ + { + code: "forbidden_action", + severity: "warning", + title: "wallet hotkey payout", + detail: "forbidden detail", + action: "wallet hotkey payout", + }, + ], + }); + + expect(plan.items).toEqual([ + expect.objectContaining({ + source: "branch_quality", + step: "Resolve branch-quality findings before submission.", + }), + ]); + }); }); diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 0b483dab86..56a19a353e 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -209,4 +209,45 @@ describe("explainScoreBreakdown", () => { expect(breakdown.highestLeverageLever.reason).toMatch(/reducer|optimization lever/i); expect(breakdown.highestLeverageLever.component).toMatch(/credibilityMultiplier|issueMultiplier|reviewPenaltyMultiplier/); }); + + it("marks eligible linked issues with a multiplier boost as full strength", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 120, + sourceLines: 60, + openPrCount: 0, + existingContributorTokenScore: 1200, + credibility: 1, + linkedIssueMode: "maintainer", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [7], solvedByPullRequests: [99] }, + }, + }); + + const breakdown = explainScoreBreakdown(preview); + expect(breakdown.components.find((entry) => entry.component === "issueMultiplier")).toMatchObject({ band: "full" }); + }); + + it("blocks near-zero multipliers that are not quite zero", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 90, + sourceLines: 50, + openPrCount: 20, + existingContributorTokenScore: 50, + credibility: 0.005, + }, + }); + + const breakdown = explainScoreBreakdown(preview); + expect(breakdown.components.find((entry) => entry.component === "credibilityMultiplier")).toMatchObject({ band: "blocked" }); + expect(breakdown.components.find((entry) => entry.component === "openPrMultiplier")).toMatchObject({ band: "blocked" }); + }); }); From 2373abba8a26eaf94dc28d90b3fdfbf90fbbdfab Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:00:32 +0200 Subject: [PATCH 08/11] fix(deps): bump hono lockfile for npm audit Refresh the lockfile so hono resolves to a patched release and CI audit passes. Co-authored-by: Cursor --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bff2cecce7..d1a1c46bca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9029,9 +9029,9 @@ } }, "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", "engines": { "node": ">=16.9.0" From 53e3c9b8a0f6bbee02740f6e55632e932b0ac5a3 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:59:32 +0200 Subject: [PATCH 09/11] test: raise remediation-plan patch coverage for codecov Cover invalid/unauthorized route paths, focusManifest handling, and remaining remediation-plan branch arms so patch coverage clears 97%. Co-authored-by: Cursor --- test/integration/routes-errors.test.ts | 3 +- test/unit/remediation-plan.test.ts | 60 ++++++++++++++ test/unit/routes-remediation-plan.test.ts | 95 +++++++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 test/unit/routes-remediation-plan.test.ts diff --git a/test/integration/routes-errors.test.ts b/test/integration/routes-errors.test.ts index 7d30557c4c..fcab402225 100644 --- a/test/integration/routes-errors.test.ts +++ b/test/integration/routes-errors.test.ts @@ -252,7 +252,7 @@ describe("api route guards and error branches", () => { branchName: "feature/private-work", changedFiles: [{ path: "src/private.ts", additions: 4, deletions: 1, status: "modified" }], }; - for (const path of ["/v1/local/branch-analysis", "/v1/agent/preflight-branch", "/v1/agent/prepare-pr-packet"] as const) { + for (const path of ["/v1/local/branch-analysis", "/v1/local/remediation-plan", "/v1/agent/preflight-branch", "/v1/agent/prepare-pr-packet"] as const) { const response = await app.request(path, { method: "POST", headers: sessionHeaders, body: JSON.stringify(victimBranchPayload) }, env); expect(response.status).toBe(403); await expect(response.json()).resolves.toMatchObject({ error: "forbidden_contributor" }); @@ -531,6 +531,7 @@ describe("api route guards and error branches", () => { expect((await app.request("/v1/preflight/pr", { method: "POST", headers: apiHeaders(env), body: "{}" }, env)).status).toBe(400); expect((await app.request("/v1/preflight/local-diff", { method: "POST", headers: apiHeaders(env), body: "{}" }, env)).status).toBe(400); expect((await app.request("/v1/local/branch-analysis", { method: "POST", headers: apiHeaders(env), body: "{}" }, env)).status).toBe(400); + expect((await app.request("/v1/local/remediation-plan", { method: "POST", headers: apiHeaders(env), body: "{}" }, env)).status).toBe(400); expect((await app.request("/v1/agent/runs/missing-run", { headers: apiHeaders(env) }, env)).status).toBe(404); expect((await app.request("/v1/agent/runs", { headers: apiHeaders(env) }, env)).status).toBe(400); expect((await app.request("/v1/agent/runs", { method: "POST", headers: apiHeaders(env), body: "{}" }, env)).status).toBe(400); diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index d95184d8d5..f6af127f3d 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -187,4 +187,64 @@ describe("buildRemediationPlan", () => { }), ]); }); + + it("returns an empty plan when every blocker is fully redacted", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["wallet hotkey payout"], + branchQualityBlockers: ["reward farming score preview"], + scoreBlockers: ["ranking raw trust score"], + recommendedRerunCondition: "wallet hotkey payout reward farming", + }); + + expect(plan.items).toEqual([]); + expect(plan.summary).toMatch(/No blockers detected/i); + expect(plan.recommendedRerunCondition).toMatch(/branch, base, or PR state changes/i); + }); + + it("uses score-source rerun conditions and medium impact for generic score blockers", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: [], + scoreBlockers: ["Branch preview confidence is low"], + recommendedRerunCondition: "Rerun after local validation passes.", + }); + + expect(plan.items).toEqual([ + expect.objectContaining({ + source: "score", + impact: "medium", + rerunCondition: "Rerun after local validation passes.", + }), + ]); + }); + + it("strips local filesystem paths from public remediation steps", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["/Users/miner/project/src/demo.ts"], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + }); + + expect(plan.items[0]?.step).toBe("Resolve branch-quality findings before submission."); + }); + + it("skips blank blocker entries during deduplication", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [" "], + branchQualityBlockers: [], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after any branch, base, or PR state changes before opening/submitting.", + }); + + expect(plan.items).toEqual([]); + }); }); diff --git a/test/unit/routes-remediation-plan.test.ts b/test/unit/routes-remediation-plan.test.ts new file mode 100644 index 0000000000..4a0f975548 --- /dev/null +++ b/test/unit/routes-remediation-plan.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import { createApp } from "../../src/api/routes"; +import { createSessionForGitHubUser } from "../../src/auth/security"; +import { upsertInstallation, upsertRepositoryFromGitHub } from "../../src/db/repositories"; +import { createTestEnv } from "../helpers/d1"; + +const PATH = "/v1/local/remediation-plan"; + +function apiHeaders(env: Env): Record { + return { + authorization: `Bearer ${env.GITTENSORY_API_TOKEN}`, + "content-type": "application/json", + }; +} + +function branchPayload(login: string, repoFullName: string, extra?: Record) { + return { + login, + repoFullName, + branchName: "feat/demo", + changedFiles: [{ path: "src/demo.ts", additions: 10, deletions: 1 }], + validation: [{ command: "npm test", status: "failed" }], + ...extra, + }; +} + +async function seedRepo(env: Env, owner: string, name: string, installationId: number): Promise { + await upsertInstallation(env, { + installation: { + id: installationId, + account: { login: owner, id: installationId, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", contents: "read" }, + events: ["repository"], + }, + }); + await upsertRepositoryFromGitHub( + env, + { name, full_name: `${owner}/${name}`, private: false, owner: { login: owner } }, + installationId, + ); +} + +describe("remediation-plan route", () => { + it("returns 400 for invalid local branch analysis payloads", async () => { + const app = createApp(); + const env = createTestEnv(); + const response = await app.request(PATH, { method: "POST", headers: apiHeaders(env), body: "{}" }, env); + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ error: "invalid_local_branch_analysis_request" }); + }); + + it("returns forbidden_contributor when a session login does not match the payload login", async () => { + const app = createApp(); + const env = createTestEnv(); + await seedRepo(env, "miner", "demo", 301); + const { token } = await createSessionForGitHubUser(env, { login: "other-user", id: 302 }); + const response = await app.request( + PATH, + { + method: "POST", + headers: { cookie: `gittensory_session=${token}`, "content-type": "application/json" }, + body: JSON.stringify(branchPayload("miner", "miner/demo")), + }, + env, + ); + expect(response.status).toBe(403); + await expect(response.json()).resolves.toMatchObject({ error: "forbidden_contributor" }); + }); + + it("honors caller-supplied focusManifest instead of loading the repo manifest", async () => { + const app = createApp(); + const env = createTestEnv(); + await seedRepo(env, "miner", "demo", 301); + const response = await app.request( + PATH, + { + method: "POST", + headers: apiHeaders(env), + body: JSON.stringify( + branchPayload("oktofeesh1", "miner/demo", { + focusManifest: { present: true, wantedPaths: ["src/"], source: "caller" }, + }), + ), + }, + env, + ); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + login: "oktofeesh1", + repoFullName: "miner/demo", + items: expect.any(Array), + }); + }); +}); From 80c3f87b7c12ce595bcc23accce2f8ce886133b5 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Wed, 17 Jun 2026 00:02:11 +0200 Subject: [PATCH 10/11] test: fix remediation-plan route and redaction expectations Use an admin session for forbidden_contributor coverage and expect fallback steps when blocker text is fully redacted. Co-authored-by: Cursor --- test/unit/remediation-plan.test.ts | 9 ++++++--- test/unit/routes-remediation-plan.test.ts | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index f6af127f3d..e5a07c1e94 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -188,7 +188,7 @@ describe("buildRemediationPlan", () => { ]); }); - it("returns an empty plan when every blocker is fully redacted", () => { + it("falls back to public-safe copy when every blocker string is fully redacted", () => { const plan = buildRemediationPlan({ login: "miner", repoFullName: "octo/demo", @@ -198,8 +198,11 @@ describe("buildRemediationPlan", () => { recommendedRerunCondition: "wallet hotkey payout reward farming", }); - expect(plan.items).toEqual([]); - expect(plan.summary).toMatch(/No blockers detected/i); + expect(plan.items).toEqual([ + expect.objectContaining({ source: "account_state", step: "Clear account or queue maturity blockers before opening more work." }), + expect.objectContaining({ source: "branch_quality", step: "Resolve branch-quality findings before submission." }), + expect.objectContaining({ source: "score", step: "Resolve scoreability blockers before relying on this preview." }), + ]); expect(plan.recommendedRerunCondition).toMatch(/branch, base, or PR state changes/i); }); diff --git a/test/unit/routes-remediation-plan.test.ts b/test/unit/routes-remediation-plan.test.ts index 4a0f975548..7caa6c685b 100644 --- a/test/unit/routes-remediation-plan.test.ts +++ b/test/unit/routes-remediation-plan.test.ts @@ -52,15 +52,15 @@ describe("remediation-plan route", () => { it("returns forbidden_contributor when a session login does not match the payload login", async () => { const app = createApp(); - const env = createTestEnv(); - await seedRepo(env, "miner", "demo", 301); - const { token } = await createSessionForGitHubUser(env, { login: "other-user", id: 302 }); + const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "attacker" }); + await seedRepo(env, "owner", "private-repo", 301); + const { token } = await createSessionForGitHubUser(env, { login: "attacker", id: 7 }); const response = await app.request( PATH, { method: "POST", - headers: { cookie: `gittensory_session=${token}`, "content-type": "application/json" }, - body: JSON.stringify(branchPayload("miner", "miner/demo")), + headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, + body: JSON.stringify(branchPayload("victim", "owner/private-repo")), }, env, ); From 599359e5087e472a628d259b13a79008b28a4d22 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Wed, 17 Jun 2026 00:05:49 +0200 Subject: [PATCH 11/11] test: cover repo manifest fallback and generic rerun branches Exercise the remediation-plan route manifest merge path and remaining remediation-plan rerun fallbacks to clear codecov patch coverage. Co-authored-by: Cursor --- test/unit/remediation-plan.test.ts | 26 +++++++++++++++++++++++ test/unit/routes-remediation-plan.test.ts | 23 ++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/test/unit/remediation-plan.test.ts b/test/unit/remediation-plan.test.ts index e5a07c1e94..21103d8e24 100644 --- a/test/unit/remediation-plan.test.ts +++ b/test/unit/remediation-plan.test.ts @@ -250,4 +250,30 @@ describe("buildRemediationPlan", () => { expect(plan.items).toEqual([]); }); + + it("uses the recommended rerun condition for generic account-state blockers", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: ["Repository allocation is inactive"], + branchQualityBlockers: [], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after registration completes.", + }); + + expect(plan.items[0]?.rerunCondition).toBe("Rerun after registration completes."); + }); + + it("uses the recommended rerun condition for generic branch-quality blockers", () => { + const plan = buildRemediationPlan({ + login: "miner", + repoFullName: "octo/demo", + accountStateBlockers: [], + branchQualityBlockers: ["Needs cleanup"], + scoreBlockers: [], + recommendedRerunCondition: "Rerun after docs are updated.", + }); + + expect(plan.items[0]?.rerunCondition).toBe("Rerun after docs are updated."); + }); }); diff --git a/test/unit/routes-remediation-plan.test.ts b/test/unit/routes-remediation-plan.test.ts index 7caa6c685b..143b67ca44 100644 --- a/test/unit/routes-remediation-plan.test.ts +++ b/test/unit/routes-remediation-plan.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { createApp } from "../../src/api/routes"; import { createSessionForGitHubUser } from "../../src/auth/security"; import { upsertInstallation, upsertRepositoryFromGitHub } from "../../src/db/repositories"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { createTestEnv } from "../helpers/d1"; const PATH = "/v1/local/remediation-plan"; @@ -92,4 +93,26 @@ describe("remediation-plan route", () => { items: expect.any(Array), }); }); + + it("falls back to the persisted repo manifest when the caller omits focusManifest", async () => { + const app = createApp(); + const env = createTestEnv(); + await seedRepo(env, "miner", "demo", 301); + await upsertRepoFocusManifest(env, "miner/demo", { wantedPaths: ["src/"], blockedPaths: ["dist/"] }); + const response = await app.request( + PATH, + { + method: "POST", + headers: apiHeaders(env), + body: JSON.stringify(branchPayload("oktofeesh1", "miner/demo")), + }, + env, + ); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + login: "oktofeesh1", + repoFullName: "miner/demo", + summary: expect.any(String), + }); + }); });