From 19b1326d4d17bd474b86247e42179771ecb81fa4 Mon Sep 17 00:00:00 2001 From: davion-knight <298846663+davion-knight@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:30:59 -0500 Subject: [PATCH] feat(api): add a REST route + CLI mirror for loopover_check_test_evidence loopover_check_test_evidence had neither a REST route nor a CLI mirror, unlike its same-tier deterministic-lint sibling loopover_check_slop_risk, which already has full parity (POST /v1/lint/slop-risk + an in-process stdio tool). Both are rate-limit-only, pure, path-metadata computations, so the asymmetry was arbitrary. The classification/guidance logic lived inside src/mcp/server.ts as a private method, so mirroring it by copy would have created a second, drifting implementation. Instead it is extracted verbatim into the engine as buildTestEvidenceReport (packages/loopover-engine/src/signals/test-evidence.ts, alongside the classifyTestCoverage/hasLocalTestEvidence/isCodeFile/isTestPath primitives it already uses). All THREE surfaces now derive their verdict from that one function: - src/mcp/server.ts's checkTestEvidence delegates to it (behaviour-preserving; the existing MCP tool tests pass unchanged). - POST /v1/lint/test-evidence, placed beside /v1/lint/slop-risk and structured identically. - The loopover_check_test_evidence stdio tool, computed IN-PROCESS like the check_slop_risk sibling rather than proxying over HTTP, so coverage self-checks work fully offline. Parity is therefore true by construction, and the tests pin it: for every arm (absent, free-text credit, docs-only, strong, adequate, weak) the route and the stdio tool each return exactly what the shared builder returns. The CLI test runs against a black-holed API URL, proving no round-trip. Plus zod/400 rejection on both surfaces. 100% line and branch coverage on every changed line across routes.ts, server.ts, and the engine builder. Closes #6749 --- .../src/signals/test-evidence.ts | 47 ++++++++++++ packages/loopover-mcp/bin/loopover-mcp.js | 27 +++++++ src/api/routes.ts | 19 +++++ src/mcp/server.ts | 35 ++------- test/unit/mcp-cli-test-evidence-tool.test.ts | 63 ++++++++++++++++ test/unit/mcp-tool-rename-aliases.test.ts | 10 +-- test/unit/routes-test-evidence.test.ts | 73 +++++++++++++++++++ 7 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 test/unit/mcp-cli-test-evidence-tool.test.ts create mode 100644 test/unit/routes-test-evidence.test.ts diff --git a/packages/loopover-engine/src/signals/test-evidence.ts b/packages/loopover-engine/src/signals/test-evidence.ts index c6801ae508..3f87c74aa3 100644 --- a/packages/loopover-engine/src/signals/test-evidence.ts +++ b/packages/loopover-engine/src/signals/test-evidence.ts @@ -125,3 +125,50 @@ export function classifyTestCoverage(changedPaths: string[]): TestCoverageClassi // spec for a framework this engine doesn't recognize. export const TEST_FRAMEWORKS = ["vitest", "jest", "pytest", "go-test", "rspec", "cargo-test"] as const; export type TestFramework = (typeof TEST_FRAMEWORKS)[number]; + +/** The test-evidence report shape returned identically by the MCP tool, the REST route, and the CLI mirror. */ +export type TestEvidenceReport = { + classification: TestCoverageClassification; + changedFileCount: number; + codeFileCount: number; + testFileCount: number; + guidance: string[]; +}; + +/** + * PURE: classify a planned change's test evidence from path metadata (plus optional free-text `tests` notes) and + * render actionable guidance. Extracted (#6749) so the remote MCP tool, `POST /v1/lint/test-evidence`, and the + * local CLI mirror all derive a byte-identical verdict from ONE implementation instead of three drifting copies. + */ +export function buildTestEvidenceReport(input: { + changedPaths: readonly string[]; + testFiles?: readonly string[] | undefined; + tests?: string | undefined; +}): TestEvidenceReport { + const allPaths = [...input.changedPaths, ...(input.testFiles ?? [])]; + const codeFileCount = input.changedPaths.filter(isCodeFile).length; + let classification = classifyTestCoverage(allPaths); + let testFileCount = allPaths.filter(isTestPath).length; + // Credit free-text `tests` evidence (e.g. "ran `go test ./...` locally, no new file") the same way the + // sibling tools loopover_check_slop_risk / loopover_suggest_boundary_tests already do via + // hasLocalTestEvidence. Only ever LIFT an otherwise-"absent" verdict -- never make this more lenient than + // the path-based signal once real test-file evidence (weak/adequate/strong) already exists. + const creditedByFreeTextTests = classification === "absent" && hasLocalTestEvidence({ tests: input.tests, testFiles: input.testFiles }); + if (creditedByFreeTextTests) { + classification = "adequate"; + testFileCount = Math.max(testFileCount, 1); + } + const guidance: string[] = []; + if (codeFileCount === 0) { + guidance.push("No hand-authored code files changed, so the missing-test-evidence signal does not apply (e.g. a docs- or config-only change)."); + } else if (creditedByFreeTextTests) { + guidance.push("No test file was detected among the changed paths, but the free-text `tests` evidence you supplied is credited as test evidence (the same way check_slop_risk and suggest_boundary_tests treat it)."); + } else if (classification === "absent") { + guidance.push("Changed code files carry no test evidence — add or update a test that exercises the change before opening the PR."); + } else if (classification === "strong") { + guidance.push("Test coverage looks strong for this change."); + } else { + guidance.push(`Test coverage is ${classification} for this change — adding another focused test would strengthen the evidence.`); + } + return { classification, changedFileCount: allPaths.length, codeFileCount, testFileCount, guidance }; +} diff --git a/packages/loopover-mcp/bin/loopover-mcp.js b/packages/loopover-mcp/bin/loopover-mcp.js index 8fd0ba9a6f..f4d3f3bf0c 100755 --- a/packages/loopover-mcp/bin/loopover-mcp.js +++ b/packages/loopover-mcp/bin/loopover-mcp.js @@ -26,6 +26,8 @@ import { computeLocalScorerTokens, } from "@loopover/engine"; import { buildSlopAssessment, SLOP_RUBRIC_MARKDOWN } from "@loopover/engine/signals/slop"; +// #6749: the same pure builder the remote MCP tool + /v1/lint/test-evidence both call. +import { buildTestEvidenceReport } from "@loopover/engine/signals/test-evidence"; import { z } from "zod"; import { buildBranchAnalysisPayload, collectLocalDiff, collectLocalBranchMetadata, probeLocalScorer, referenceScorePreviewExample, resolveScorePreviewCommand, resolveWorkspaceCwd, sanitizeLocalScorerStatus, setupGuidanceForLocalScorer, isTestFile } from "../lib/local-branch.js"; import { formatTable } from "../lib/format-table.js"; @@ -532,6 +534,13 @@ const validateConfigShape = { source: z.enum(["repo_file", "api_record", "none"]).optional(), }; +// #6749: mirrors checkTestEvidenceShape in src/mcp/server.ts exactly. +const checkTestEvidenceShape = { + changedPaths: z.array(z.string()).min(1), + testFiles: z.array(z.string()).optional(), + tests: z.string().optional(), +}; + const checkSlopRiskShape = { changedFiles: z .array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() })) @@ -841,6 +850,12 @@ const STDIO_TOOL_DESCRIPTORS = [ category: "review", description: "Assess the deterministic slop risk of a planned change from local diff metadata (paths + line counts) + the PR description — an agent-native, source-free quality self-check. Returns slopRisk (0-100), band, findings, and the rubric. Computed in-process; no repo data and no API round-trip.", }, + { + name: "loopover_check_test_evidence", + category: "review", + description: + "Classify whether a planned change's changed files carry enough test evidence, from path metadata alone (no source uploaded) — an agent-native coverage-gap self-check before opening a PR. Returns a coverage band (strong/adequate/weak/absent) plus actionable guidance. Computed in-process; no API round-trip.", + }, { name: "loopover_check_issue_slop", category: "review", @@ -1401,6 +1416,18 @@ registerStdioTool( (input) => toolResult("LoopOver slop-risk self-check.", { ...buildSlopAssessment(input), rubric: SLOP_RUBRIC_MARKDOWN }), ); +registerStdioTool( + "loopover_check_test_evidence", + { + description: stdioToolDescription("loopover_check_test_evidence"), + inputSchema: checkTestEvidenceShape, + }, + // Computed in-process from @loopover/engine (#6749) — the same buildTestEvidenceReport the remote server + // (src/mcp/server.ts) and the /v1/lint/test-evidence route both call, so all three surfaces return a + // byte-identical verdict and coverage self-checks work fully offline. + (input) => toolResult("LoopOver test-evidence check.", buildTestEvidenceReport(input)), +); + registerStdioTool( "loopover_check_issue_slop", { diff --git a/src/api/routes.ts b/src/api/routes.ts index 9c6a175e3b..11f3817641 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -197,6 +197,7 @@ import { } from "../services/control-panel-roles"; import { runFindOpportunities, validateFindOpportunitiesInput, type FindOpportunitiesInput } from "../mcp/find-opportunities"; import { runIssueRagRetrieval, validateIssueRagInput, type IssueRagInput } from "../mcp/issue-rag"; +import { buildTestEvidenceReport } from "../signals/test-evidence"; import { loadPrAiReviewFindings } from "../mcp/pr-ai-review-findings"; import { buildMcpCompatibilityMetadata, @@ -478,6 +479,14 @@ const validateFocusManifestSchema = z.object({ // Pure local-metadata slop self-checks (no repo data, no secrets) — mirror the loopover_check_slop_risk / // loopover_check_issue_slop MCP tools so the npm package can offer the same agent-native self-check. +// #6749: mirrors checkTestEvidenceShape in src/mcp/server.ts exactly, so REST can never accept what the tool +// would reject. +const testEvidenceSchema = z.object({ + changedPaths: z.array(z.string()).min(1), + testFiles: z.array(z.string()).optional(), + tests: z.string().optional(), +}); + const slopRiskSchema = z.object({ changedFiles: z .array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() })) @@ -3218,6 +3227,16 @@ export function createApp() { return c.json({ ...buildSlopAssessment(parsed.data), rubric: SLOP_RUBRIC_MARKDOWN }); }); + // #6749: REST mirror of the loopover_check_test_evidence MCP tool, bringing it to the same parity its + // same-tier deterministic-lint sibling /v1/lint/slop-risk (directly above) already has. Delegates to the + // engine's buildTestEvidenceReport -- the same function the MCP tool and the CLI mirror call. + app.post("/v1/lint/test-evidence", async (c) => { + const body = await c.req.json().catch(() => null); + const parsed = testEvidenceSchema.safeParse(body); + if (!parsed.success) return c.json({ error: "invalid_test_evidence_request", issues: parsed.error.issues }, 400); + return c.json(buildTestEvidenceReport(parsed.data)); + }); + app.post("/v1/lint/issue-slop", async (c) => { const body = await c.req.json().catch(() => null); const parsed = issueSlopSchema.safeParse(body); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 127f2c23ba..c567ffd8b9 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -156,7 +156,7 @@ import { buildTestGenSpec, type LocalWriteActionSpec, } from "./local-write-tools"; -import { classifyTestCoverage, hasLocalTestEvidence, isCodeFile, isTestPath, TEST_FRAMEWORKS } from "../signals/test-evidence"; +import { buildTestEvidenceReport, classifyTestCoverage, hasLocalTestEvidence, isCodeFile, isTestPath, TEST_FRAMEWORKS } from "../signals/test-evidence"; import { applyStepResult, buildPlanDag, nextReadySteps, planProgress, validatePlanDag, type PlanDag } from "../services/plan-dag"; import { buildFocusManifestValidation } from "../services/focus-manifest-validation"; import { isGlobalAgentPause, resolveAgentActionMode, resolveAgentPermissionReadiness } from "../settings/agent-execution"; @@ -3652,35 +3652,12 @@ export class LoopoverMcp { private async checkTestEvidence(input: z.infer>): Promise { await this.enforceToolRateLimit("loopover_check_test_evidence"); - const allPaths = [...input.changedPaths, ...(input.testFiles ?? [])]; - const codeFileCount = input.changedPaths.filter(isCodeFile).length; - let classification = classifyTestCoverage(allPaths); - let testFileCount = allPaths.filter(isTestPath).length; - // Credit free-text `tests` evidence (e.g. "ran `go test ./...` locally, no new file") the same way the - // sibling tools loopover_check_slop_risk / loopover_suggest_boundary_tests already do via - // hasLocalTestEvidence. Only ever LIFT an otherwise-"absent" verdict -- never make this more lenient than - // the path-based signal once real test-file evidence (weak/adequate/strong) already exists. - const creditedByFreeTextTests = - classification === "absent" && hasLocalTestEvidence({ tests: input.tests, testFiles: input.testFiles }); - if (creditedByFreeTextTests) { - classification = "adequate"; - testFileCount = Math.max(testFileCount, 1); - } - const guidance: string[] = []; - if (codeFileCount === 0) { - guidance.push("No hand-authored code files changed, so the missing-test-evidence signal does not apply (e.g. a docs- or config-only change)."); - } else if (creditedByFreeTextTests) { - guidance.push("No test file was detected among the changed paths, but the free-text `tests` evidence you supplied is credited as test evidence (the same way check_slop_risk and suggest_boundary_tests treat it)."); - } else if (classification === "absent") { - guidance.push("Changed code files carry no test evidence — add or update a test that exercises the change before opening the PR."); - } else if (classification === "strong") { - guidance.push("Test coverage looks strong for this change."); - } else { - guidance.push(`Test coverage is ${classification} for this change — adding another focused test would strengthen the evidence.`); - } + // #6749: the classification/guidance logic now lives in the engine's buildTestEvidenceReport, shared with + // POST /v1/lint/test-evidence and the local CLI mirror so all three surfaces agree by construction. + const report = buildTestEvidenceReport(input); return { - summary: `Test evidence: ${classification}.`, - data: { classification, changedFileCount: allPaths.length, codeFileCount, testFileCount, guidance } as unknown as Record, + summary: `Test evidence: ${report.classification}.`, + data: report as unknown as Record, }; } diff --git a/test/unit/mcp-cli-test-evidence-tool.test.ts b/test/unit/mcp-cli-test-evidence-tool.test.ts new file mode 100644 index 0000000000..e59992dfde --- /dev/null +++ b/test/unit/mcp-cli-test-evidence-tool.test.ts @@ -0,0 +1,63 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { buildTestEvidenceReport } from "../../src/signals/test-evidence"; + +// #6749: the local mirror of loopover_check_test_evidence. Like its same-tier sibling loopover_check_slop_risk +// it computes IN-PROCESS from @loopover/engine — no API round-trip — so coverage self-checks work offline. The +// point of these tests is cross-surface PARITY with the same buildTestEvidenceReport the route + MCP tool call. +const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +let client: Client; +let transport: StdioClientTransport; +let configDir: string; + +beforeEach(async () => { + configDir = mkdtempSync(join(tmpdir(), "loopover-test-evidence-")); + transport = new StdioClientTransport({ + command: "node", + args: [bin, "--stdio"], + // Black-holed API URL: a residual round-trip would fail every case, proving the in-process claim. + env: { ...process.env, LOOPOVER_CONFIG_DIR: configDir, LOOPOVER_TOKEN: "session-token", LOOPOVER_API_URL: "http://127.0.0.1:1", LOOPOVER_API_TIMEOUT_MS: "1000" }, + }); + client = new Client({ name: "test-evidence-tool-test", version: "0.0.1" }); + await client.connect(transport); +}); + +afterEach(async () => { + await client?.close().catch(() => undefined); + if (configDir) rmSync(configDir, { recursive: true, force: true }); +}); + +describe("loopover_check_test_evidence stdio mirror (#6749)", () => { + it("registers alongside its same-tier check_slop_risk sibling", async () => { + const names = new Set((await client.listTools()).tools.map((t) => t.name)); + expect(names).toContain("loopover_check_test_evidence"); + expect(names).toContain("loopover_check_slop_risk"); + }); + + it("matches the shared builder for every arm — offline, with no API reachable", async () => { + const cases = [ + { changedPaths: ["src/a.ts"] }, + { changedPaths: ["src/a.ts"], testFiles: ["test/a.test.ts"] }, + { changedPaths: ["src/a.ts"], tests: "ran `go test ./...` locally, no new file" }, + { changedPaths: ["README.md"] }, + ]; + for (const args of cases) { + const result = await client.callTool({ name: "loopover_check_test_evidence", arguments: args }); + expect(result.isError, JSON.stringify(args)).toBeFalsy(); + expect((result as { structuredContent?: unknown }).structuredContent, JSON.stringify(args)).toEqual( + JSON.parse(JSON.stringify(buildTestEvidenceReport(args))), + ); + } + }); + + it("rejects invalid input (zod input-schema validation)", async () => { + for (const args of [{}, { changedPaths: [] }, { changedPaths: "nope" }, { changedPaths: ["src/a.ts"], tests: 7 }]) { + const rejected = await client.callTool({ name: "loopover_check_test_evidence", arguments: args }).then((r) => Boolean(r.isError), () => true); + expect(rejected, `${JSON.stringify(args)} should be rejected`).toBe(true); + } + }); +}); diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts index 8e6af0efab..99f8e71888 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -52,14 +52,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { }); afterEach(disconnect); - it("lists exactly 64 loopover_ tools and zero gittensory_-prefixed aliases", async () => { + it("lists exactly 65 loopover_ tools and zero gittensory_-prefixed aliases", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); const primary = names.filter((n) => n.startsWith("loopover_")); const legacy = names.filter((n) => n.startsWith("gittensory_")); - expect(primary.length).toBe(64); + expect(primary.length).toBe(65); expect(legacy.length).toBe(0); - expect(names.length).toBe(64); + expect(names.length).toBe(65); }); it("no loopover_ tool's description carries a stale deprecation notice", async () => { @@ -69,11 +69,11 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { } }); - it("`loopover-mcp tools --json` reports the same 64-tool count the live server registers", async () => { + it("`loopover-mcp tools --json` reports the same 65-tool count the live server registers", async () => { const { tools } = await client.listTools(); const payload = JSON.parse(run(["tools", "--json"])) as { count: number; tools: Array<{ name: string }> }; expect(payload.count).toBe(tools.length); - expect(payload.count).toBe(64); + expect(payload.count).toBe(65); expect([...payload.tools.map((t) => t.name)].sort()).toEqual([...tools.map((t) => t.name)].sort()); }); }); diff --git a/test/unit/routes-test-evidence.test.ts b/test/unit/routes-test-evidence.test.ts new file mode 100644 index 0000000000..13c2afe416 --- /dev/null +++ b/test/unit/routes-test-evidence.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { createApp } from "../../src/api/routes"; +import { buildTestEvidenceReport } from "../../src/signals/test-evidence"; +import { createTestEnv } from "../helpers/d1"; + +// #6749: POST /v1/lint/test-evidence — the REST mirror bringing loopover_check_test_evidence to the parity its +// same-tier deterministic-lint sibling /v1/lint/slop-risk already has. Route + MCP tool + CLI all delegate to +// the engine's buildTestEvidenceReport, so these pin the ROUTE contract and assert cross-surface parity. +const apiHeaders = (env: Env) => ({ authorization: `Bearer ${env.LOOPOVER_API_TOKEN}`, "content-type": "application/json" }); +const PATH = "/v1/lint/test-evidence"; +const post = (env: Env, body: unknown) => createApp().request(PATH, { method: "POST", headers: apiHeaders(env), body: JSON.stringify(body) }, env); + +describe("POST /v1/lint/test-evidence (#6749)", () => { + it("matches the shared builder for every classification arm", async () => { + const env = createTestEnv(); + const cases = [ + { changedPaths: ["src/a.ts"] }, // code, no tests → absent + { changedPaths: ["src/a.ts"], testFiles: ["test/a.test.ts"] }, // real test evidence + { changedPaths: ["src/a.ts"], tests: "ran `go test ./...` locally, no new file" }, // free-text credit + { changedPaths: ["README.md"] }, // docs-only → signal does not apply + // ratio >= 0.4 -> strong + { changedPaths: ["src/a.ts", "src/b.ts", "src/c.ts"], testFiles: ["test/a.test.ts", "test/b.test.ts", "test/c.test.ts"] }, + // ratio >= 0.2 but < 0.4 -> adequate + { changedPaths: ["src/a.ts", "src/b.ts", "src/c.ts", "src/d.ts"], testFiles: ["test/a.test.ts"] }, + // ratio < 0.2 -> weak (one test across nine changed files) + { changedPaths: ["src/a.ts", "src/b.ts", "src/c.ts", "src/d.ts", "src/e.ts", "src/f.ts", "src/g.ts", "src/h.ts", "src/i.ts"], testFiles: ["test/a.test.ts"] }, + ]; + for (const body of cases) { + const response = await post(env, body); + expect(response.status, JSON.stringify(body)).toBe(200); + // PARITY: the route returns exactly what the MCP tool + CLI derive from the same builder. + await expect(response.json()).resolves.toEqual(JSON.parse(JSON.stringify(buildTestEvidenceReport(body)))); + } + }); + + it("renders the strong and weak guidance arms from the shared builder", async () => { + const env = createTestEnv(); + const strong = (await (await post(env, { changedPaths: ["src/a.ts", "src/b.ts"], testFiles: ["test/a.test.ts", "test/b.test.ts"] })).json()) as { classification: string; guidance: string[] }; + expect(strong.classification).toBe("strong"); + expect(strong.guidance.join(" ")).toMatch(/strong/i); + + const weak = (await (await post(env, { changedPaths: ["src/a.ts", "src/b.ts", "src/c.ts", "src/d.ts", "src/e.ts", "src/f.ts", "src/g.ts", "src/h.ts", "src/i.ts"], testFiles: ["test/a.test.ts"] })).json()) as { classification: string; guidance: string[] }; + expect(weak.classification).toBe("weak"); + expect(weak.guidance.join(" ")).toMatch(/adding another focused test/i); + }); + + it("credits free-text tests evidence only to LIFT an absent verdict, never to loosen a real one", async () => { + const env = createTestEnv(); + const credited = (await (await post(env, { changedPaths: ["src/a.ts"], tests: "ran the suite locally" })).json()) as { classification: string; guidance: string[] }; + expect(credited.classification).toBe("adequate"); + expect(credited.guidance.join(" ")).toMatch(/free-text/i); + + const absent = (await (await post(env, { changedPaths: ["src/a.ts"] })).json()) as { classification: string }; + expect(absent.classification).toBe("absent"); + }); + + it("rejects an invalid or unparseable body with 400", async () => { + const env = createTestEnv(); + for (const body of [{}, { changedPaths: [] }, { changedPaths: "nope" }, { changedPaths: ["src/a.ts"], tests: 7 }]) { + const response = await post(env, body); + expect(response.status, JSON.stringify(body)).toBe(400); + await expect(response.json()).resolves.toMatchObject({ error: "invalid_test_evidence_request" }); + } + const malformed = await createApp().request(PATH, { method: "POST", headers: apiHeaders(createTestEnv()), body: "{not json" }, createTestEnv()); + expect(malformed.status).toBe(400); + }); + + it("uploads no source and leaks no private terms — path metadata only", async () => { + const env = createTestEnv(); + const text = JSON.stringify(await (await post(env, { changedPaths: ["src/a.ts"] })).json()); + expect(text).not.toMatch(/wallet|hotkey|coldkey|trust score|reward/i); + }); +});