fix(api): expose kb_ready and gate E2E/nightly on shared KB

Health reports kb_ready; PI E2E uses layered Load + require-online;
nightly preflights SELECT 1 before product suite.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 02:03:07 +00:00
co-authored by Cursor
parent f3ed57c41f
commit e782400736
6 changed files with 66 additions and 30 deletions
+2
View File
@@ -21,6 +21,8 @@ SYSTEM_DB_NAME=agent_system_dev
SYSTEM_DB_PUBLISH_PORT=5433
# 4. 共享知识库(容器 app 经 shuimu-kb-shared 访问)
# KB_DB_PASSWORD 必须在仓库根 .env 提供;config.Load("") 会分层合并根 .env + 本文件。
# 勿依赖代码内默认口令(已清空);宿主机 go test / 本地进程同样走分层 Load。
KB_DB_USER=regkb
KB_DB_NAME=regulations
KB_DB_HOST=shuimu-kb-shared
+2
View File
@@ -22,10 +22,12 @@ SYSTEM_DB_NAME=agent_system_main
SYSTEM_DB_PUBLISH_PORT=5435
# 4. 共享知识库连接 (只读检索共享数据)
# KB_DB_PASSWORD 必须在仓库根 .env;Load("") 分层合并;生产禁止弱口令/空口令。
KB_DB_USER=regkb
KB_DB_NAME=regulations
KB_DB_HOST=shuimu-kb-shared
KB_DB_PORT=5432
KB_DB_PASSWORD=
# 5. Cursor / LLM Agent Runner 服务
CURSOR_RUNNER_PORT=18767
+1
View File
@@ -20,6 +20,7 @@ SYSTEM_DB_NAME=agent_system_test
SYSTEM_DB_PUBLISH_PORT=5434
# 4. 共享知识库连接 (只读检索共享数据)
# KB_DB_PASSWORD 必须在仓库根 .env;config.Load("") 分层合并根 .env + 本文件。勿依赖代码默认口令。
KB_DB_USER=regkb
KB_DB_NAME=regulations
KB_DB_HOST=shuimu-kb-shared
+4
View File
@@ -30,6 +30,7 @@ type HealthResponse struct {
AgentBackend string `json:"agent_backend"`
Version string `json:"version"`
SharedKBPort int `json:"shared_kb_port"`
KBReady bool `json:"kb_ready"`
DatabaseReady bool `json:"database_ready"`
SchemaVersion int64 `json:"schema_version,omitempty"`
SchemaReady bool `json:"schema_ready,omitempty"`
@@ -117,12 +118,15 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
}
}
kbReady := s.kb != nil && s.kb.Online()
return HealthResponse{
Status: status,
Env: s.cfg.Env,
AgentBackend: s.cfg.AgentBackend,
Version: "v1.0",
SharedKBPort: s.cfg.KbDbPort,
KBReady: kbReady,
DatabaseReady: dbReady,
SchemaVersion: schemaVersion,
SchemaReady: schemaReady,
+19 -30
View File
@@ -9,7 +9,6 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -22,8 +21,13 @@ import (
"chat-agent-system/internal/squad"
)
// mergePiAgentE2EKbConfig 从 .env / 环境变量注入共享知识库与向量检索配置(E2E 必须走真实 KB,不可 stub)。
// mergePiAgentE2EKbConfig 从分层 Load(根 .env + configs/.env.{ENV})注入共享知识库配置。
// E2E 默认 fail-closed:须在线 KB;仅 KB_ALLOW_OFFLINE=1 时允许降级并打印 NOTE。
func mergePiAgentE2EKbConfig(cfg *config.Config) {
if cfg == nil {
return
}
_ = os.Setenv("KB_REQUIRE_ONLINE", "1")
envCfg, err := config.Load("")
if err == nil && envCfg != nil {
cfg.KbDbHost = envCfg.KbDbHost
@@ -38,39 +42,16 @@ func mergePiAgentE2EKbConfig(cfg *config.Config) {
cfg.RerankerAPIKey = envCfg.RerankerAPIKey
cfg.RerankerModel = envCfg.RerankerModel
}
config.NormalizeKBHostForRuntime(cfg)
if cfg.KbDbHost == "" {
cfg.KbDbHost = "127.0.0.1"
}
// Host-side go test cannot use compose DNS; shuimu-kb-shared often resolves to a
// poisoned/VPN address (e.g. 198.18.0.0/15). Prefer the published localhost port.
if cfg.KbDbHost == "shuimu-kb-shared" || cfg.KbDbHost == "shuimu-kb" {
cfg.KbDbHost = "127.0.0.1"
}
if cfg.KbDbPort == 0 {
cfg.KbDbPort = 5432
}
if cfg.KbDbUser == "" {
cfg.KbDbUser = "regkb"
}
// configs/.env.dev often omits KB_DB_PASSWORD; Load then keeps the insecure default.
// Prefer process env, then root .env, so host E2E actually authenticates to :5432.
if v := strings.TrimSpace(os.Getenv("KB_DB_PASSWORD")); v != "" {
cfg.KbDbPassword = v
} else if cfg.KbDbPassword == "" || cfg.KbDbPassword == "shared_kb_secret_pass" {
if b, err := os.ReadFile(".env"); err == nil {
for _, line := range strings.Split(string(b), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "KB_DB_PASSWORD=") {
continue
}
cfg.KbDbPassword = strings.Trim(strings.TrimPrefix(line, "KB_DB_PASSWORD="), "\"'")
break
}
}
}
if cfg.KbDbPassword == "" {
cfg.KbDbPassword = envOr("KB_DB_PASSWORD", "kb_secret_pass")
}
if cfg.KbDbName == "" {
cfg.KbDbName = "regulations"
}
@@ -131,12 +112,20 @@ func TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E(t *testing.T) {
var healthData server.HealthResponse
_ = json.NewDecoder(respHealth.Body).Decode(&healthData)
respHealth.Body.Close()
t.Logf("✓ [0. 环境校验] Health检查正常: Env=%s, AgentBackend=%s, SharedKBPort=%d",
healthData.Env, healthData.AgentBackend, healthData.SharedKBPort)
t.Logf("✓ [0. 环境校验] Health检查正常: Env=%s, AgentBackend=%s, SharedKBPort=%d kb_ready=%v",
healthData.Env, healthData.AgentBackend, healthData.SharedKBPort, healthData.KBReady)
if !kb.RequireOnline() && !healthData.KBReady {
t.Logf("NOTE: KB offline degradation test (KB_ALLOW_OFFLINE=1)")
}
kbClient := kb.NewClient(cfg)
defer kbClient.Close()
if stats, err := kbClient.Stats(ctx); err != nil {
t.Logf("⚠ [0. 知识库] 未连接共享 KB(%s:%d/%s): %v", cfg.KbDbHost, cfg.KbDbPort, cfg.KbDbName, err)
if !kbClient.Online() {
if kb.RequireOnline() {
t.Fatalf("KB required online but offline (host=%s port=%d); set KB_ALLOW_OFFLINE=1 only for degradation tests", cfg.KbDbHost, cfg.KbDbPort)
}
t.Logf("NOTE: KB offline degradation test — continuing without shared KB")
} else if stats, err := kbClient.Stats(ctx); err != nil {
t.Fatalf("KB online but Stats failed: %v", err)
} else if reg, ok := stats[kb.DomainRegulatory]; ok {
t.Logf("✓ [0. 知识库] 已连接共享 KB: documents=%d chunks=%d", reg.DocumentCount, reg.ChunkCount)
} else {
+38
View File
@@ -126,6 +126,44 @@ export PI_AGENT_RUNNER_AUTH_TOKEN="${TOKEN}"
export RUNNER_AUTH_TOKEN="${TOKEN}"
export PI_AGENT_MODEL="${MODEL_ID}"
export RUN_PI_E2E=1
export KB_REQUIRE_ONLINE="${KB_REQUIRE_ONLINE:-1}"
# Shared KB preflight (host :5432). Fail closed unless KB_ALLOW_OFFLINE=1.
preflight_shared_kb() {
if [[ "${KB_ALLOW_OFFLINE:-0}" == "1" ]]; then
echo "NOTE: KB offline degradation test (KB_ALLOW_OFFLINE=1)" | tee "${EVIDENCE_DIR}/kb-allow-offline.txt"
return 0
fi
local kb_host="${KB_DB_HOST:-127.0.0.1}"
local kb_port="${KB_DB_PORT:-5432}"
local kb_user="${KB_DB_USER:-regkb}"
local kb_name="${KB_DB_NAME:-regulations}"
local kb_pass="${KB_DB_PASSWORD:-}"
if [[ -z "${kb_pass}" && -f "${ROOT}/.env" ]]; then
kb_pass="$(grep -E '^KB_DB_PASSWORD=' "${ROOT}/.env" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
fi
if [[ -z "${kb_pass}" ]]; then
echo "KB_DB_PASSWORD missing (root .env or env); cannot preflight shared KB" | tee "${EVIDENCE_DIR}/infra-kb-missing-password.txt"
write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-kb-missing-password.txt"
echo "infrastructure failure: shared KB password missing" >&2
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found; skip SQL preflight, rely on go test KB_REQUIRE_ONLINE" | tee "${EVIDENCE_DIR}/kb-preflight-skipped-no-psql.txt"
return 0
fi
if ! PGPASSWORD="${kb_pass}" psql -h "${kb_host}" -p "${kb_port}" -U "${kb_user}" -d "${kb_name}" -c 'SELECT 1' >/dev/null 2>"${EVIDENCE_DIR}/kb-preflight.err"; then
{
echo "shared KB preflight failed host=${kb_host} port=${kb_port} db=${kb_name} user=${kb_user}"
cat "${EVIDENCE_DIR}/kb-preflight.err" 2>/dev/null || true
} | tee "${EVIDENCE_DIR}/infra-kb-down.txt"
write_receipt infra_fail infra "${EVIDENCE_DIR}/infra-kb-down.txt"
echo "infrastructure failure: shared KB unreachable" >&2
exit 1
fi
echo "shared KB preflight OK ${kb_host}:${kb_port}/${kb_name}" | tee "${EVIDENCE_DIR}/kb-preflight-ok.txt"
}
preflight_shared_kb
# Clear env pollution that flakes short/config tests elsewhere.
unset AGENT_TIMEOUT KB_DATABASE_URL KB_EMBEDDING_DIM 2>/dev/null || true