- SEMANTIC_SUMMARY_REPLACE_SESSIONS supports * (all sessions, present and future), mirroring INTENT_PLAN_PROJECT_ALLOWLIST; empty allowlist still replaces nothing (fail-closed defense in depth unchanged) - declare SEMANTIC_SUMMARY_REPLACE(_SESSIONS) in dev/test/main compose app env (stale Wave2 "REPLACE unwired" comment fixed); extend A-4 declaration test; .env.example key docs updated - sysadmin feature catalog: SESSIONS description + REPLACE reason note * Prepares dev degraded-flags enable (task degraded-flags-enable-2026-09-26-r4): user-authorized all-session replace, plan tier confirm, shadow summary, PI change-report narrative; env ops and runtime verification follow.
134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|