fix(staging): allow model catalog bootstrap
Staging Backend Quality Gate / validate (push) Successful in 12m33s
Staging Backend Quality Gate / publish (push) Successful in 19m38s

This commit is contained in:
Jesse_Chen
2026-08-07 21:21:32 +08:00
parent 508f3db9c6
commit d1bea7611e
3 changed files with 26 additions and 5 deletions
+15
View File
@@ -2474,3 +2474,18 @@
- 相关记录:BUG-127、BUG-143
- 复发自:无(独立根因,非 BUG-127 / BUG-143 复发)
- 修复版本:待 follow-up commit / gate
## BUG-145 | staging Web health 被空模型目录错误阻断
- 状态:resolvedlocal 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
+7 -5
View File
@@ -27,7 +27,7 @@ async function jyotishApiCheck(): Promise<Check> {
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<Check> {
async function modelCatalogCheck(): Promise<Check> {
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<string, Check>) {
@@ -94,6 +96,6 @@ export async function GET() {
truthSource: truthSourceIdentity,
checks,
},
{ status: status === "ok" ? 200 : 503 },
{ status: status === "blocked" ? 503 : 200 },
);
}
+4
View File
@@ -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/);
});