Skip to content

Commit e566613

Browse files
authored
feat(api): GraphQL subnet_health field mirroring GET /api/v1/subnets/{netuid}/health (#7640) (#7653)
Adds the base per-subnet live operational-health card to GraphQL, completing the health-field family whose windowed views (subnet_health_trends / subnet_health_incidents / subnet_health_percentiles) already exist. The resolver reuses the exact live composition the REST subnet-health route and the get_subnet_health MCP tool share (resolveLiveHealth + overlaySubnetHealth + loadSubnetReliability) -- no health logic is re-derived. A cold health store resolves to the same schema-stable unknown card the MCP tool returns; a negative netuid is a BAD_USER_INPUT error. Returned as opaque JSON (the existing typed SubnetHealth is the flat health-list item, a different shape). Closes #7640
1 parent caee7bc commit e566613

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/graphql.mjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,10 @@ import {
173173
mergeFreshness,
174174
mergeRpcEndpoints,
175175
overlayOverviewHealth,
176+
loadSubnetReliability,
176177
overlayCatalogDetail,
177178
overlayCatalogIndex,
179+
overlaySubnetHealth,
178180
resolveLiveEconomics,
179181
resolveLiveHealth,
180182
subnetBadgeStatus,
@@ -499,6 +501,8 @@ export const SDL = `
499501
subnet_health_incidents(netuid: Int!, window: String): SubnetHealthIncidents!
500502
"One subnet's per-surface latency percentiles (p50/p90/p95/p99) over a 7d/30d window (default 7d), computed live from the success-only health-probe history. The latency-distribution companion of subnet_health_incidents' availability view. A subnet with no probe history resolves to a schema-stable empty surfaces list, never null. Mirrors GET /api/v1/subnets/{netuid}/health/percentiles."
501503
subnet_health_percentiles(netuid: Int!, window: String): SubnetHealthPercentiles!
504+
"One subnet's current live operational-health card: the per-surface status/latency/last-ok rows from the latest ~15-minute cron probe (summarized into ok/degraded/failed/unknown counts) plus the cross-window reliability score. The at-a-glance base card completing the health family whose windowed views are subnet_health_trends/subnet_health_incidents/subnet_health_percentiles. A subnet with no live health data resolves to the same schema-stable unknown card (summary.status of unknown, empty surfaces), never null. Opaque JSON passed through verbatim, matching the get_subnet_health MCP/REST shape (the existing typed SubnetHealth is the flat health-list item, a different shape, so this base card is JSON like the sibling surfaces payloads). Mirrors GET /api/v1/subnets/{netuid}/health."
505+
subnet_health(netuid: Int!): JSON
502506
"One subnet's rolling 24h alpha trading volume from the StakeAdded/StakeRemoved trade stream: buy/sell volume in alpha and TAO, trade counts, net flow, a buy-vs-sell sentiment ratio, and volume-to-market-cap ratio. A subnet with no trades resolves to a schema-stable zeroed card, never null. Mirrors GET /api/v1/subnets/{netuid}/volume."
503507
subnet_volume(netuid: Int!): SubnetVolume!
504508
"The machine-readable AI-resources index: the copyable agent prompt (/agent.md), MCP server install metadata and tool listing, the Bittensor skill, llms.txt, OpenAPI, and links to the agent-facing APIs. Use it to bootstrap an agent integration before calling the catalog/search fields. Null when the index has not been baked in this environment (rather than a GraphQL error). Opaque JSON passed through verbatim, matching the get_agent_resources MCP/REST shape. Mirrors GET /api/v1/agent-resources."
@@ -4252,6 +4256,7 @@ export const FIELD_COMPLEXITY = {
42524256
subnet_uptime: RELATIONSHIP_FIELD_COMPLEXITY,
42534257
subnet_health_incidents: RELATIONSHIP_FIELD_COMPLEXITY,
42544258
subnet_health_percentiles: RELATIONSHIP_FIELD_COMPLEXITY,
4259+
subnet_health: RELATIONSHIP_FIELD_COMPLEXITY,
42554260
agent_resources: RELATIONSHIP_FIELD_COMPLEXITY,
42564261
curation: RELATIONSHIP_FIELD_COMPLEXITY,
42574262
candidates: RELATIONSHIP_FIELD_COMPLEXITY,
@@ -9547,6 +9552,41 @@ const rootValue = {
95479552
};
95489553
},
95499554

9555+
async subnet_health({ netuid }, context) {
9556+
// Same non-negative netuid gate the other per-subnet resolvers use --
9557+
// GraphQL Int coercion rejects non-integers at parse time; a negative
9558+
// netuid is a BAD_USER_INPUT error, not a silent card.
9559+
if (!Number.isInteger(netuid) || netuid < 0) {
9560+
throw new GraphQLError("netuid must be a non-negative integer.", {
9561+
extensions: { code: "BAD_USER_INPUT" },
9562+
});
9563+
}
9564+
// Same live composition REST's subnet-health route (workers/api.mjs's
9565+
// subnet-health overlay) and the get_subnet_health MCP tool share: the
9566+
// latest ~15-minute cron snapshot (resolveLiveHealth) overlaid per subnet
9567+
// (overlaySubnetHealth), plus the cross-window reliability summary
9568+
// (loadSubnetReliability). A subnet with no live rows overlays to null, so
9569+
// it resolves to the identical schema-stable "unknown" card the MCP tool
9570+
// returns on a cold store -- never a GraphQL error. Nothing is re-derived.
9571+
const [live, reliability] = await Promise.all([
9572+
resolveLiveHealth({ readHealthKv, env: context.env }),
9573+
loadSubnetReliability(),
9574+
]);
9575+
const overlaid = overlaySubnetHealth(null, live, netuid);
9576+
if (overlaid) {
9577+
return { ...overlaid, reliability };
9578+
}
9579+
return {
9580+
schema_version: 1,
9581+
netuid,
9582+
summary: { status: "unknown", surface_count: 0 },
9583+
operational_observed_at: null,
9584+
health_source: "unavailable",
9585+
reliability,
9586+
surfaces: [],
9587+
};
9588+
},
9589+
95509590
async subnet_uptime({ netuid, window, min_samples: minSamples }, context) {
95519591
// Same 90d/1y window validation handleUptime / get_subnet_uptime use -- an
95529592
// unsupported window is a GraphQL BAD_USER_INPUT error, not a silent card.

tests/graphql.test.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14791,6 +14791,76 @@ describe("graphql — subnet_health_trends (#5883, Postgres-tier + D1-live fallb
1479114791
});
1479214792
});
1479314793

14794+
describe("graphql — subnet_health (#7640, live-cron overlay parity with REST + MCP)", () => {
14795+
const NETUID = 7;
14796+
14797+
test("subnet_health overlays the live per-surface card for the netuid", async () => {
14798+
const env = fixtureEnv(
14799+
{},
14800+
{
14801+
kv: {
14802+
[KV_HEALTH_CURRENT]: {
14803+
surfaces: [
14804+
{
14805+
surface_id: "7:subnet-api:x",
14806+
netuid: NETUID,
14807+
kind: "subnet-api",
14808+
provider: "x",
14809+
url: "https://x.example/api",
14810+
status: "ok",
14811+
classification: "live",
14812+
latency_ms: 120,
14813+
last_ok: "2026-06-13T00:00:00.000Z",
14814+
last_checked: "2026-06-13T00:05:00.000Z",
14815+
},
14816+
],
14817+
},
14818+
},
14819+
},
14820+
);
14821+
const { status, body } = await gql(
14822+
`{ subnet_health(netuid: ${NETUID}) }`,
14823+
env,
14824+
);
14825+
assert.equal(status, 200);
14826+
const card = body.data.subnet_health;
14827+
assert.equal(card.netuid, NETUID);
14828+
assert.equal(card.surfaces.length, 1);
14829+
assert.equal(card.surfaces[0].status, "ok");
14830+
assert.equal(card.summary.surface_count, 1);
14831+
assert.equal(card.summary.status, "ok");
14832+
// loadSubnetReliability() returns null since D1 retirement -- the card still
14833+
// resolves, matching the get_subnet_health MCP tool's null-reliability path.
14834+
assert.equal(card.reliability, null);
14835+
});
14836+
14837+
test("subnet_health resolves the schema-stable unknown card when the live store is cold", async () => {
14838+
const { status, body } = await gql(
14839+
`{ subnet_health(netuid: ${NETUID}) }`,
14840+
emptyEnv,
14841+
);
14842+
assert.equal(status, 200);
14843+
assert.deepEqual(body.data.subnet_health, {
14844+
schema_version: 1,
14845+
netuid: NETUID,
14846+
summary: { status: "unknown", surface_count: 0 },
14847+
operational_observed_at: null,
14848+
health_source: "unavailable",
14849+
reliability: null,
14850+
surfaces: [],
14851+
});
14852+
});
14853+
14854+
test("subnet_health rejects a negative netuid with BAD_USER_INPUT", async () => {
14855+
const { body } = await gql("{ subnet_health(netuid: -1) }", emptyEnv);
14856+
assert.equal(body.errors[0].extensions.code, "BAD_USER_INPUT");
14857+
});
14858+
14859+
test("subnet_health is weighted as a fan-out field", () => {
14860+
assert.equal(FIELD_COMPLEXITY.subnet_health, 5);
14861+
});
14862+
});
14863+
1479414864
describe("graphql — subnet_uptime (#5885, Postgres-tier + D1-live fallback)", () => {
1479514865
const NETUID = 7;
1479614866

0 commit comments

Comments
 (0)