From d1bea7611ed37f75adfeb0b70d87a14411fc6708 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 7 Aug 2026 21:21:32 +0800 Subject: [PATCH] fix(staging): allow model catalog bootstrap --- docs/BUG_HISTORY.md | 15 +++++++++++++++ frontend/src/app/api/health/route.ts | 12 +++++++----- frontend/tests/health-deployment.test.ts | 4 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5b29925e..376ede77 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2474,3 +2474,18 @@ - 相关记录:BUG-127、BUG-143 - 复发自:无(独立根因,非 BUG-127 / BUG-143 复发) - 修复版本:待 follow-up commit / gate + +## BUG-145 | staging Web health 被空模型目录错误阻断 + +- 状态:resolved(local candidate,远端 gate 待 follow-up 更新) +- 首次发现:2026-08-07 +- 最近更新:2026-08-07 +- 影响面:`frontend/src/app/api/health/route.ts` 的 Web 容器 healthcheck;数据库尚未发布默认模型时 staging Web 被错误判定为 unhealthy,真实基础设施故障的 503 语义不变。 +- 用户现象:staging 数据库没有 published default model 时,model catalog 只有 `default_model_unavailable` 或 `database_model_catalog_empty`,但 `/api/health` 返回 HTTP 503,导致 Web bootstrap/Compose healthcheck 互相等待,页面无法进入稳定 healthy 状态。 +- 根因:health route 将所有非 `ok` 聚合状态统一映射为 503;同时把可恢复的模型目录缺少默认模型状态标成 `blocked`,混淆了业务配置未就绪与模型目录数据库基础设施不可用。上一轮放宽为 `degraded -> 200` 后,又会把 Jyotish API 非 2xx 误报为 healthy。 +- 修复:模型目录缺少 published default model 时报告 `degraded`;`database_model_catalog_unavailable` 与 Jyotish API 非 2xx 保持 `blocked`。整体 health 仅在存在 `blocked` 检查时返回 503,模型目录缺省配置仍返回 200;未恢复 legacy model env vars,也未改变 consult/report/onboarding 的 fail-closed 模型解析路径。 +- 验证:`npx tsx --test tests/health-deployment.test.ts`(12 passed);`npx eslint src/app/api/health/route.ts tests/health-deployment.test.ts`(通过);`git diff --check`(通过)。 +- 防复发:health contract 必须同时覆盖“无 published default model -> `modelCatalog=degraded`、HTTP 200”、“Jyotish API 非 2xx -> `blocked`”和“任意 `blocked` -> HTTP 503”,不得让真实基础设施故障落入 `degraded`。 +- 相关记录:BUG-145 +- 复发自:无(staging bootstrap deadlock 的独立 health contract 根因) +- 修复版本:待 follow-up commit / gate diff --git a/frontend/src/app/api/health/route.ts b/frontend/src/app/api/health/route.ts index 209a7c44..e14c326c 100644 --- a/frontend/src/app/api/health/route.ts +++ b/frontend/src/app/api/health/route.ts @@ -27,7 +27,7 @@ async function jyotishApiCheck(): Promise { signal: controller.signal, }); return { - status: response.ok ? "ok" : "degraded", + status: response.ok ? "ok" : "blocked", message: response.ok ? undefined : `http:${response.status}`, latencyMs: Date.now() - started, }; @@ -45,9 +45,11 @@ async function jyotishApiCheck(): Promise { async function modelCatalogCheck(): Promise { const catalog = await loadLanguageModelCatalog(); const defaults = catalog.models.filter((model) => model.isDefault && model.id === catalog.defaultModelId); - return defaults.length === 1 - ? { status: "ok" } - : { status: "blocked", message: catalog.issues[0] ?? "default_model_unavailable" }; + if (defaults.length === 1) return { status: "ok" }; + const message = catalog.issues[0] ?? "default_model_unavailable"; + return message === "database_model_catalog_unavailable" + ? { status: "blocked", message } + : { status: "degraded", message }; } function aggregate(checks: Record) { @@ -94,6 +96,6 @@ export async function GET() { truthSource: truthSourceIdentity, checks, }, - { status: status === "ok" ? 200 : 503 }, + { status: status === "blocked" ? 503 : 200 }, ); } diff --git a/frontend/tests/health-deployment.test.ts b/frontend/tests/health-deployment.test.ts index 253cd53e..2ca95f9d 100644 --- a/frontend/tests/health-deployment.test.ts +++ b/frontend/tests/health-deployment.test.ts @@ -66,6 +66,10 @@ test("health endpoint exposes deployment identity for production verification", assert.match(source, /const defaults = catalog\.models\.filter/); assert.match(source, /defaults\.length === 1/); assert.match(source, /default_model_unavailable/); + assert.match(source, /message === "database_model_catalog_unavailable"/); + assert.match(source, /: \{ status: "degraded", message \}/); + assert.match(source, /status: response\.ok \? "ok" : "blocked"/); + assert.match(source, /status === "blocked" \? 503 : 200/); assert.doesNotMatch(source, /anyEnvCheck\(\["LLM_MODELS_JSON"|OPENAI_API_KEY|DEEPSEEK_API_KEY|LLM_API_KEY/); });