|
1 | 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
2 | 2 | import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; |
3 | 3 | import { describe, expect, it, vi } from "vitest"; |
4 | | -import { persistSignalSnapshot, upsertBounty, upsertIssueFromGitHub, upsertPullRequestFromGitHub, upsertRepositoryFromGitHub, updatePullRequestSlopAssessment } from "../../src/db/repositories"; |
| 4 | +import { persistSignalSnapshot, upsertBounty, upsertIssueFromGitHub, upsertPullRequestFromGitHub, upsertRepositoryFromGitHub, updatePullRequestSlopAssessment, persistUpstreamRulesetSnapshot } from "../../src/db/repositories"; |
5 | 5 | import type { AuthIdentity } from "../../src/auth/security"; |
6 | 6 | import { LoopoverMcp } from "../../src/mcp/server"; |
7 | 7 | import { normalizeRegistryPayload } from "../../src/registry/normalize"; |
@@ -36,6 +36,7 @@ const TOOLS_WITH_OUTPUT_SCHEMA = [ |
36 | 36 | "loopover_get_registry_changes", |
37 | 37 | "loopover_get_registry_snapshot", |
38 | 38 | "loopover_get_upstream_drift", |
| 39 | + "loopover_get_upstream_ruleset", |
39 | 40 | "loopover_local_status", |
40 | 41 | "loopover_remediation_plan", |
41 | 42 | "loopover_explain_score_breakdown", |
@@ -142,6 +143,10 @@ describe("MCP output schema discovery", () => { |
142 | 143 | const registrySnapshot = byName.get("loopover_get_registry_snapshot"); |
143 | 144 | const registrySnapshotProps = Object.keys((registrySnapshot?.outputSchema?.properties ?? {}) as Record<string, unknown>); |
144 | 145 | expect(registrySnapshotProps).toEqual(expect.arrayContaining(["id", "repoCount", "repositories", "error"])); |
| 146 | + |
| 147 | + const upstreamRuleset = byName.get("loopover_get_upstream_ruleset"); |
| 148 | + const upstreamRulesetProps = Object.keys((upstreamRuleset?.outputSchema?.properties ?? {}) as Record<string, unknown>); |
| 149 | + expect(upstreamRulesetProps).toEqual(expect.arrayContaining(["id", "activeModel", "registryRepoCount", "payload", "error"])); |
145 | 150 | }); |
146 | 151 |
|
147 | 152 | it("preserves the full tool inventory while adding output schemas", async () => { |
@@ -217,6 +222,27 @@ describe("MCP tool calls return schema-valid structured content", () => { |
217 | 222 | expect(result.structuredContent).toEqual({ error: "registry_snapshot_not_found" }); |
218 | 223 | }); |
219 | 224 |
|
| 225 | + it("loopover_get_upstream_ruleset returns the latest ruleset when one exists (#7807)", async () => { |
| 226 | + const env = createTestEnv(); |
| 227 | + await seedUpstreamRulesetSnapshot(env); |
| 228 | + const { client } = await connectTestClient(env); |
| 229 | + const result = await client.callTool({ name: "loopover_get_upstream_ruleset", arguments: {} }); |
| 230 | + expect(result.isError).toBeFalsy(); |
| 231 | + expect(result.structuredContent).toMatchObject({ |
| 232 | + id: "fixture-upstream-ruleset", |
| 233 | + activeModel: "pending_saturation_model", |
| 234 | + registryRepoCount: 1, |
| 235 | + }); |
| 236 | + expect(JSON.stringify(result.structuredContent)).not.toContain("upstream_ruleset_not_found"); |
| 237 | + }); |
| 238 | + |
| 239 | + it("loopover_get_upstream_ruleset returns a normal not-found result when empty (#7807)", async () => { |
| 240 | + const { client } = await connectTestClient(createTestEnv()); |
| 241 | + const result = await client.callTool({ name: "loopover_get_upstream_ruleset", arguments: {} }); |
| 242 | + expect(result.isError).toBeFalsy(); |
| 243 | + expect(result.structuredContent).toEqual({ error: "upstream_ruleset_not_found" }); |
| 244 | + }); |
| 245 | + |
220 | 246 | it("loopover_get_repo_context returns validated structured content", async () => { |
221 | 247 | const { client } = await connectTestClient(); |
222 | 248 | const result = await client.callTool({ name: "loopover_get_repo_context", arguments: { owner: "octo", repo: "demo" } }); |
@@ -682,7 +708,7 @@ describe("MCP output schemas do not declare private financial fields", () => { |
682 | 708 | it("structured content from public-safe tools never includes redacted financial keys", async () => { |
683 | 709 | const { client } = await connectTestClient(); |
684 | 710 |
|
685 | | - for (const name of ["loopover_local_status", "loopover_get_upstream_drift", "loopover_get_registry_changes", "loopover_get_registry_snapshot"]) { |
| 711 | + for (const name of ["loopover_local_status", "loopover_get_upstream_drift", "loopover_get_upstream_ruleset", "loopover_get_registry_changes", "loopover_get_registry_snapshot"]) { |
686 | 712 | const result = await client.callTool({ name, arguments: {} }); |
687 | 713 | const serialized = JSON.stringify(result.structuredContent ?? {}); |
688 | 714 | expect(serialized, `tool "${name}" structured content must not leak financial fields`).not.toMatch( |
@@ -719,6 +745,29 @@ async function seedRegistryChangeSnapshots(env: Env) { |
719 | 745 | ); |
720 | 746 | } |
721 | 747 |
|
| 748 | +async function seedUpstreamRulesetSnapshot(env: Env) { |
| 749 | + await persistUpstreamRulesetSnapshot(env, { |
| 750 | + id: "fixture-upstream-ruleset", |
| 751 | + sourceRepo: "entrius/gittensor", |
| 752 | + sourceRef: "test", |
| 753 | + commitSha: "fixture-commit", |
| 754 | + sourceSnapshotIds: [], |
| 755 | + activeModel: "pending_saturation_model", |
| 756 | + registryRepoCount: 1, |
| 757 | + totalEmissionShare: 0.01, |
| 758 | + semanticHash: "fixture-semantic-hash", |
| 759 | + payload: { |
| 760 | + registry: { |
| 761 | + repoCount: 1, |
| 762 | + totalEmissionShare: 0.01, |
| 763 | + repositories: [], |
| 764 | + }, |
| 765 | + }, |
| 766 | + warnings: [], |
| 767 | + generatedAt: "2026-05-30T00:00:00.000Z", |
| 768 | + }); |
| 769 | +} |
| 770 | + |
722 | 771 | function repoOutcomePatternsPayload(repoFullName: string, generatedAt: string) { |
723 | 772 | return { |
724 | 773 | repoFullName, |
|
0 commit comments