From adab0240abe0be123230c0544466ac151d498580 Mon Sep 17 00:00:00 2001 From: nghequyettien Date: Mon, 27 Jul 2026 23:38:16 +0700 Subject: [PATCH] feat(openapi): document selftune override audit and clear routes GET /v1/repos/{owner}/{repo}/selftune/overrides/audit and DELETE /v1/repos/{owner}/{repo}/selftune/overrides were live and MCP-mirrored but missing from the OpenAPI spec. Add SelftuneOverrideAuditResponseSchema and ClearSelftuneOverrideResponseSchema mirroring the MCP tool output shapes, register both routes following the gate-config/effective pattern, and regenerate apps/loopover-ui/public/openapi.json. Closes #9303 --- apps/loopover-ui/public/openapi.json | 159 +++++++++++++++++++++++++++ src/openapi/schemas.ts | 17 +++ src/openapi/spec.ts | 49 +++++++++ test/unit/openapi.test.ts | 27 +++++ 4 files changed, 252 insertions(+) diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index eb25b146c2..c6778234a9 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -14856,6 +14856,39 @@ "recommendation", "summary" ] + }, + "SelftuneOverrideAuditResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "audit": { + "type": "array", + "items": { + "nullable": true + } + } + }, + "required": [ + "repoFullName", + "audit" + ] + }, + "ClearSelftuneOverrideResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "cleared": { + "type": "boolean" + } + }, + "required": [ + "repoFullName", + "cleared" + ] } }, "parameters": {}, @@ -19370,6 +19403,132 @@ } ] } + }, + "/v1/repos/{owner}/{repo}/selftune/overrides/audit": { + "get": { + "summary": "Self-tune gate override audit trail for a repo (#9303)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "50" + }, + "required": false, + "description": "Optional row cap for the returned audit history (positive integer).", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Newest-first override_audit rows recording why the self-tune loop promoted, shadowed, or cleared a live gate override", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelftuneOverrideAuditResponse" + } + } + } + }, + "403": { + "description": "Insufficient role" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/repos/{owner}/{repo}/selftune/overrides": { + "delete": { + "summary": "Clear the live self-tune gate override for a repo (#9303)", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "confirm": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "confirm" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Live override cleared; the automatic self-tune promote path is untouched", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearSelftuneOverrideResponse" + } + } + } + }, + "400": { + "description": "Malformed confirmation body" + }, + "403": { + "description": "Insufficient role" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } } }, "servers": [ diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 79085ab1e2..e6182cc02d 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1926,6 +1926,23 @@ export const GateConfigEffectiveResponseSchema = z }) .openapi("GateConfigEffectiveResponse"); +/** #9303: mirrors selftuneOverrideAuditOutputSchema (src/mcp/server.ts) — audit rows stay z.unknown(), + * listOverrideAudit is the single source of truth for the event fields. */ +export const SelftuneOverrideAuditResponseSchema = z + .object({ + repoFullName: z.string(), + audit: z.array(z.unknown()), + }) + .openapi("SelftuneOverrideAuditResponse"); + +/** #9303: mirrors clearSelftuneOverrideOutputSchema (src/mcp/server.ts) — the write-side confirmation shape. */ +export const ClearSelftuneOverrideResponseSchema = z + .object({ + repoFullName: z.string(), + cleared: z.boolean(), + }) + .openapi("ClearSelftuneOverrideResponse"); + export const BurdenForecastSchema = z .object({ repoFullName: z.string(), diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index b8bfea05be..678c46ab99 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -81,6 +81,8 @@ import { RewardRiskActionSchema, ScorePreviewSchema, ScoringModelSnapshotSchema, + SelftuneOverrideAuditResponseSchema, + ClearSelftuneOverrideResponseSchema, SignalFidelitySchema, SkippedPrAuditExportSchema, SyncStatusSchema, @@ -164,6 +166,8 @@ export function buildOpenApiSpec() { registry.register("IssueQualityResponse", IssueQualityResponseSchema); registry.register("GateConfigEffectiveResponse", GateConfigEffectiveResponseSchema); registry.register("LiveGateThresholdsResponse", LiveGateThresholdsResponseSchema); + registry.register("SelftuneOverrideAuditResponse", SelftuneOverrideAuditResponseSchema); + registry.register("ClearSelftuneOverrideResponse", ClearSelftuneOverrideResponseSchema); registry.register("BurdenForecast", BurdenForecastSchema); registry.register("ContributorScoringProfile", ContributorScoringProfileSchema); registry.register("ContributorStrategy", ContributorStrategySchema); @@ -480,6 +484,51 @@ export function buildOpenApiSpec() { 404: { description: "No live or shadow gate override is active for this repo" }, }, }); + registry.registerPath({ + method: "get", + path: "/v1/repos/{owner}/{repo}/selftune/overrides/audit", + summary: "Self-tune gate override audit trail for a repo (#9303)", + request: { + params: z.object({ owner: z.string(), repo: z.string() }), + query: z.object({ + limit: z.string().optional().openapi({ + param: { description: "Optional row cap for the returned audit history (positive integer)." }, + example: "50", + }), + }), + }, + responses: { + 200: { + description: "Newest-first override_audit rows recording why the self-tune loop promoted, shadowed, or cleared a live gate override", + content: { "application/json": { schema: SelftuneOverrideAuditResponseSchema } }, + }, + 403: { description: "Insufficient role" }, + }, + }); + registry.registerPath({ + method: "delete", + path: "/v1/repos/{owner}/{repo}/selftune/overrides", + summary: "Clear the live self-tune gate override for a repo (#9303)", + request: { + params: z.object({ owner: z.string(), repo: z.string() }), + body: { + required: false, + content: { + "application/json": { + schema: z.object({ confirm: z.literal(true) }), + }, + }, + }, + }, + responses: { + 200: { + description: "Live override cleared; the automatic self-tune promote path is untouched", + content: { "application/json": { schema: ClearSelftuneOverrideResponseSchema } }, + }, + 400: { description: "Malformed confirmation body" }, + 403: { description: "Insufficient role" }, + }, + }); registry.registerPath({ method: "get", path: "/v1/repos/{owner}/{repo}/outcome-patterns", diff --git a/test/unit/openapi.test.ts b/test/unit/openapi.test.ts index f4afc434cf..3b6681c081 100644 --- a/test/unit/openapi.test.ts +++ b/test/unit/openapi.test.ts @@ -17,6 +17,8 @@ describe("OpenAPI contract", () => { expect(spec.paths["/v1/repos/{owner}/{repo}/issue-quality"]).toBeDefined(); expect(spec.paths["/v1/repos/{owner}/{repo}/outcome-patterns"]).toBeDefined(); expect(spec.paths["/v1/repos/{owner}/{repo}/gate-config/effective"]).toBeDefined(); + expect(spec.paths["/v1/repos/{owner}/{repo}/selftune/overrides/audit"]).toBeDefined(); + expect(spec.paths["/v1/repos/{owner}/{repo}/selftune/overrides"]).toBeDefined(); expect(spec.paths["/v1/repos/{owner}/{repo}/registration-readiness"]).toBeDefined(); expect(spec.paths["/v1/repos/{owner}/{repo}/gittensor-config-recommendation"]).toBeDefined(); expect(spec.paths["/v1/repos/{owner}/{repo}/pulls/{number}/maintainer-packet"]).toBeDefined(); @@ -135,6 +137,31 @@ describe("OpenAPI contract", () => { expect(spec.paths["/v1/auth/github/token"]?.post?.security).toEqual([{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }]); }); + // #9303: selftune-override routes were live but undocumented; assert both paths, their HTTP methods, and the + // response schema keys matching each route's MCP tool output shape (selftuneOverrideAuditOutputSchema / + // clearSelftuneOverrideOutputSchema in src/mcp/server.ts). + it("documents the selftune-override audit and clear routes with matching MCP output shapes", () => { + const spec = buildOpenApiSpec(); + + const auditPath = spec.paths["/v1/repos/{owner}/{repo}/selftune/overrides/audit"]; + expect(auditPath?.get).toBeDefined(); + expect(auditPath?.get?.responses?.["200"]?.content?.["application/json"]?.schema).toEqual({ + $ref: "#/components/schemas/SelftuneOverrideAuditResponse", + }); + + const clearPath = spec.paths["/v1/repos/{owner}/{repo}/selftune/overrides"]; + expect(clearPath?.delete).toBeDefined(); + expect(clearPath?.delete?.responses?.["200"]?.content?.["application/json"]?.schema).toEqual({ + $ref: "#/components/schemas/ClearSelftuneOverrideResponse", + }); + + const auditSchema = spec.components?.schemas?.SelftuneOverrideAuditResponse as { properties?: Record } | undefined; + expect(Object.keys(auditSchema?.properties ?? {}).sort()).toEqual(["audit", "repoFullName"]); + + const clearSchema = spec.components?.schemas?.ClearSelftuneOverrideResponse as { properties?: Record } | undefined; + expect(Object.keys(clearSchema?.properties ?? {}).sort()).toEqual(["cleared", "repoFullName"]); + }); + // #5810: every operation needs a title in the generated spec and the rendered API browser. Iterating the built // document (rather than counting `summary:` lines in the source) also covers the paths registered from a loop, // and fails loudly when a future route is added without one.