diff --git a/deploy/README.md b/deploy/README.md index f7d0687..3fd06f5 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -8,10 +8,15 @@ Grafana 与 Kubernetes。配置已通过静态展开,但当前仓库的 `cmd/p ```powershell docker compose -f deploy/docker-compose.yml config --quiet +docker compose -f deploy/docker-compose.test.yml config --quiet kubectl kustomize deploy/kubernetes/base | Out-Null go run ./deploy/tools/configcheck deploy/config/local.yaml ``` +`docker-compose.test.yml` 为 Redis 与 PostgreSQL 18 提供互相独立的本地集成测试 +服务。两者只绑定回环地址;Redis 禁止持久化,PostgreSQL 使用 +`/var/lib/postgresql` tmpfs。`test-redis.ps1` 通过专属 Compose 项目只启动 Redis; +PostgreSQL Adapter 和对应执行脚本完成前,不把数据库契约记为通过。 + 运行时完成后,还必须通过 `production-readiness.md` 中的一致性、安全、恢复、 竞态与容量门禁,才能构建镜像并发布。 - diff --git a/deploy/compose_test.go b/deploy/compose_test.go index 41e061e..f7f099e 100644 --- a/deploy/compose_test.go +++ b/deploy/compose_test.go @@ -2,6 +2,8 @@ package deploy import ( "os" + "slices" + "strings" "testing" "go.yaml.in/yaml/v4" @@ -13,11 +15,48 @@ type composeDocument struct { } type composeService struct { + Image string `yaml:"image"` Command []string `yaml:"command"` + Ports []string `yaml:"ports"` Volumes []string `yaml:"volumes"` + Tmpfs []string `yaml:"tmpfs"` DependsOn any `yaml:"depends_on"` } +func TestPostgresIntegrationFixtureIsIsolatedAndEphemeral(t *testing.T) { + document := loadComposeFile(t, "docker-compose.test.yml") + postgres, ok := document.Services["postgres"] + if !ok { + t.Fatal("docker-compose.test.yml has no postgres service") + } + if postgres.Image != "postgres:18-alpine" { + t.Fatalf("postgres image = %q, want postgres:18-alpine", postgres.Image) + } + if !slices.Equal(postgres.Ports, []string{"127.0.0.1:15432:5432"}) { + t.Fatalf("postgres ports = %v, want loopback test port", postgres.Ports) + } + if !slices.Contains(postgres.Tmpfs, "/var/lib/postgresql") || len(postgres.Volumes) != 0 { + t.Fatalf("postgres tmpfs = %v, volumes = %v; want ephemeral PG18 data root", postgres.Tmpfs, postgres.Volumes) + } +} + +func TestRedisFixtureScriptUsesDedicatedComposeProject(t *testing.T) { + payload, err := os.ReadFile("../scripts/test-redis.ps1") + if err != nil { + t.Fatalf("read test-redis.ps1: %v", err) + } + script := string(payload) + for _, required := range []string{ + `-p $composeProject`, + `up -d --wait --wait-timeout 60 redis`, + `down --volumes --remove-orphans`, + } { + if !strings.Contains(script, required) { + t.Errorf("test-redis.ps1 missing %q", required) + } + } +} + func TestLocalRedisIsExplicitlyEphemeral(t *testing.T) { document := loadComposeDocument(t) redis, ok := document.Services["redis"] @@ -55,13 +94,18 @@ func TestLocalGatewaysDoNotDependOnControlPlaneStorage(t *testing.T) { func loadComposeDocument(t *testing.T) composeDocument { t.Helper() - payload, err := os.ReadFile("docker-compose.yml") + return loadComposeFile(t, "docker-compose.yml") +} + +func loadComposeFile(t *testing.T, name string) composeDocument { + t.Helper() + payload, err := os.ReadFile(name) if err != nil { - t.Fatalf("read docker-compose.yml: %v", err) + t.Fatalf("read %s: %v", name, err) } var document composeDocument if err := yaml.Unmarshal(payload, &document); err != nil { - t.Fatalf("parse docker-compose.yml: %v", err) + t.Fatalf("parse %s: %v", name, err) } return document } diff --git a/deploy/docker-compose.test.yml b/deploy/docker-compose.test.yml index 838943b..a2bdd08 100644 --- a/deploy/docker-compose.test.yml +++ b/deploy/docker-compose.test.yml @@ -1,6 +1,23 @@ name: proxy-pool-test services: + postgres: + image: postgres:18-alpine + environment: + POSTGRES_DB: proxy_pool_test + POSTGRES_USER: proxy_pool_test + POSTGRES_PASSWORD: proxy-pool-test + ports: + - "127.0.0.1:15432:5432" + tmpfs: + - /var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U proxy_pool_test -d proxy_pool_test"] + interval: 1s + timeout: 1s + retries: 30 + start_period: 1s + redis: image: redis:8.2-alpine command: ["redis-server", "--appendonly", "no", "--save", ""] diff --git a/docs/testing/test-strategy.md b/docs/testing/test-strategy.md index 3accfd3..38d04e1 100644 --- a/docs/testing/test-strategy.md +++ b/docs/testing/test-strategy.md @@ -95,8 +95,9 @@ fixture 的执行命令是: PostgreSQL 管理面使用 `adminstate/contracttest` 作为 Memory/PostgreSQL 公用 行为契约。当前 MemoryStore 已覆盖配置提交、Upstream 幂等、100 并发 Routing -CAS、审计和租约 Outbox;Schema 静态测试证明只声明六张管理表。只有 pgx -Adapter 在真实 PostgreSQL 18 上运行同一契约后,才标记生产持久化完成。 +CAS、审计分页、Routing no-op、原子批量 ACK 和租约 Outbox;Schema 静态测试 +证明只声明六张管理表。PostgreSQL 18 的回环端口、tmpfs 隔离 fixture 已完成 +静态验证;只有 pgx Adapter 在该实例上运行同一契约后,才标记生产持久化完成。 Admin 应用层测试覆盖 typed-nil 依赖、Actor/SourceIP 映射、Routing CAS 错误、 权威管理快照与低基数运行态聚合、未知字段拒绝、主配置/Secret 文件 I/O 分类、 diff --git a/findings.md b/findings.md index 89fd45e..d72c2a2 100644 --- a/findings.md +++ b/findings.md @@ -173,3 +173,13 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两 可执行的 `deploy/tools/configcheck`,并明确生产 Controller 入口仍是计划能力。 - 文档中的具体 `go run`/`go build` 目标现在必须存在;包含 `...` 的通配包命令 由 Go 工具链自身解析并在完整验证中执行。 + +## PostgreSQL 18 Fixture 审计(2026-07-29) + +- 集成 Compose 已新增仅绑定 `127.0.0.1:15432` 的 `postgres:18-alpine` 服务, + 数据目录挂载为 `/var/lib/postgresql` tmpfs,不声明测试持久卷。 +- Redis 测试脚本现使用专属 Compose 项目并只启动 Redis,防止未来加入的 + PostgreSQL fixture 被无关测试启动或清理。 +- 生产 `docker-compose.yml` 仍把 PostgreSQL 18 命名卷挂在旧路径 + `/var/lib/postgresql/data`;直接修改可能影响已有本地数据,必须配套迁移步骤后 + 单独处理,当前不能把生产持久化拓扑视为已验证。 diff --git a/progress.md b/progress.md index 9b62093..ac7a430 100644 --- a/progress.md +++ b/progress.md @@ -2,6 +2,9 @@ ## 2026-07-29 +- 新增 PostgreSQL 18 隔离测试服务的静态契约:回环端口、正确 PG18 tmpfs 数据 + 根目录、零持久卷;Redis 测试改用独立 Compose 项目和显式服务启动,避免两个 + fixture 相互影响。未拉取或启动容器,pgx Adapter/真实契约仍待依赖确认。 - 扩展 `adminstate/contracttest` 公用契约:审计完整字段和 AfterID 分页、Routing no-op 审计且不写 Outbox、批量 ACK 零部分提交,以及六个 Store 方法的 Context 取消;MemoryStore 全部通过,后续 PostgreSQL Adapter 必须运行同一套契约。 diff --git a/scripts/test-redis.ps1 b/scripts/test-redis.ps1 index c2c4220..74f5b2a 100644 --- a/scripts/test-redis.ps1 +++ b/scripts/test-redis.ps1 @@ -2,10 +2,11 @@ $ErrorActionPreference = "Stop" $repositoryRoot = Split-Path -Parent $PSScriptRoot $composeFile = Join-Path $repositoryRoot "deploy/docker-compose.test.yml" +$composeProject = "proxy-pool-redis-test" $previousRedisURL = [Environment]::GetEnvironmentVariable("PROXY_POOL_TEST_REDIS_URL", "Process") try { - docker compose -f $composeFile up -d --wait + docker compose -p $composeProject -f $composeFile up -d --wait --wait-timeout 60 redis if ($LASTEXITCODE -ne 0) { throw "starting Redis test fixture failed with exit code $LASTEXITCODE" } @@ -29,5 +30,5 @@ finally { else { $env:PROXY_POOL_TEST_REDIS_URL = $previousRedisURL } - docker compose -f $composeFile down --remove-orphans + docker compose -p $composeProject -f $composeFile down --volumes --remove-orphans }