merge: fix/wave1-main-env-fix
Wave 1: align main app/events ENV=main, app depends_on events healthy, fail-closed empty-Env chat import.
This commit is contained in:
+3
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **系统体检整改**:Wave 0(SSE/缓存 P0)**done**;Wave 1(main ENV E-1)待决策
|
||||
1. **系统体检整改**:Wave 0 **done**;Wave 1(main ENV E-1/E-2/E-5)**in_progress**
|
||||
2. **rdev 模板深度 / document-author-only**:已合入 `origin/dev`;未晋升 test/main
|
||||
3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|-------|------|--------|----------|-------|--------|
|
||||
| Cursor | 2026-09-18-wave1-main-env-fix | fix/wave1-main-env-fix | - | infra | in_progress |
|
||||
| Cursor | 2026-09-18-wave0-p0-bug-fixes | fix/wave0-p0-sse-chat-cache-bugs | - | chat + web | done |
|
||||
| Kimi | 2026-09-17-system-health-check(只读审计,未实施) | - | - | audit | done |
|
||||
| Cursor | 2026-09-17-docgov-modification-proposal-http | feature/docgov-modification-proposal-http | - | docgov | done |
|
||||
@@ -132,7 +133,7 @@
|
||||
|
||||
| holder | phase | since | scope | queue |
|
||||
|--------|-------|-------|-------|-------|
|
||||
| - | idle | 2026-09-18 | Wave 0 P0 已合入并冒烟 PASS(见 [test-report](../test-reports/2026-09-18-wave0-p0-bug-fixes-test-report.md)) | - |
|
||||
| Cursor | merging | 2026-09-18 | Wave 1 main ENV(fix/wave1-main-env-fix) | - |
|
||||
|
||||
**phase**: `idle` | `merging` | `smoke` | `e2e`
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Wave 1:main 环境 ENV/schema 修正(E-1/E-2/E-5)"
|
||||
status: in_progress
|
||||
created: 2026-09-18
|
||||
updated: 2026-09-18
|
||||
owner: Cursor
|
||||
branch: fix/wave1-main-env-fix
|
||||
worktree: "-"
|
||||
mutex: infra
|
||||
---
|
||||
|
||||
# Wave 1:main 环境 ENV/schema 修正(E-1/E-2/E-5)
|
||||
|
||||
## 目标
|
||||
|
||||
统一 main 环境 app/events 的 `ENV=main` 语义,用 compose 依赖强制 events 先迁移再启 app,并关闭 `Env==""` 视同 dev 的导入门禁漏洞。
|
||||
|
||||
## 范围
|
||||
|
||||
- `docker-compose.main.yml`:app `ENV: main`;`depends_on.events` healthy
|
||||
- `configs/.env.main` / `.env.main.example`:`ENV=main`
|
||||
- `internal/server/chat_window_api.go`:去掉空 Env 视同 dev
|
||||
- 回归测试 + 真实 main rebuild 验证
|
||||
|
||||
## 非目标
|
||||
|
||||
- Wave 2+;不改 `event_consumer_modes` 的 mode 值;不触碰 FAV / MULTI_INTENT / SEMANTIC_SUMMARY_REPLACE
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] 从本地 `dev` 开分支并 CURRENT 认领
|
||||
2. [ ] E-1 ENV 对齐
|
||||
3. [ ] E-2 depends_on events
|
||||
4. [ ] E-5 空 Env fail-closed
|
||||
5. [ ] 单测 + 合入 + main rebuild 冒烟
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] app/events health 均报 `env=main`
|
||||
- [ ] main `schema_version=49`
|
||||
- [ ] `event_consumer_modes` 中 main 四行仍为 `off`
|
||||
- [ ] `go test -short` 全绿;导入端点空 Env → 403
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Audit: [../../audit/2026-09-17-system-health-check.md](../../audit/2026-09-17-system-health-check.md)
|
||||
- Remediation: [../../remediation/2026-09-17-system-health-remediation-plan.md](../../remediation/2026-09-17-system-health-remediation-plan.md)
|
||||
- Test report: (待归档)
|
||||
|
||||
## 证据 / 备注
|
||||
|
||||
(实施中)
|
||||
@@ -4,7 +4,7 @@
|
||||
# 根 .env 提供公共模型与检索配置;本文件覆盖生产端口、数据库和生产密钥。
|
||||
# =============================================================================
|
||||
|
||||
ENV=prod
|
||||
ENV=main
|
||||
|
||||
# 1. 业务 HTTP 服务
|
||||
HTTP_PORT=8000
|
||||
|
||||
@@ -86,8 +86,10 @@ services:
|
||||
depends_on:
|
||||
system-db:
|
||||
condition: service_healthy
|
||||
events:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
ENV: prod
|
||||
ENV: main
|
||||
HTTP_ADDR: ":${HTTP_PORT:-8000}"
|
||||
HTTP_PORT: "${HTTP_PORT:-8000}"
|
||||
HTTP_HOST: "0.0.0.0"
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func composeRepoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller")
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
|
||||
}
|
||||
|
||||
var composeServiceENV = regexp.MustCompile(`(?m)^\s+ENV:\s*(\S+)\s*$`)
|
||||
|
||||
func extractServiceENV(composeYAML, serviceName string) (string, error) {
|
||||
// Find " serviceName:" at top-level service indentation (two spaces).
|
||||
marker := "\n " + serviceName + ":\n"
|
||||
idx := strings.Index(composeYAML, marker)
|
||||
if idx < 0 {
|
||||
// file may start with the service after "services:\n"
|
||||
alt := "\nservices:\n " + serviceName + ":\n"
|
||||
idx = strings.Index(composeYAML, alt)
|
||||
if idx < 0 {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
idx += len("\nservices:")
|
||||
}
|
||||
rest := composeYAML[idx+1:] // start at " serviceName:"
|
||||
// Next top-level service starts at "\n <name>:" where name is not indented further.
|
||||
next := regexp.MustCompile(`(?m)^ [a-zA-Z0-9_-]+:\s*$`)
|
||||
locs := next.FindAllStringIndex(rest, -1)
|
||||
block := rest
|
||||
if len(locs) >= 2 {
|
||||
// locs[0] is current service header; locs[1] is next sibling
|
||||
block = rest[:locs[1][0]]
|
||||
}
|
||||
m := composeServiceENV.FindStringSubmatch(block)
|
||||
if m == nil {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return strings.Trim(m[1], `"'`), nil
|
||||
}
|
||||
|
||||
func TestComposeAppEventsENVAligned(t *testing.T) {
|
||||
root := composeRepoRoot(t)
|
||||
files := []string{
|
||||
"docker-compose.dev.yml",
|
||||
"docker-compose.test.yml",
|
||||
"docker-compose.main.yml",
|
||||
}
|
||||
for _, name := range files {
|
||||
raw, err := os.ReadFile(filepath.Join(root, name))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
text := string(raw)
|
||||
appEnv, err := extractServiceENV(text, "app")
|
||||
if err != nil {
|
||||
t.Fatalf("%s: app ENV: %v", name, err)
|
||||
}
|
||||
eventsEnv, err := extractServiceENV(text, "events")
|
||||
if err != nil {
|
||||
t.Fatalf("%s: events ENV: %v", name, err)
|
||||
}
|
||||
if appEnv != eventsEnv {
|
||||
t.Fatalf("%s: app ENV=%q != events ENV=%q", name, appEnv, eventsEnv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeMainAppDependsOnEventsHealthy(t *testing.T) {
|
||||
root := composeRepoRoot(t)
|
||||
raw, err := os.ReadFile(filepath.Join(root, "docker-compose.main.yml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
marker := "\n app:\n"
|
||||
idx := strings.Index(text, marker)
|
||||
if idx < 0 {
|
||||
t.Fatal("app service not found in docker-compose.main.yml")
|
||||
}
|
||||
rest := text[idx+1:]
|
||||
next := regexp.MustCompile(`(?m)^ [a-zA-Z0-9_-]+:\s*$`)
|
||||
locs := next.FindAllStringIndex(rest, -1)
|
||||
block := rest
|
||||
if len(locs) >= 2 {
|
||||
block = rest[:locs[1][0]]
|
||||
}
|
||||
// Nail E-2: app depends_on events with service_healthy
|
||||
re := regexp.MustCompile(`(?m)^\s+events:\s*\n\s+condition:\s*service_healthy\s*$`)
|
||||
if !re.MatchString(block) {
|
||||
t.Fatalf("docker-compose.main.yml app must depends_on events with condition: service_healthy\n--- app block ---\n%s", block)
|
||||
}
|
||||
appEnv, err := extractServiceENV(text, "app")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if appEnv != "main" {
|
||||
t.Fatalf("main app ENV want main, got %q", appEnv)
|
||||
}
|
||||
}
|
||||
@@ -167,8 +167,8 @@ func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// 安全门禁:生产环境默认禁止调用,仅在 dev 或显式开启时开放
|
||||
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || s.cfg.Env == "" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
|
||||
// 安全门禁:生产环境默认禁止调用,仅在显式 dev 或 ENABLE_DEV_IMPORT 时开放
|
||||
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
|
||||
if !isDev {
|
||||
writeSafeError(w, r, http.StatusForbidden, "chat messages import endpoint is only available in dev/demo environment")
|
||||
return
|
||||
|
||||
@@ -179,3 +179,30 @@ func TestServer_ChatWindow_ImportGuard(t *testing.T) {
|
||||
t.Fatalf("expected 403 Forbidden in production env, got %d", rrImport.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ChatWindow_ImportGuard_EmptyEnvFailsClosed(t *testing.T) {
|
||||
// E-5:Env 为空不得视同 dev,导入端点默认拒绝
|
||||
cfg := &config.Config{
|
||||
Env: "test",
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPort: 8889,
|
||||
DataDir: t.TempDir(),
|
||||
AllowInMemory: true,
|
||||
AgentBackend: "stub",
|
||||
}
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := server.NewServer(cfg, svc)
|
||||
cfg.Env = ""
|
||||
t.Setenv("ENABLE_DEV_IMPORT", "")
|
||||
handler := srv.Routes()
|
||||
|
||||
reqImport := httptest.NewRequest("POST", "/api/v1/chat/messages/import", bytes.NewReader([]byte("[]")))
|
||||
reqImport.Header.Set("Content-Type", "application/json")
|
||||
withBearerAuth(reqImport, loginTestAdmin(t, handler, cfg))
|
||||
rrImport := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rrImport, reqImport)
|
||||
|
||||
if rrImport.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden when Env is empty, got %d body=%s", rrImport.Code, rrImport.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user