Move DB health to management API (#1757)

* Move DB health to management API

* Address DB health review feedback
This commit is contained in:
Randi
2026-04-29 07:53:31 -04:00
committed by GitHub
parent e12df00c40
commit d39e5fb48f
9 changed files with 76 additions and 55 deletions

View File

@@ -7,15 +7,30 @@ import { MCP_TOOL_MAP, dbHealthCheckInput } from "../schemas/tools.ts";
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
const mockRunManagedDbHealthCheck = vi.hoisted(() => vi.fn());
vi.mock("../audit.ts", () => ({
logToolCall: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../../src/lib/db/core.ts", () => ({
runManagedDbHealthCheck: mockRunManagedDbHealthCheck,
}));
describe("omniroute_db_health_check MCP tool", () => {
let client: Client;
beforeEach(async () => {
mockFetch.mockReset();
mockRunManagedDbHealthCheck.mockReset();
mockRunManagedDbHealthCheck.mockReturnValue({
isHealthy: false,
issues: [{ type: "broken_reference", table: "combos", description: "broken", count: 1 }],
repairedCount: 1,
backupCreated: true,
autoRepair: true,
checkedAt: new Date().toISOString(),
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const server = createMcpServer();
await server.connect(serverTransport);
@@ -38,32 +53,19 @@ describe("omniroute_db_health_check MCP tool", () => {
expect(dbHealthCheckInput.safeParse({ autoRepair: "yes" }).success).toBe(false);
});
it("dispatches to /api/v1/db/health using POST when autoRepair=true", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
isHealthy: false,
issues: [{ type: "broken_reference", table: "combos", description: "broken", count: 1 }],
repairedCount: 1,
backupCreated: true,
autoRepair: true,
checkedAt: new Date().toISOString(),
}),
});
it("runs the database repair flow directly when autoRepair=true", async () => {
const result = await client.callTool({
name: "omniroute_db_health_check",
arguments: { autoRepair: true },
});
expect(result.isError).toBeFalsy();
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/db/health"),
expect.objectContaining({ method: "POST" })
);
expect(mockFetch).not.toHaveBeenCalled();
expect(mockRunManagedDbHealthCheck).toHaveBeenCalledWith({ autoRepair: true });
const content = result.content[0] as { type: string; text: string };
const payload = JSON.parse(content.text);
expect(payload.autoRepair).toBe(true);
expect(payload.repairedCount).toBe(1);
expect(payload.backupCreated).toBe(true);
});

View File

@@ -882,7 +882,7 @@ export const dbHealthCheckTool: McpToolDefinition<
scopes: ["read:health", "write:resilience"],
auditLevel: "full",
phase: 2,
sourceEndpoints: ["/api/v1/db/health"],
sourceEndpoints: ["/api/db/health"],
};
// --- Tool 19: omniroute_sync_pricing ---

View File

@@ -893,11 +893,8 @@ export async function handleDbHealthCheck(args: { autoRepair?: boolean }) {
const autoRepair = args.autoRepair === true;
try {
const result = toRecord(
await apiFetch("/api/v1/db/health", {
method: autoRepair ? "POST" : "GET",
})
);
const { runManagedDbHealthCheck } = await import("../../../src/lib/db/core.ts");
const result = runManagedDbHealthCheck({ autoRepair });
await logToolCall(
"omniroute_db_health_check",

View File

@@ -81,7 +81,7 @@ export default function HealthPage() {
const fetchDbHealth = useCallback(async () => {
try {
const res = await fetch("/api/v1/db/health");
const res = await fetch("/api/db/health");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setDbHealth(json);
@@ -138,7 +138,7 @@ export default function HealthPage() {
const handleRepairDb = async () => {
setRepairingDb(true);
try {
const res = await fetch("/api/v1/db/health", { method: "POST" });
const res = await fetch("/api/db/health", { method: "POST" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setDbHealth(json);

View File

@@ -167,7 +167,7 @@ async function mockHealthPageApis(page: Page) {
});
});
await page.route("**/api/v1/db/health", async (route) => {
await page.route("**/api/db/health", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",

View File

@@ -153,6 +153,7 @@ const cases: Case[] = [
expectedClass: "MANAGEMENT",
},
{ name: "/api/keys MANAGEMENT", path: "/api/keys", expectedClass: "MANAGEMENT" },
{ name: "/api/db/health MANAGEMENT", path: "/api/db/health", expectedClass: "MANAGEMENT" },
{ name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" },
{ name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" },

View File

@@ -174,6 +174,20 @@ test("runAuthzPipeline allows dashboard sessions to read model catalog aliases",
assert.equal(response.headers.get("x-omniroute-route-class"), "CLIENT_API");
});
test("runAuthzPipeline allows dashboard sessions to reach DB health management API", async () => {
await forceAuthRequired();
const response = await pipeline.runAuthzPipeline(
request("http://localhost/api/db/health", {
headers: { cookie: await dashboardCookie() },
}),
{ enforce: true }
);
assert.equal(response.status, 200);
assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT");
});
test("runAuthzPipeline refreshes dashboard JWTs near expiry", async () => {
await forceAuthRequired();
const secret = new TextEncoder().encode(process.env.JWT_SECRET);

View File

@@ -3,29 +3,44 @@ import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { SignJWT } from "jose";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-health-route-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "task-303-api-key-secret";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const routeModule = await import("../../src/app/api/v1/db/health/route.ts");
const routeModule = await import("../../src/app/api/db/health/route.ts");
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const TEST_JWT_SECRET = "db-health-route-jwt-secret";
const TEST_INITIAL_PASSWORD = "db-health-route-password";
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
process.env.JWT_SECRET = TEST_JWT_SECRET;
process.env.INITIAL_PASSWORD = TEST_INITIAL_PASSWORD;
}
function makeRequest(method, token) {
return new Request("http://localhost/api/v1/db/health", {
function makeRequest(method, cookie) {
return new Request("http://localhost/api/db/health", {
method,
headers: token ? { Authorization: `Bearer ${token}` } : {},
headers: cookie ? { cookie } : {},
});
}
async function dashboardCookie() {
const secret = new TextEncoder().encode(TEST_JWT_SECRET);
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
return `auth_token=${token}`;
}
function insertBrokenRows(db) {
db.prepare(
`INSERT INTO quota_snapshots
@@ -43,35 +58,27 @@ test.beforeEach(async () => {
test.after(async () => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
});
test("GET /api/v1/db/health requires authentication", async () => {
const previousInitialPassword = process.env.INITIAL_PASSWORD;
process.env.INITIAL_PASSWORD = "route-health-auth";
test("GET /api/db/health requires authentication", async () => {
const response = await routeModule.GET(makeRequest("GET"));
const body = (await response.json()) as any;
try {
const response = await routeModule.GET(makeRequest("GET"));
const body = (await response.json()) as any;
assert.equal(response.status, 401);
assert.equal(body.error.message, "Authentication required");
} finally {
if (previousInitialPassword === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = previousInitialPassword;
}
}
assert.equal(response.status, 401);
assert.equal(body.error.message, "Authentication required");
});
test("GET /api/v1/db/health diagnoses without mutating database rows", async () => {
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
test("GET /api/db/health diagnoses without mutating database rows", async () => {
const cookie = await dashboardCookie();
const db = core.getDbInstance();
insertBrokenRows(db);
const response = await routeModule.GET(makeRequest("GET", authKey.key));
const response = await routeModule.GET(makeRequest("GET", cookie));
const body = (await response.json()) as any;
assert.equal(response.status, 200);
@@ -81,12 +88,12 @@ test("GET /api/v1/db/health diagnoses without mutating database rows", async ()
assert.equal((db.prepare("SELECT COUNT(*) AS count FROM domain_budgets").get() as any).count, 1);
});
test("POST /api/v1/db/health repairs broken rows for authenticated callers", async () => {
const authKey = await apiKeysDb.createApiKey("Health Route", "machine-route-health");
test("POST /api/db/health repairs broken rows for authenticated callers", async () => {
const cookie = await dashboardCookie();
const db = core.getDbInstance();
insertBrokenRows(db);
const response = await routeModule.POST(makeRequest("POST", authKey.key));
const response = await routeModule.POST(makeRequest("POST", cookie));
const body = (await response.json()) as any;
assert.equal(response.status, 200);