From 136ad2375aed7fd741a80d79cf1c0acb3f568681 Mon Sep 17 00:00:00 2001 From: Helios531 <57456290+Helios531@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:06:01 +0200 Subject: [PATCH] fix: gittensory-consume-metagraphed --- src/env.d.ts | 3 + src/queue/processors.ts | 7 ++ src/services/metagraphed.ts | 91 ++++++++++++++++++++++++++ src/signals/subnet-claim.ts | 104 ++++++++++++++++++++++++++++++ test/unit/metagraphed.test.ts | 100 +++++++++++++++++++++++++++++ test/unit/subnet-claim.test.ts | 114 +++++++++++++++++++++++++++++++++ 6 files changed, 419 insertions(+) create mode 100644 src/services/metagraphed.ts create mode 100644 src/signals/subnet-claim.ts create mode 100644 test/unit/metagraphed.test.ts create mode 100644 test/unit/subnet-claim.test.ts diff --git a/src/env.d.ts b/src/env.d.ts index 6c1ebfe65e..34a2eb4a97 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -27,6 +27,9 @@ declare global { GITTENSOR_UPSTREAM_REPO?: string; GITTENSOR_UPSTREAM_REF?: string; GITTENSOR_REGISTRY_URL: string; + /** #697: metagraphed base URL. When set, subnet/netuid integration claims in PRs/issues are validated + * (existence + interface health) and surfaced as ADVISORY gate evidence. Unset = feature dormant. */ + METAGRAPHED_API_URL?: string; GITHUB_PUBLIC_TOKEN?: string; /** #703: owner-gated global to apply upstream sigmoid time-decay in score previews. Default off. */ SCORING_TIME_DECAY_ENABLED?: string; diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 6cd4685e51..2436e232be 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -133,6 +133,7 @@ import { unionScopedOverlapClusters, } from "../signals/engine"; import { buildIssueSlopAssessment, buildSlopAssessment, type SlopBand } from "../signals/slop"; +import { assessSubnetClaimFindings } from "../services/metagraphed"; import { runGittensoryAiSlopAdvisory } from "../services/ai-slop"; import { decidePublicSurface } from "../signals/settings-preview"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; @@ -738,6 +739,9 @@ async function processGitHubWebhook(env: Env, deliveryId: string, eventName: str otherOpenPullRequests, requireLinkedIssue: settings.requireLinkedIssue || settings.linkedIssueGateMode !== "off", }); + // #697: validate any subnet/netuid integration claim against metagraphed and attach the verdict as + // advisory gate evidence. No-op (and no network call) unless METAGRAPHED_API_URL is configured. + advisory.findings.push(...(await assessSubnetClaimFindings(env, { title: pr.title, body: pr.body }))); await persistAdvisory(env, advisory); if (installationId && shouldProcessPullRequestPublicSurface(payload.action)) { await maybePublishPrPublicSurface(env, installationId, repoFullName, pr, repo, settings, advisory, { @@ -770,6 +774,9 @@ async function processGitHubWebhook(env: Env, deliveryId: string, eventName: str if (issueSettings.slopGateMode !== "off") { advisory.findings.push(...buildIssueSlopAssessment({ title: issue.title, body: issue.body }).findings); } + // #697: subnet/netuid claim validation also applies to issues ("integrates subnet X"). Advisory-only; + // dormant unless METAGRAPHED_API_URL is set. + advisory.findings.push(...(await assessSubnetClaimFindings(env, { title: issue.title, body: issue.body }))); await persistAdvisory(env, advisory); // #699 path B: a newly opened grabbable, high-multiplier issue notifies the miners watching this repo // (fanned out through the same #535 pipeline below). diff --git a/src/services/metagraphed.ts b/src/services/metagraphed.ts new file mode 100644 index 0000000000..136680744d --- /dev/null +++ b/src/services/metagraphed.ts @@ -0,0 +1,91 @@ +// #697 (roadmap #525): the metagraphed consumer. Validates a claimed Bittensor subnet/netuid against +// metagraphed (netuid existence + interface health) and adapts the verdict into advisory gate findings. +// +// This mirrors gittensor/api.ts's fetch discipline (JSON accept header, hard timeout, never let a slow +// upstream hang the Worker). It is fail-open and ADVISORY: any error, timeout, or unexpected shape maps to +// `unavailable`, which produces NO finding — a metagraphed outage must never block or spam a contributor +// (#525: advisory-first, no auto-block). The feature is dormant until `METAGRAPHED_API_URL` is configured. + +import type { AdvisoryFinding } from "../types"; +import { assessSubnetClaims, type NetuidValidation } from "../signals/subnet-claim"; + +/** Hard cap on a single metagraphed request so a slow/half-open upstream can never stall the webhook. */ +export const METAGRAPHED_FETCH_TIMEOUT_MS = 10_000; + +/** Tolerant view of metagraphed's subnet response. Only `exists === false` and `healthy === false` are + * treated as negative signals; any other/missing shape is read as "exists, healthy" so an unrecognized + * 200 never yields a false-positive finding. `interfaceHealthy` / `interface.healthy` are accepted as + * aliases for `healthy` to match the netuid-existence/chain-binding shape from the reviewbot work. */ +export type MetagraphedSubnetResponse = { + netuid?: number; + exists?: boolean; + healthy?: boolean; + interfaceHealthy?: boolean; + interface?: { healthy?: boolean } | null; +}; + +/** Map a parsed metagraphed subnet response to a validation verdict. Exported for direct unit testing. */ +export function interpretSubnetResponse(netuid: number, data: MetagraphedSubnetResponse): NetuidValidation { + if (data.exists === false) { + return { netuid, status: "not_found", detail: `metagraphed reports subnet ${netuid} does not exist.` }; + } + const healthy = data.healthy ?? data.interfaceHealthy ?? data.interface?.healthy; + if (healthy === false) { + return { netuid, status: "exists_unhealthy", detail: `metagraphed reports subnet ${netuid} interface health did not pass.` }; + } + return { netuid, status: "exists_healthy", detail: `metagraphed confirms subnet ${netuid}.` }; +} + +export type MetagraphedClientDeps = { + /** metagraphed base URL (no trailing slash required). */ + readonly baseUrl: string; + /** Injected fetch for tests; defaults to global `fetch`. */ + readonly fetchImpl?: typeof fetch | undefined; + readonly timeoutMs?: number | undefined; +}; + +/** + * Validate one netuid against metagraphed. Never rejects: a 404 → `not_found`, any other non-2xx / network + * error / timeout / parse failure → `unavailable`, and a 2xx is interpreted by {@link interpretSubnetResponse}. + */ +export async function validateNetuid(netuid: number, deps: MetagraphedClientDeps): Promise { + const base = deps.baseUrl.replace(/\/+$/, ""); + const url = `${base}/subnets/${netuid}`; + const fetchImpl = deps.fetchImpl ?? fetch; + try { + const response = await fetchImpl(url, { + headers: { accept: "application/json", "user-agent": "gittensory/0.1" }, + signal: AbortSignal.timeout(deps.timeoutMs ?? METAGRAPHED_FETCH_TIMEOUT_MS), + }); + if (response.status === 404) { + return { netuid, status: "not_found", detail: `metagraphed reports subnet ${netuid} does not exist.` }; + } + if (!response.ok) { + return { netuid, status: "unavailable", detail: `metagraphed returned status ${response.status} for subnet ${netuid}.` }; + } + return interpretSubnetResponse(netuid, (await response.json()) as MetagraphedSubnetResponse); + } catch { + return { netuid, status: "unavailable", detail: `metagraphed could not be reached for subnet ${netuid}.` }; + } +} + +/** + * Top-level adapter used by the webhook pipeline: detect subnet claims in a contribution's title/body and + * return advisory findings sourced from metagraphed. Returns `[]` (and makes no network call) when + * `METAGRAPHED_API_URL` is unset, so the feature is fully opt-in and existing behavior is unchanged. + */ +export async function assessSubnetClaimFindings( + env: Pick, + input: { readonly title?: string | null | undefined; readonly body?: string | null | undefined }, + deps: { readonly fetchImpl?: typeof fetch | undefined; readonly timeoutMs?: number | undefined } = {}, +): Promise { + const baseUrl = env.METAGRAPHED_API_URL?.trim(); + if (!baseUrl) return []; + return assessSubnetClaims(input, (netuid) => + validateNetuid(netuid, { + baseUrl, + ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}), + ...(deps.timeoutMs ? { timeoutMs: deps.timeoutMs } : {}), + }), + ); +} diff --git a/src/signals/subnet-claim.ts b/src/signals/subnet-claim.ts new file mode 100644 index 0000000000..fc9a438ea1 --- /dev/null +++ b/src/signals/subnet-claim.ts @@ -0,0 +1,104 @@ +// #697 (roadmap #525): gittensory consumes metagraphed — validate subnet/netuid claims as gate evidence. +// +// PURE core of the subnet-claim gate signal. It (a) detects when a contribution's text claims to integrate +// a Bittensor subnet/netuid, and (b) turns a metagraphed validation verdict for that netuid into a +// public-safe, ADVISORY `AdvisoryFinding`. The actual metagraphed HTTP call lives in +// ../services/metagraphed.ts (injected here as `validate`), so this module stays deterministic and +// unit-testable without a network. +// +// Advisory-first (#525): findings are `warning` severity at most — they surface evidence, they never hard +// block. Public-safe (#542): wording uses only subnet/netuid/interface/metagraphed vocabulary and is run +// through `isPublicSafeText` in tests, so it carries no reward/score/identity language. + +import type { AdvisoryFinding } from "../types"; + +/** A subnet/netuid integration claim parsed from contribution text. */ +export type SubnetClaim = { readonly netuid: number; readonly raw: string }; + +/** Verdict for one claimed netuid, sourced from metagraphed. `unavailable` = could not validate (metagraphed + * unreachable/unexpected) — deliberately produces NO finding so a metagraphed outage is never noisy. */ +export type NetuidValidationStatus = "exists_healthy" | "exists_unhealthy" | "not_found" | "unavailable"; + +export type NetuidValidation = { + readonly netuid: number; + readonly status: NetuidValidationStatus; + readonly detail: string; +}; + +/** Validate a single netuid against metagraphed. Implemented by ../services/metagraphed.ts; injected so the + * pure layer can be tested with a fake. Must never reject — connectivity failures map to `unavailable`. */ +export type NetuidValidator = (netuid: number) => Promise; + +/** Highest netuid we treat as a plausible subnet claim. Keeps detection from matching years / PR numbers / + * large unrelated integers (Bittensor netuids are small and dense from 0). */ +export const MAX_RECOGNIZED_NETUID = 1023; + +// Matches "subnet 42", "subnet #42", "subnet-42", "subnets 42", "netuid 42", "netuid: 5", "net uid 5", +// "netuid=12", "sn74", "SN 74", "subnet number 7". The number is captured; a separator is optional but the +// number must follow within optional whitespace/separator so plain words ("subnetwork", "snapshot") miss. +const SUBNET_CLAIM_PATTERN = /\b(?:net\s?uid|subnets?|sn)\s*(?:number\s*)?[:#=-]?\s*(\d{1,5})\b/gi; + +/** + * Detect distinct subnet/netuid integration claims in free text (PR/issue title + body). Returns at most one + * claim per netuid (first mention wins), sorted ascending for deterministic output. Out-of-range numbers + * (> MAX_RECOGNIZED_NETUID) are ignored to avoid false positives on years/IDs. + */ +export function detectSubnetClaims(text: string | null | undefined): SubnetClaim[] { + if (!text) return []; + const byNetuid = new Map(); + for (const match of text.matchAll(SUBNET_CLAIM_PATTERN)) { + const netuid = Number(match[1]); + if (!Number.isInteger(netuid) || netuid < 0 || netuid > MAX_RECOGNIZED_NETUID) continue; + if (!byNetuid.has(netuid)) byNetuid.set(netuid, match[0].trim()); + } + return [...byNetuid.entries()].map(([netuid, raw]) => ({ netuid, raw })).sort((a, b) => a.netuid - b.netuid); +} + +/** + * Turn one metagraphed verdict into an advisory finding. Returns `null` when there is nothing to surface — + * the netuid exists and is healthy, or metagraphed could not be reached (`unavailable`). Only a missing + * (`not_found`) or unhealthy (`exists_unhealthy`) subnet produces a finding. + */ +export function buildSubnetClaimFinding(validation: NetuidValidation): AdvisoryFinding | null { + const { netuid } = validation; + if (validation.status === "not_found") { + return { + code: "subnet_claim_not_found", + severity: "warning", + title: `Claimed subnet ${netuid} was not found via metagraphed`, + detail: `This contribution references subnet/netuid ${netuid}, but metagraphed reports no such subnet on the network. ${validation.detail}`, + action: `Verify the netuid and correct or remove the subnet ${netuid} integration claim.`, + publicText: `metagraphed could not find subnet ${netuid}; verify the referenced netuid.`, + }; + } + if (validation.status === "exists_unhealthy") { + return { + code: "subnet_claim_unhealthy", + severity: "warning", + title: `Claimed subnet ${netuid} appears unhealthy via metagraphed`, + detail: `metagraphed reports subnet/netuid ${netuid} exists but its interface health check did not pass, so the integration claim could not be confirmed as healthy. ${validation.detail}`, + action: `Confirm the subnet ${netuid} interface is reachable, or note the integration as experimental.`, + publicText: `metagraphed reports subnet ${netuid} exists but its interface health check did not pass.`, + }; + } + return null; +} + +/** + * Assess all subnet/netuid claims in a contribution's title + body and return the advisory findings sourced + * from metagraphed. Validates each distinct claimed netuid via the injected validator. Deterministic and + * never throws (the validator must map failures to `unavailable`). Empty result = no claims, or all claimed + * subnets validated cleanly / could not be checked. + */ +export async function assessSubnetClaims( + input: { readonly title?: string | null | undefined; readonly body?: string | null | undefined }, + validate: NetuidValidator, +): Promise { + const claims = detectSubnetClaims(`${input.title ?? ""}\n${input.body ?? ""}`); + const findings: AdvisoryFinding[] = []; + for (const claim of claims) { + const finding = buildSubnetClaimFinding(await validate(claim.netuid)); + if (finding) findings.push(finding); + } + return findings; +} diff --git a/test/unit/metagraphed.test.ts b/test/unit/metagraphed.test.ts new file mode 100644 index 0000000000..462223a347 --- /dev/null +++ b/test/unit/metagraphed.test.ts @@ -0,0 +1,100 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + assessSubnetClaimFindings, + interpretSubnetResponse, + METAGRAPHED_FETCH_TIMEOUT_MS, + validateNetuid, + type MetagraphedSubnetResponse, +} from "../../src/services/metagraphed"; + +/** Build a minimal fetch stub returning the given status + JSON body. */ +function fetchReturning(status: number, body: unknown): typeof fetch { + return vi.fn(async () => ({ status, ok: status >= 200 && status < 300, json: async () => body }) as unknown as Response) as unknown as typeof fetch; +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("interpretSubnetResponse", () => { + it("maps explicit non-existence to not_found", () => { + expect(interpretSubnetResponse(9, { exists: false }).status).toBe("not_found"); + }); + it("maps an unhealthy interface (any alias) to exists_unhealthy", () => { + expect(interpretSubnetResponse(9, { healthy: false }).status).toBe("exists_unhealthy"); + expect(interpretSubnetResponse(9, { interfaceHealthy: false }).status).toBe("exists_unhealthy"); + expect(interpretSubnetResponse(9, { interface: { healthy: false } }).status).toBe("exists_unhealthy"); + }); + it("treats present/healthy/unknown shapes as exists_healthy", () => { + expect(interpretSubnetResponse(9, { exists: true, healthy: true }).status).toBe("exists_healthy"); + expect(interpretSubnetResponse(9, { netuid: 9 }).status).toBe("exists_healthy"); + expect(interpretSubnetResponse(9, {} as MetagraphedSubnetResponse).status).toBe("exists_healthy"); + expect(interpretSubnetResponse(9, { interface: null }).status).toBe("exists_healthy"); + }); +}); + +describe("validateNetuid", () => { + it("maps HTTP 404 to not_found and strips a trailing slash from the base URL", async () => { + const fetchImpl = fetchReturning(404, {}); + const result = await validateNetuid(42, { baseUrl: "https://meta.example/", fetchImpl }); + expect(result.status).toBe("not_found"); + expect(fetchImpl).toHaveBeenCalledWith("https://meta.example/subnets/42", expect.objectContaining({ headers: expect.any(Object) })); + }); + + it("maps other non-2xx responses to unavailable", async () => { + const result = await validateNetuid(42, { baseUrl: "https://meta.example", fetchImpl: fetchReturning(503, {}) }); + expect(result.status).toBe("unavailable"); + }); + + it("interprets a 2xx body (not_found / unhealthy / healthy)", async () => { + expect((await validateNetuid(1, { baseUrl: "https://m", fetchImpl: fetchReturning(200, { exists: false }) })).status).toBe("not_found"); + expect((await validateNetuid(1, { baseUrl: "https://m", fetchImpl: fetchReturning(200, { healthy: false }) })).status).toBe("exists_unhealthy"); + expect((await validateNetuid(1, { baseUrl: "https://m", fetchImpl: fetchReturning(200, { healthy: true }) })).status).toBe("exists_healthy"); + }); + + it("never rejects — network/parse errors map to unavailable", async () => { + const fetchImpl = vi.fn(async () => { + throw new Error("ECONNREFUSED"); + }) as unknown as typeof fetch; + const result = await validateNetuid(42, { baseUrl: "https://meta.example", fetchImpl }); + expect(result.status).toBe("unavailable"); + expect(result.detail).toContain("42"); + }); + + it("uses the global fetch and default timeout when none are injected", async () => { + const globalFetch = fetchReturning(200, { healthy: true }); + vi.stubGlobal("fetch", globalFetch); + const result = await validateNetuid(74, { baseUrl: "https://meta.example", timeoutMs: 1234 }); + expect(result.status).toBe("exists_healthy"); + expect(globalFetch).toHaveBeenCalledTimes(1); + expect(METAGRAPHED_FETCH_TIMEOUT_MS).toBe(10_000); + }); +}); + +describe("assessSubnetClaimFindings", () => { + it("is dormant (no findings, no fetch) when METAGRAPHED_API_URL is unset or blank", async () => { + const fetchImpl = fetchReturning(404, {}); + expect(await assessSubnetClaimFindings({}, { title: "integrates subnet 42" }, { fetchImpl })).toEqual([]); + expect(await assessSubnetClaimFindings({ METAGRAPHED_API_URL: " " }, { title: "integrates subnet 42" }, { fetchImpl })).toEqual([]); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it("surfaces an advisory finding when a claimed subnet is not found (acceptance criterion)", async () => { + const findings = await assessSubnetClaimFindings( + { METAGRAPHED_API_URL: "https://meta.example" }, + { title: "feat: integrates subnet 999", body: "wires the new subnet" }, + { fetchImpl: fetchReturning(404, {}), timeoutMs: 2000 }, + ); + expect(findings).toHaveLength(1); + expect(findings[0]?.code).toBe("subnet_claim_not_found"); + expect(findings[0]?.detail).toContain("metagraphed"); + }); + + it("falls back to global fetch when no fetchImpl is provided", async () => { + const globalFetch = fetchReturning(200, { healthy: false }); + vi.stubGlobal("fetch", globalFetch); + const findings = await assessSubnetClaimFindings({ METAGRAPHED_API_URL: "https://meta.example" }, { body: "uses subnet 5" }); + expect(findings.map((f) => f.code)).toEqual(["subnet_claim_unhealthy"]); + expect(globalFetch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit/subnet-claim.test.ts b/test/unit/subnet-claim.test.ts new file mode 100644 index 0000000000..46971e5bcf --- /dev/null +++ b/test/unit/subnet-claim.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from "vitest"; +import { + assessSubnetClaims, + buildSubnetClaimFinding, + detectSubnetClaims, + MAX_RECOGNIZED_NETUID, + type NetuidValidation, +} from "../../src/signals/subnet-claim"; +import { isPublicSafeText } from "../../src/signals/redaction"; + +function assertPublicSafe(finding: { title: string; detail: string; action?: string; publicText?: string }) { + for (const text of [finding.title, finding.detail, finding.action ?? "", finding.publicText ?? ""]) { + expect(isPublicSafeText(text)).toBe(true); + } +} + +describe("detectSubnetClaims", () => { + it("returns nothing for empty/blank input", () => { + expect(detectSubnetClaims(null)).toEqual([]); + expect(detectSubnetClaims(undefined)).toEqual([]); + expect(detectSubnetClaims("")).toEqual([]); + expect(detectSubnetClaims("just a normal description with no claims")).toEqual([]); + }); + + it("detects the common subnet/netuid phrasings", () => { + expect(detectSubnetClaims("This integrates subnet 42 cleanly")).toEqual([{ netuid: 42, raw: "subnet 42" }]); + expect(detectSubnetClaims("targets netuid 5")[0]?.netuid).toBe(5); + expect(detectSubnetClaims("net uid 9")[0]?.netuid).toBe(9); + expect(detectSubnetClaims("netuid: 12")[0]?.netuid).toBe(12); + expect(detectSubnetClaims("netuid=7")[0]?.netuid).toBe(7); + expect(detectSubnetClaims("subnet #3")[0]?.netuid).toBe(3); + expect(detectSubnetClaims("subnet-8")[0]?.netuid).toBe(8); + expect(detectSubnetClaims("supports subnets 2")[0]?.netuid).toBe(2); + expect(detectSubnetClaims("built for SN74")[0]?.netuid).toBe(74); + expect(detectSubnetClaims("sn 11")[0]?.netuid).toBe(11); + expect(detectSubnetClaims("subnet number 7")[0]?.netuid).toBe(7); + }); + + it("deduplicates by netuid and sorts ascending", () => { + expect(detectSubnetClaims("subnet 42 and later netuid 42, plus subnet 5")).toEqual([ + { netuid: 5, raw: "subnet 5" }, + { netuid: 42, raw: "subnet 42" }, + ]); + }); + + it("ignores out-of-range numbers and non-claim words", () => { + expect(detectSubnetClaims("released in subnet 2024")).toEqual([]); // year-like, > MAX + expect(detectSubnetClaims(`subnet ${MAX_RECOGNIZED_NETUID + 1}`)).toEqual([]); + expect(detectSubnetClaims(`subnet ${MAX_RECOGNIZED_NETUID}`)).toEqual([{ netuid: MAX_RECOGNIZED_NETUID, raw: `subnet ${MAX_RECOGNIZED_NETUID}` }]); + expect(detectSubnetClaims("refactored the subnetwork module")).toEqual([]); + expect(detectSubnetClaims("took a snapshot at step 5")).toEqual([]); + expect(detectSubnetClaims("netuid zero")).toEqual([]); // no digit + }); + + it("accepts the root subnet (netuid 0)", () => { + expect(detectSubnetClaims("binds netuid 0")).toEqual([{ netuid: 0, raw: "netuid 0" }]); + }); +}); + +describe("buildSubnetClaimFinding", () => { + it("surfaces a public-safe warning for a non-existent subnet", () => { + const finding = buildSubnetClaimFinding({ netuid: 999, status: "not_found", detail: "metagraphed reports subnet 999 does not exist." }); + expect(finding).not.toBeNull(); + expect(finding!.code).toBe("subnet_claim_not_found"); + expect(finding!.severity).toBe("warning"); + expect(finding!.title).toContain("999"); + expect(finding!.detail).toContain("metagraphed"); + assertPublicSafe(finding!); + }); + + it("surfaces a public-safe warning for an unhealthy subnet", () => { + const finding = buildSubnetClaimFinding({ netuid: 12, status: "exists_unhealthy", detail: "metagraphed reports subnet 12 interface health did not pass." }); + expect(finding!.code).toBe("subnet_claim_unhealthy"); + expect(finding!.severity).toBe("warning"); + assertPublicSafe(finding!); + }); + + it("produces no finding for healthy or unavailable verdicts", () => { + expect(buildSubnetClaimFinding({ netuid: 7, status: "exists_healthy", detail: "ok" })).toBeNull(); + expect(buildSubnetClaimFinding({ netuid: 7, status: "unavailable", detail: "down" })).toBeNull(); + }); +}); + +describe("assessSubnetClaims", () => { + const validatorFor = (statuses: Record) => + vi.fn(async (netuid: number): Promise => ({ netuid, status: statuses[netuid] ?? "exists_healthy", detail: `verdict for ${netuid}` })); + + it("returns no findings when there are no claims (and never calls the validator)", async () => { + const validate = validatorFor({}); + expect(await assessSubnetClaims({ title: "plain title", body: "plain body" }, validate)).toEqual([]); + expect(validate).not.toHaveBeenCalled(); + }); + + it("validates each distinct claimed netuid across title + body and collects only actionable findings", async () => { + const validate = validatorFor({ 42: "not_found", 7: "exists_healthy", 9: "exists_unhealthy" }); + const findings = await assessSubnetClaims({ title: "integrates subnet 42", body: "also subnet 7 and subnet 9" }, validate); + expect(validate).toHaveBeenCalledTimes(3); // 7, 9, 42 distinct + expect(findings.map((f) => f.code)).toEqual(["subnet_claim_unhealthy", "subnet_claim_not_found"]); // netuid 7 healthy → omitted, sorted 9 then 42 + }); + + it("tolerates a missing title (body only)", async () => { + const validate = validatorFor({ 5: "not_found" }); + const findings = await assessSubnetClaims({ body: "needs subnet 5" }, validate); + expect(findings).toHaveLength(1); + expect(findings[0]?.code).toBe("subnet_claim_not_found"); + }); + + it("tolerates a missing body (title only)", async () => { + const validate = validatorFor({ 8: "not_found" }); + const findings = await assessSubnetClaims({ title: "integrates subnet 8" }, validate); + expect(findings).toHaveLength(1); + expect(findings[0]?.code).toBe("subnet_claim_not_found"); + }); +});