package config_test import ( "os" "path/filepath" "regexp" "strings" "testing" ) var composeFilesForFlagCheck = []string{ "docker-compose.dev.yml", "docker-compose.test.yml", "docker-compose.main.yml", } func readComposeFile(t *testing.T, name string) string { t.Helper() root := composeRepoRoot(t) raw, err := os.ReadFile(filepath.Join(root, name)) if err != nil { t.Fatalf("%s: %v", name, err) } return string(raw) } // serviceBlockText extracts one top-level compose service block's raw YAML text. func serviceBlockText(t *testing.T, composeYAML, serviceName string) string { t.Helper() marker := "\n " + serviceName + ":\n" idx := strings.Index(composeYAML, marker) if idx < 0 { t.Fatalf("service %q not found", serviceName) } rest := composeYAML[idx+1:] next := regexp.MustCompile(`(?m)^ [a-zA-Z0-9_-]+:\s*$`) locs := next.FindAllStringIndex(rest, -1) if len(locs) >= 2 { return rest[:locs[1][0]] } return rest } // E-3: MULTI_INTENT_PLAN_TIER and U9-U12 must be declared (legacy/false) in all // three environments' app service so operator-set env vars actually reach the // container. Values must never change from legacy/false in this test. func TestComposeAppDeclaresMultiIntentPlanTierAndExpansionFlags(t *testing.T) { wantKeys := []string{ "MULTI_INTENT_U9_SEGMENT:", "MULTI_INTENT_U10_MUTATE:", "MULTI_INTENT_U11_DOCGOV:", "MULTI_INTENT_U12_RUN_CONTROL:", } for _, name := range composeFilesForFlagCheck { block := serviceBlockText(t, readComposeFile(t, name), "app") if !strings.Contains(block, "MULTI_INTENT_PLAN_TIER: ${MULTI_INTENT_PLAN_TIER:-legacy}") { t.Errorf("%s: app service must declare MULTI_INTENT_PLAN_TIER with legacy default", name) } for _, key := range wantKeys { if !strings.Contains(block, key) { t.Errorf("%s: app service missing %s declaration", name, key) } } } } // A-4: SEMANTIC_SUMMARY_SHADOW / REPLACE(+SESSIONS allowlist) / // PI_CHANGE_REPORT_NARRATIVE must be declared in all three environments so // operator-set env vars reach the container (REPLACE is fail-closed gray: // master + non-empty session allowlist; defaults stay off/empty). func TestComposeAppDeclaresSemanticSummaryAndNarrativeShadowFlags(t *testing.T) { for _, name := range composeFilesForFlagCheck { block := serviceBlockText(t, readComposeFile(t, name), "app") for _, key := range []string{ "SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}", "SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}", "SEMANTIC_SUMMARY_REPLACE_SESSIONS: ${SEMANTIC_SUMMARY_REPLACE_SESSIONS:-}", "PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}", } { if !strings.Contains(block, key) { t.Errorf("%s: app service missing %s declaration", name, key) } } } } func TestComposeAppDeclaresJevDecideFlags(t *testing.T) { for _, name := range composeFilesForFlagCheck { block := serviceBlockText(t, readComposeFile(t, name), "app") if !strings.Contains(block, "JEV_DECIDE_MODE: ${JEV_DECIDE_MODE:-off}") { t.Errorf("%s: app service must declare JEV_DECIDE_MODE with off default", name) } for _, key := range []string{"JEV_BASE_URL:", "JEV_API_KEY:"} { if !strings.Contains(block, key) { t.Errorf("%s: app service missing %s declaration", name, key) } } } } // E-6: main must explicitly disable demo-user seeding at the compose layer // (defense in depth; internal/auth/bootstrap.go already gates on cfg.Env=="main"). func TestComposeMainSeedDemoUsersExplicitFalse(t *testing.T) { block := serviceBlockText(t, readComposeFile(t, "docker-compose.main.yml"), "app") if !strings.Contains(block, `SEED_DEMO_USERS: "false"`) { t.Fatalf("docker-compose.main.yml: app service must declare SEED_DEMO_USERS: \"false\"") } } // E-4: CURSOR_RUNNER_URL has zero Go readers repo-wide; neither it nor the // cursor-agent placeholder service should exist in any environment. func TestComposeNoDeadCursorRunnerConfig(t *testing.T) { for _, name := range composeFilesForFlagCheck { text := readComposeFile(t, name) if strings.Contains(text, "CURSOR_RUNNER_URL") { t.Errorf("%s: dead CURSOR_RUNNER_URL must stay removed (E-4)", name) } if strings.Contains(text, "\n cursor-agent:\n") { t.Errorf("%s: dead cursor-agent placeholder service must stay removed (E-4)", name) } } } // X-1: SEMANTIC_FALLBACK_ENABLED has zero Go readers repo-wide and no design // doc describing intended semantics; Wave 5 decision = delete, not re-declare. func TestComposeNoDeadSemanticFallbackConfig(t *testing.T) { for _, name := range composeFilesForFlagCheck { if strings.Contains(readComposeFile(t, name), "SEMANTIC_FALLBACK_ENABLED") { t.Errorf("%s: dead SEMANTIC_FALLBACK_ENABLED must stay removed (Wave5 X-1)", name) } } }