package main import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "testing" "time" ) func TestDiscoverComposeInputsRequiredAndOptional(t *testing.T) { content := `A=${POSTGRES_SUPERUSER} B=${POSTGRES_DB:-assistant} C=${CREATOR_PASSWORD_FILE} D=${FOO_CREDENTIAL_REF} ` inputs := discoverComposeInputs(content) if _, ok := inputs["POSTGRES_SUPERUSER"]; !ok { t.Fatal("POSTGRES_SUPERUSER should be discovered") } if !inputs["POSTGRES_SUPERUSER"].required { t.Fatalf("POSTGRES_SUPERUSER expected required") } if inputs["POSTGRES_DB"].required { t.Fatalf("POSTGRES_DB expected optional") } if !inputs["CREATOR_PASSWORD_FILE"].required { t.Fatalf("CREATOR_PASSWORD_FILE expected required from compose placeholder") } if !inputs["EPISTEMICS_POSTGRES_CREDENTIAL_REF"].required { t.Fatalf("required production bootstrap variable should be required") } if got := len(inputs); got < 4 { t.Fatalf("expected at least 4 discovered inputs, got %d", got) } } func TestResolveDeploymentInputsRejectsAmbientProduction(t *testing.T) { t.Setenv("POSTGRES_SUPERUSER", "from_ambient") inputs := map[string]composeInput{ "POSTGRES_SUPERUSER": {name: "POSTGRES_SUPERUSER", required: true}, } resolved, _, _, err := resolveDeploymentInputs(inputs, "", true) if err != nil { t.Fatalf("resolveDeploymentInputs error: %v", err) } if len(resolved) != 1 { t.Fatalf("expected one resolved input, got %d", len(resolved)) } if resolved[0].Source != "ambient" || resolved[0].Resolved { t.Fatalf("expected unresolved ambient resolution, got source=%s resolved=%v", resolved[0].Source, resolved[0].Resolved) } } func TestDeployPrepareRequiresEnvFileForProduction(t *testing.T) { root := t.TempDir() composeFile := filepath.Join(root, "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil { t.Fatal(err) } err := deployPrepare([]string{"--target", "production", "--compose", composeFile}) if err == nil || !strings.Contains(err.Error(), "production prepare requires --env-file") { t.Fatalf("expected missing env-file failure, got %v", err) } } func TestDeployPrepareBlockedAndApplyRefusesMissingRequiredInput(t *testing.T) { root := t.TempDir() repoRoot, err := gitRepositoryRoot(".") if err != nil { t.Fatal(err) } composeFile := filepath.Join(root, "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(root, "production.env") parts := make([]string, 0, len(requiredChatOnlyInputs)) for key := range requiredChatOnlyInputs { if key == "POSTGRES_SUPERUSER" { continue } parts = append(parts, key+"=value-"+key) } if err := os.WriteFile(envFile, []byte(strings.Join(parts, "\n")), 0o600); err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } target.ComposeFile = composeFile plan, err := buildDeploymentPlan(repoRoot, target, envFile, "HEAD", 24, "") if err != nil { t.Fatal(err) } if plan.Readiness != readinessFailed { t.Fatalf("expected blocked plan, got readiness=%s", plan.Readiness) } if !strings.Contains(strings.Join(plan.ReadinessReasons, "\n"), "POSTGRES_SUPERUSER") { t.Fatalf("expected missing required input blocker for POSTGRES_SUPERUSER, got %v", plan.ReadinessReasons) } planPath := filepath.Join(root, "blocked-plan.json") payload, err := json.Marshal(plan) if err != nil { t.Fatal(err) } if err := os.WriteFile(planPath, payload, 0o600); err != nil { t.Fatal(err) } if err := deployApply([]string{"-plan", planPath}); err == nil { t.Fatal("expected apply refusal for blocked plan") } } func TestAssertPlanCurrentRejectsStaleInputChange(t *testing.T) { root := t.TempDir() composeFile := filepath.Join(root, "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(root, "deploy.env") if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=first\n"), 0o600); err != nil { t.Fatal(err) } inputs, err := discoverComposeInputsFromFile(composeFile) if err != nil { t.Fatal(err) } resolved, _, _, err := resolveDeploymentInputs(inputs, envFile, true) if err != nil { t.Fatal(err) } plan := &deploymentPlan{ Version: 1, PlanID: "stale-test", CreatedAt: time.Now().UTC().Format(time.RFC3339), ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339), Readiness: readinessReady, ReadinessReasons: nil, Target: "production", ComposeFile: composeFile, DockerContext: "docker-prod", ProjectName: "assistant", EnvFile: envFile, SourceRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98", SourceRevisionMode: "explicit", ArtifactRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98", DeploymentConfigRev: resolveConfigHash(composeFile), CurrentProductionRev: "", KnownGoodRevision: "", Inputs: resolved, Classify: map[string][]string{}, KnownInputsSource: map[string]string{}, } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } plan.PlanBindingHash = buildPlanBindingFromInputs( resolved, plan.SourceRevision, plan.ArtifactRevision, plan.DeploymentConfigRev, target.DockerContext, target.Containers, target.Volumes, ) if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=second\n"), 0o600); err != nil { t.Fatal(err) } if err := assertPlanCurrent(plan); err == nil { t.Fatal("expected stale plan after input change") } } func TestAssertPlanCurrentRejectsArtifactMismatch(t *testing.T) { root := t.TempDir() composeFile := filepath.Join(root, "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(root, "deploy.env") envParts := make([]string, 0, len(requiredChatOnlyInputs)) for key := range requiredChatOnlyInputs { envParts = append(envParts, key+"=value-"+strings.ToLower(key)) } envParts = append(envParts, "POSTGRES_SUPERUSER=from_file") if err := os.WriteFile(envFile, []byte(strings.Join(envParts, "\n")), 0o600); err != nil { t.Fatal(err) } inputs, err := discoverComposeInputsFromFile(composeFile) if err != nil { t.Fatal(err) } discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true) if err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } configRev := resolveConfigHash(composeFile) shaA := strings.Repeat("a", 40) shaB := strings.Repeat("b", 40) plan := &deploymentPlan{ Version: 1, PlanID: "artifact-mismatch", CreatedAt: time.Now().UTC().Format(time.RFC3339), ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339), Readiness: readinessReady, Target: "production", ComposeFile: composeFile, DockerContext: "docker-prod", ProjectName: "assistant", EnvFile: envFile, SourceRevision: shaA, SourceRevisionMode: "explicit", ArtifactRevision: shaA, DeploymentConfigRev: configRev, Inputs: discovered, Classify: map[string][]string{}, KnownInputsSource: map[string]string{}, } plan.PlanBindingHash = buildPlanBindingFromInputs( discovered, shaA, shaA, configRev, target.DockerContext, target.Containers, target.Volumes, ) plan.ArtifactRevision = shaB plan.SourceRevision = shaB if err := assertPlanCurrent(plan); err == nil { t.Fatal("expected stale plan due artifact mismatch") } } func TestMaxMigrationPrefixFromListHandlesNestedMigrationPaths(t *testing.T) { got := maxMigrationPrefixFromList([]string{ "database/migrations/001_bootstrap.sql", "database/migrations/007_legacy.sql", "database/migrations/work/015_capability.sql", "database/migrations/README.md", "database/migrations/artifact/019_work.sql", }) if got != "19" { t.Fatalf("expected max migration prefix 19, got %q", got) } } func TestAssertPlanCurrentRejectsMigrationUpgradeWithoutKnownGood(t *testing.T) { root, err := gitRepositoryRoot(".") if err != nil { t.Fatal(err) } headOut, err := runGit(root, "rev-parse", "HEAD") if err != nil { t.Fatal(err) } targetRevision := strings.TrimSpace(string(headOut)) targetMaxStr, err := maxMigrationPrefixForRevision(root, targetRevision) if err != nil { t.Fatal(err) } targetMax, err := strconv.Atoi(targetMaxStr) if err != nil { t.Fatalf("invalid target migration max %q: %v", targetMaxStr, err) } revisionsOut, err := runGit(root, "rev-list", "HEAD", "--max-count", "20") if err != nil { t.Fatal(err) } ancestorList := strings.Fields(string(revisionsOut)) currentProductionRevision := "" for _, revision := range ancestorList { if strings.TrimSpace(revision) == targetRevision { continue } revMaxStr, err := maxMigrationPrefixForRevision(root, revision) if err != nil { continue } revMax, err := strconv.Atoi(revMaxStr) if err != nil { continue } if revMax < targetMax { currentProductionRevision = revision break } } if currentProductionRevision == "" { t.Skip("could not find historical revision with lower migration max to validate precondition block") } rootDir := t.TempDir() composeFile := filepath.Join(rootDir, "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(rootDir, "bootstrap.env") requiredValues := make([]string, 0, len(requiredChatOnlyInputs)) for key := range requiredChatOnlyInputs { requiredValues = append(requiredValues, key+"=value-"+strings.ToLower(key)) } if err := os.WriteFile(envFile, []byte(strings.Join(requiredValues, "\n")), 0o600); err != nil { t.Fatal(err) } inputs, err := discoverComposeInputsFromFile(composeFile) if err != nil { t.Fatal(err) } discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true) if err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } target.ComposeFile = composeFile plan := &deploymentPlan{ Version: 1, PlanID: "migration-precondition", CreatedAt: time.Now().UTC().Format(time.RFC3339), ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339), Readiness: readinessReady, ReadinessReasons: nil, Target: "production", ComposeFile: composeFile, DockerContext: target.DockerContext, ProjectName: target.ProjectName, EnvFile: envFile, SourceRevision: targetRevision, SourceRevisionMode: "explicit", ArtifactRevision: targetRevision, DeploymentConfigRev: resolveConfigHash(composeFile), CurrentProductionRev: currentProductionRevision, KnownGoodRevision: "", Classify: map[string][]string{}, Inputs: discovered, KnownInputsSource: map[string]string{}, } plan.Migrations, err = migrationState(root, targetRevision, currentProductionRevision) if err != nil { t.Fatal(err) } if !plan.Migrations.NeedKnownGood { t.Fatal("expected migration state to require known good for this test") } plan.PlanBindingHash = buildPlanBindingFromInputs( plan.Inputs, plan.SourceRevision, plan.ArtifactRevision, plan.DeploymentConfigRev, target.DockerContext, target.Containers, target.Volumes, ) if err := assertPlanCurrent(plan); err == nil { t.Fatal("expected assertPlanCurrent to reject migration precondition without known-good") } else if !strings.Contains(err.Error(), "migration precondition failed") { t.Fatalf("expected migration precondition failure, got: %v", err) } } func TestDeploymentPlanDoesNotLeakSecretValues(t *testing.T) { root, err := gitRepositoryRoot(".") if err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } composeFile := filepath.Join(t.TempDir(), "docker-compose.prod.yml") if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil { t.Fatal(err) } target.ComposeFile = composeFile envFile := filepath.Join(t.TempDir(), "production.env") if err := os.WriteFile(envFile, []byte(strings.Join([]string{ "POSTGRES_SUPERUSER_PASSWORD=super-secret-123", "CREATOR_PASSWORD_FILE=creator-password-file-456", "COMMIT_EXECUTOR_TOKEN=executor-token-789", }, "\n")), 0o600); err != nil { t.Fatal(err) } plan, err := buildDeploymentPlan(root, target, envFile, "HEAD", 24, "") if err != nil { t.Fatal(err) } payload, err := json.Marshal(plan) if err != nil { t.Fatal(err) } serialized := string(payload) for _, secretValue := range []string{ "super-secret-123", "creator-password-file-456", "executor-token-789", } { if strings.Contains(serialized, secretValue) { t.Fatalf("plain secret value leaked into plan: %q", secretValue) } } if got := len(plan.SecretReferences); got < 3 { t.Fatalf("expected at least 3 secret references in plan, got %d", got) } } func TestDefaultKnownGoodManifestPathStableWithoutLocalAppData(t *testing.T) { t.Setenv("LOCALAPPDATA", "") t.Setenv("LOCALAPPDATA_ALT", "") if strings.TrimSpace(defaultKnownGoodManifestPath()) != "" { t.Fatal("expected empty path when LOCALAPPDATA is unset") } } func TestDeployApplyRunsHealthGateWithReadyPlanAndSimulatedComposeDocker(t *testing.T) { originalPath := os.Getenv("PATH") fakeDockerDir := t.TempDir() fakeDocker := filepath.Join(fakeDockerDir, "docker.bat") dockerBatch := "@echo off\n" + "if \"%~1\"==\"--context\" (\n" + " if \"%~3\"==\"compose\" exit /B 0\n" + " if \"%~3\"==\"inspect\" (\n" + " if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" + " if \"%~6\"==\"{{.State.Status}}\" echo running\n" + " exit /B 0\n" + " )\n" + " if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" + ")\n" + "exit /B 1\n" if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil { t.Fatal(err) } os.Setenv("PATH", fakeDockerDir+";"+originalPath) defer os.Setenv("PATH", originalPath) root, err := gitRepositoryRoot(".") if err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml") target.ComposeFile = composeFile if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(fakeDockerDir, "production.env") env := []string{ "POSTGRES_SUPERUSER=deploy-apply-test", "POSTGRES_SUPERUSER_PASSWORD=pw-super", "CONTINUITY_DB_PASSWORD=pw-cont", "WORK_DB_PASSWORD=pw-work", "CAPABILITY_DB_PASSWORD=pw-cap", "COMMIT_DB_PASSWORD=pw-commit", "AUTHORITY_DB_PASSWORD=pw-auth", "IDENTITY_DB_PASSWORD=pw-id", "SITUATION_DB_PASSWORD=pw-sit", "CREATOR_USERNAME=creator", "CREATOR_PASSWORD_FILE=creator-password", "CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap", "CREATOR_SESSION_SIGNING_KEY=creator-session", "COMMIT_EXECUTOR_TOKEN=commit-exec", "EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref", "INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref", "EPISTEMICS_POSTGRES_PASSWORD=epi-password", "INTERACTION_POSTGRES_PASSWORD=int-password", } if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil { t.Fatal(err) } knownGood, _, err := captureKnownGood(target, "") if err != nil || strings.TrimSpace(knownGood) == "" { knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98" } plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "") if err != nil { t.Fatal(err) } server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) })) defer server.Close() originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...) deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"} defer func() { deploymentExternalHealthEndpoints = originalHealthEndpoints }() plan.PlanBindingHash = buildPlanBindingFromInputs( plan.Inputs, plan.SourceRevision, plan.ArtifactRevision, plan.DeploymentConfigRev, target.DockerContext, target.Containers, target.Volumes, ) planPath := filepath.Join(fakeDockerDir, "ready-plan.json") payload, err := json.Marshal(plan) if err != nil { t.Fatal(err) } if err := os.WriteFile(planPath, payload, 0o600); err != nil { t.Fatal(err) } t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision) if err := deployApply([]string{"-plan", planPath}); err != nil { t.Fatalf("expected successful apply with simulated docker and passing health checks, got %v", err) } } func TestDeployApplyHealthGateRejectsOnFailedHTTPHealth(t *testing.T) { originalPath := os.Getenv("PATH") fakeDockerDir := t.TempDir() fakeDocker := filepath.Join(fakeDockerDir, "docker.bat") dockerBatch := "@echo off\n" + "if \"%~1\"==\"--context\" (\n" + " if \"%~3\"==\"compose\" exit /B 0\n" + " if \"%~3\"==\"inspect\" (\n" + " if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" + " if \"%~6\"==\"{{.State.Status}}\" echo running\n" + " exit /B 0\n" + " )\n" + " if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" + ")\n" + "exit /B 1\n" if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil { t.Fatal(err) } os.Setenv("PATH", fakeDockerDir+";"+originalPath) defer os.Setenv("PATH", originalPath) root, err := gitRepositoryRoot(".") if err != nil { t.Fatal(err) } target, err := resolveDeploymentTarget("production") if err != nil { t.Fatal(err) } composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml") target.ComposeFile = composeFile if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil { t.Fatal(err) } envFile := filepath.Join(fakeDockerDir, "production.env") env := []string{ "POSTGRES_SUPERUSER=deploy-apply-test", "POSTGRES_SUPERUSER_PASSWORD=pw-super", "CONTINUITY_DB_PASSWORD=pw-cont", "WORK_DB_PASSWORD=pw-work", "CAPABILITY_DB_PASSWORD=pw-cap", "COMMIT_DB_PASSWORD=pw-commit", "AUTHORITY_DB_PASSWORD=pw-auth", "IDENTITY_DB_PASSWORD=pw-id", "SITUATION_DB_PASSWORD=pw-sit", "CREATOR_USERNAME=creator", "CREATOR_PASSWORD_FILE=creator-password", "CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap", "CREATOR_SESSION_SIGNING_KEY=creator-session", "COMMIT_EXECUTOR_TOKEN=commit-exec", "EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref", "INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref", "EPISTEMICS_POSTGRES_PASSWORD=epi-password", "INTERACTION_POSTGRES_PASSWORD=int-password", } if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil { t.Fatal(err) } knownGood, _, err := captureKnownGood(target, "") if err != nil || strings.TrimSpace(knownGood) == "" { knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98" } plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "") if err != nil { t.Fatal(err) } plan.PlanBindingHash = buildPlanBindingFromInputs( plan.Inputs, plan.SourceRevision, plan.ArtifactRevision, plan.DeploymentConfigRev, target.DockerContext, target.Containers, target.Volumes, ) planPath := filepath.Join(fakeDockerDir, "ready-plan-bad-health.json") payload, err := json.Marshal(plan) if err != nil { t.Fatal(err) } if err := os.WriteFile(planPath, payload, 0o600); err != nil { t.Fatal(err) } t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision) server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.URL.Path { case "/healthz": writer.WriteHeader(http.StatusInternalServerError) default: writer.WriteHeader(http.StatusOK) } })) defer server.Close() originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...) deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"} defer func() { deploymentExternalHealthEndpoints = originalHealthEndpoints }() if err := deployApply([]string{"-plan", planPath}); err == nil { t.Fatalf("expected apply to fail when health check fails") } }