From f86889b9b60ffa97442c538661633c0e32e81999 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 19:48:22 -0400 Subject: [PATCH 1/3] test(e2e): embedded-postgres smoke leg and WithEmbeddedPostgres (RIG-2906) TestEmbeddedPostgresSmoke stands up its own stack over the stack-owned postgres, the product default, and checks the pgid record names a live postgres group, Ready health, one authed GetServerInfo, then a clean Down: not Ready, the postgres group reaped, the record removed. Every other leg attaches to the suite postgres, so this is the only e2e leg that covers the supervisor-owns-postgres teardown. WithEmbeddedPostgres pins that arm and fails the test when combined with an external DSN. Fixture.StateDir exposes the record's location. Co-authored-by: Matt Wilkinson --- .../ci/compass-test-harness-tiers/design.md | 3 +- go/e2e/embedded_postgres_test.go | 134 ++++++++++++++++++ go/e2e/fixture.go | 33 ++++- go/e2e/harness_test.go | 2 +- 4 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 go/e2e/embedded_postgres_test.go diff --git a/docs/designs/infra/ci/compass-test-harness-tiers/design.md b/docs/designs/infra/ci/compass-test-harness-tiers/design.md index 12a3c4195..5c0dd5232 100644 --- a/docs/designs/infra/ci/compass-test-harness-tiers/design.md +++ b/docs/designs/infra/ci/compass-test-harness-tiers/design.md @@ -491,8 +491,7 @@ docs. (depends T1). - [x] T3 — `WithOwnStack()` + leg migration + shared canned-stub `LoadScript`. Satisfied on main by `sharedFixture`; see T3 errata. -- [ ] T4 — `TestEmbeddedPostgresSmoke` + `WithEmbeddedPostgres()`. Owner: - implement lane (depends T3). +- [x] T4 — `TestEmbeddedPostgresSmoke` + `WithEmbeddedPostgres()`. - [x] T5 — forge-injection convergence (`mountForgeForTest`). Satisfied on main by `newForgeE2EWire`; see T5 errata. - [ ] T6 — tier-pyramid contributor doc. Owner: implement lane (depends diff --git a/go/e2e/embedded_postgres_test.go b/go/e2e/embedded_postgres_test.go new file mode 100644 index 000000000..91b7edc26 --- /dev/null +++ b/go/e2e/embedded_postgres_test.go @@ -0,0 +1,134 @@ +//go:build podman + +package e2e + +import ( + "context" + "errors" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "testing" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/stack" +) + +// TestEmbeddedPostgresSmoke is the one e2e leg that runs the product-default +// postgres: the stack spawns and owns it, so this is the only dedicated e2e +// coverage of the supervisor-owns-postgres lifecycle and its pgid-record +// teardown. Every other leg attaches to the suite postgres instead. +func TestEmbeddedPostgresSmoke(t *testing.T) { + if !podmanUsable(t.Context()) { + t.Skip("rootless podman cannot run compass-agent:latest here; skipping the real-stack e2e") + } + ctx := context.Background() + f := NewFixture(ctx, t, WithEmbeddedPostgres()) + + // The stack recorded the postgres it spawned, and that group is alive. + pgid := recordedPostgresPgid(t, f.StateDir()) + if err := syscall.Kill(-pgid, 0); err != nil { + t.Fatalf("recorded postgres group %d is not alive after Up: %v", pgid, err) + } + + health, err := f.Stack().Health(ctx) + if err != nil { + t.Fatalf("Health: %v", err) + } + if health.State != stack.StatusReady { + t.Fatalf("stack state = %v (%s), want Ready", health.State, health.Detail) + } + + rctx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + resp, err := f.Compass().GetServerInfo(rctx, connect.NewRequest(&compassv1.GetServerInfoRequest{})) + if err != nil { + t.Fatalf("authed GetServerInfo over the embedded postgres: %v", err) + } + if resp.Msg.GetVersion() == "" { + t.Fatal("GetServerInfo returned an empty version") + } + + if err := f.Stack().Down(ctx); err != nil { + t.Fatalf("Down: %v", err) + } + post, err := f.Stack().Health(ctx) + if err != nil { + t.Fatalf("Health after Down: %v", err) + } + if post.State == stack.StatusReady { + t.Fatalf("stack still Ready after Down (%s)", post.Detail) + } + // A clean Down reaps the postgres group and drops the teardown record. + if err := syscall.Kill(-pgid, 0); !errors.Is(err, syscall.ESRCH) { + t.Fatalf("postgres group %d after Down: kill(0) = %v, want ESRCH", pgid, err) + } + if _, err := os.Stat(filepath.Join(f.StateDir(), "stack.pgids")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("pgid record after a clean Down: stat = %v, want not-exist", err) + } +} + +// fatalRecorder is a testing.TB whose Fatalf records the call and stops the +// goroutine, so a test can observe a helper's fatal without failing itself. +type fatalRecorder struct { + testing.TB + fataled bool +} + +func (r *fatalRecorder) Helper() {} + +func (r *fatalRecorder) Fatalf(string, ...any) { + r.fataled = true + runtime.Goexit() +} + +// TestWithEmbeddedPostgresRejectsExternalDSN has no podmanUsable skip, so it runs +// even where the stack cannot: it checks only the option guard, which must +// refuse a fixture asked for both postgres arms. +func TestWithEmbeddedPostgresRejectsExternalDSN(t *testing.T) { + for name, opts := range map[string][]fixtureOption{ + "conflict": {WithEmbeddedPostgres(), withExternalDatabase("host=/tmp dbname=x")}, + "embedded only": {WithEmbeddedPostgres()}, + } { + t.Run(name, func(t *testing.T) { + rec := &fatalRecorder{TB: t} + done := make(chan struct{}) + go func() { + defer close(done) + buildFixtureConfig(rec, opts) + }() + <-done + if want := name == "conflict"; rec.fataled != want { + t.Fatalf("buildFixtureConfig fataled = %v, want %v", rec.fataled, want) + } + }) + } +} + +// recordedPostgresPgid reads the postgres process-group id from the stack's +// pgid record ("proc postgres " in the v2 format). +func recordedPostgresPgid(t *testing.T, stateDir string) int { + t.Helper() + path := filepath.Join(stateDir, "stack.pgids") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read pgid record: %v", err) + } + for _, line := range strings.Split(string(raw), "\n")[1:] { + f := strings.Fields(line) + if len(f) == 4 && f[0] == "proc" && f[1] == "postgres" { + pgid, err := strconv.Atoi(f[2]) + if err != nil || pgid <= 1 { + t.Fatalf("pgid record postgres entry %q has a bad pgid", line) + } + return pgid + } + } + t.Fatalf("pgid record %q has no proc postgres entry; the stack did not spawn its own postgres:\n%s", path, raw) + return 0 +} diff --git a/go/e2e/fixture.go b/go/e2e/fixture.go index d1693bdf4..f79e05483 100644 --- a/go/e2e/fixture.go +++ b/go/e2e/fixture.go @@ -136,6 +136,8 @@ type Fixture struct { // forwarded to the runner as --runtime-dir. Exposed so a process-table // assertion can scope its match to this fixture's own runner. runtimeDir string + // stateDir holds the stack's TLS anchor, postgres data and pgid record. + stateDir string // stub is the canned model backend when the fixture was built with // WithCannedModel, else nil. Its lifecycle rides a t.Cleanup registered at // startup, so a consumer never closes it directly. @@ -183,6 +185,8 @@ type fixtureConfig struct { // externalDSN, when set, points the stack at an already-running postgres // (ExternalDatabase) instead of spawning its own. externalDSN string + // embeddedPostgres pins the stack-owned postgres arm (WithEmbeddedPostgres). + embeddedPostgres bool } // fixtureOption mutates a fixtureConfig. Variadic options keep NewFixture's @@ -297,6 +301,13 @@ func withExternalDatabase(dsn string) fixtureOption { return func(fc *fixtureConfig) { fc.externalDSN = dsn } } +// WithEmbeddedPostgres states that a leg needs the stack-owned postgres. Plain +// NewFixture already takes that wrapper-process arm; this option sets nothing on +// stack.Config and only fails the test if an external DSN is also set. +func WithEmbeddedPostgres() fixtureOption { + return func(fc *fixtureConfig) { fc.embeddedPostgres = true } +} + // WithForgeStub makes NewFixture stand up the deterministic forge stub backend. func WithForgeStub() fixtureOption { return func(fc *fixtureConfig) { fc.forge = true } @@ -318,10 +329,7 @@ func WithForgeStub() fixtureOption { func NewFixture(ctx context.Context, tb testing.TB, opts ...fixtureOption) *Fixture { tb.Helper() - var fc fixtureConfig - for _, opt := range opts { - opt(&fc) - } + fc := buildFixtureConfig(tb, opts) // The three stack child binaries are built once per run by TestMain and // exported on PATH there (see main_test.go); the ProcessSupervisor resolves // each Component to a bare binary name via exec.LookPath against that entry. @@ -459,6 +467,7 @@ func NewFixture(ctx context.Context, tb testing.TB, opts ...fixtureOption) *Fixt serverURL: serverURL, adminToken: adminToken, runtimeDir: runtimeDir, + stateDir: stateDir, stub: stub, forgeStub: forgeStub, agentModel: cfg.AgentModel, @@ -500,6 +509,19 @@ func fixtureDSN(fc fixtureConfig, pgSockDir string, pgPort int) string { return "host=" + pgSockDir + " port=" + strconv.Itoa(pgPort) + " dbname=compass sslmode=disable" } +// buildFixtureConfig applies opts and rejects a contradictory set. +func buildFixtureConfig(tb testing.TB, opts []fixtureOption) fixtureConfig { + tb.Helper() + var fc fixtureConfig + for _, opt := range opts { + opt(&fc) + } + if fc.embeddedPostgres && fc.externalDSN != "" { + tb.Fatalf("NewFixture: WithEmbeddedPostgres conflicts with an external database DSN") + } + return fc +} + func acquireFixtureRoot(tb testing.TB, ctx context.Context, fc fixtureConfig) (root, stateDir string, pgPort int) { tb.Helper() // Acquire the root/stateDir/ports either fresh (the default ephemeral @@ -581,6 +603,9 @@ func (f *Fixture) ForgeStub() *forgeStub { return f.forgeStub } // child processes rather than matching unrelated host processes. func (f *Fixture) RuntimeDir() string { return f.runtimeDir } +// StateDir is the stack's state dir: TLS anchor, postgres data and pgid record. +func (f *Fixture) StateDir() string { return f.stateDir } + // AsObserver mints a NON-ADMIN bearer for an existing account and returns the // two Connect clients scoped to it, so a leg can assert what that account CAN // and CANNOT see over the real TLS door (RIG-3528 T1). Every other fixture RPC diff --git a/go/e2e/harness_test.go b/go/e2e/harness_test.go index bbce8feb4..ac9dac3de 100644 --- a/go/e2e/harness_test.go +++ b/go/e2e/harness_test.go @@ -162,7 +162,7 @@ func assertRunnerHasConfiguredFlags(t *testing.T, f *Fixture) { // binds, so the data dir is a lasting trace even if the process has exited. func assertNoStackPostgres(t *testing.T, f *Fixture) { t.Helper() - dataDir := filepath.Join(filepath.Dir(f.caPath), "postgres") + dataDir := filepath.Join(f.StateDir(), "postgres") if _, err := os.Stat(dataDir); !errors.Is(err, fs.ErrNotExist) { t.Fatalf("the shared stack ran its own postgres: %s exists (stat err %v)", dataDir, err) } From 9a9f6ccecfac553bb0ab676f3e558f6387158d20 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 20:42:57 -0400 Subject: [PATCH 2/3] fix(e2e): clear inherited forge env for an own stack without the forge stub (RIG-2906) The shared stack sets the forge env for the whole test process. A later own-stack NewFixture without WithForgeStub inherited those secret names, so its server declared forge secrets the fixture's secrets file did not hold and failed to boot. NewFixture now blanks that env for the test when the forge stub is off. Co-authored-by: Matt Wilkinson --- go/e2e/fixture.go | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/go/e2e/fixture.go b/go/e2e/fixture.go index f79e05483..58db08db3 100644 --- a/go/e2e/fixture.go +++ b/go/e2e/fixture.go @@ -80,17 +80,7 @@ func configureForgeStub(tb testing.TB, secretsPath string) *forgeStub { // Scrub every ambient knob that could redirect the leg off the loopback // stub. The S3 and OTLP names are env-only fallbacks the server reads // directly, so an operator's shell would otherwise reach a real endpoint. - for _, name := range []string{ - "COMPASS_FORGE_REPOS", "COMPASS_FORGE_HOST", "COMPASS_FORGE_APP_ID", "COMPASS_FORGE_INSTALLATION_ID", - "COMPASS_FORGE_APP_KEY_SECRET", "COMPASS_FORGE_APP_WEBHOOK_SECRET", "COMPASS_FORGE_REVIEWER_APP_ID", - "COMPASS_FORGE_REVIEWER_APP_INSTALLATION_ID", "COMPASS_FORGE_REVIEWER_APP_KEY_SECRET", - "COMPASS_FORGE_LINEAR_CLIENT_ID", "COMPASS_FORGE_LINEAR_CLIENT_SECRET", "COMPASS_FORGE_LINEAR_WEBHOOK_SECRET", - "COMPASS_FORGE_DISABLE_SCOPE_ENFORCEMENT", "COMPASS_SECRET_PROVIDER", "LINEAR_FORGE_CLIENT_ID", "LINEAR_FORGE_CLIENT_SECRET", "LINEAR_FORGE_WEBHOOK_SECRET", - "COMPASS_S3_ENDPOINT", "COMPASS_S3_BUCKET", "COMPASS_S3_ACCESS_KEY", "COMPASS_S3_SECRET_KEY", - "COMPASS_S3_REGION", "COMPASS_S3_USE_TLS", "OTEL_EXPORTER_OTLP_ENDPOINT", - } { - tb.Setenv(name, "") - } + scrubForgeEnv(tb) // Dogfood forge writes use the single-trust-domain posture, with no grant seed. tb.Setenv("COMPASS_FORGE_DISABLE_SCOPE_ENFORCEMENT", "true") for k, v := range map[string]string{"COMPASS_FORGE_HOST": stub.Host(), "COMPASS_FORGE_APP_ID": "1001", "COMPASS_FORGE_INSTALLATION_ID": "1", "COMPASS_FORGE_APP_KEY_SECRET": "FORGE_APP_PRIVATE_KEY", "COMPASS_FORGE_APP_WEBHOOK_SECRET": "FORGE_APP_WEBHOOK_SECRET", "COMPASS_FORGE_REVIEWER_APP_ID": "1002", "COMPASS_FORGE_REVIEWER_APP_INSTALLATION_ID": "2", "COMPASS_FORGE_REVIEWER_APP_KEY_SECRET": "FORGE_REVIEWER_APP_PRIVATE_KEY", "COMPASS_FORGE_CA": stub.CAPath()} { //nolint:gosec // G101 false positive: secret-store key names and stub app IDs, not credentials @@ -99,6 +89,25 @@ func configureForgeStub(tb testing.TB, secretsPath string) *forgeStub { return stub } +// forgeEnvNames are the ambient knobs the server reads for forge, S3 and OTLP. +var forgeEnvNames = []string{ + "COMPASS_FORGE_REPOS", "COMPASS_FORGE_HOST", "COMPASS_FORGE_APP_ID", "COMPASS_FORGE_INSTALLATION_ID", + "COMPASS_FORGE_APP_KEY_SECRET", "COMPASS_FORGE_APP_WEBHOOK_SECRET", "COMPASS_FORGE_REVIEWER_APP_ID", + "COMPASS_FORGE_REVIEWER_APP_INSTALLATION_ID", "COMPASS_FORGE_REVIEWER_APP_KEY_SECRET", + "COMPASS_FORGE_LINEAR_CLIENT_ID", "COMPASS_FORGE_LINEAR_CLIENT_SECRET", "COMPASS_FORGE_LINEAR_WEBHOOK_SECRET", + "COMPASS_FORGE_DISABLE_SCOPE_ENFORCEMENT", "COMPASS_SECRET_PROVIDER", "LINEAR_FORGE_CLIENT_ID", "LINEAR_FORGE_CLIENT_SECRET", "LINEAR_FORGE_WEBHOOK_SECRET", + "COMPASS_S3_ENDPOINT", "COMPASS_S3_BUCKET", "COMPASS_S3_ACCESS_KEY", "COMPASS_S3_SECRET_KEY", + "COMPASS_S3_REGION", "COMPASS_S3_USE_TLS", "OTEL_EXPORTER_OTLP_ENDPOINT", +} + +// scrubForgeEnv blanks forgeEnvNames for tb's lifetime. +func scrubForgeEnv(tb testing.TB) { + tb.Helper() + for _, name := range forgeEnvNames { + tb.Setenv(name, "") + } +} + // agentImage is the REAL agent image the dogfood stack runs — present in the // local containers-storage on the dev/CI box, never a public stand-in. The // runner refuses to boot without it present; the stack pulls/validates it but @@ -350,6 +359,10 @@ func NewFixture(ctx context.Context, tb testing.TB, opts ...fixtureOption) *Fixt var forgeStub *forgeStub if fc.forge { forgeStub = configureForgeStub(tb, secretsPath) + } else { + // The shared stack sets the forge env process-wide for the whole run, and + // a server that inherits it declares forge secrets this file lacks. + scrubForgeEnv(tb) } var garage *garageFixture From 7bca1ad22f1a5ea1eda0885626c0d75225982436 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 20:46:33 -0400 Subject: [PATCH 3/3] fix(e2e): scrub the forge CA and scope-grant env too (RIG-2906) Co-authored-by: Matt Wilkinson --- go/e2e/fixture.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/e2e/fixture.go b/go/e2e/fixture.go index 58db08db3..c15fd9e29 100644 --- a/go/e2e/fixture.go +++ b/go/e2e/fixture.go @@ -95,7 +95,7 @@ var forgeEnvNames = []string{ "COMPASS_FORGE_APP_KEY_SECRET", "COMPASS_FORGE_APP_WEBHOOK_SECRET", "COMPASS_FORGE_REVIEWER_APP_ID", "COMPASS_FORGE_REVIEWER_APP_INSTALLATION_ID", "COMPASS_FORGE_REVIEWER_APP_KEY_SECRET", "COMPASS_FORGE_LINEAR_CLIENT_ID", "COMPASS_FORGE_LINEAR_CLIENT_SECRET", "COMPASS_FORGE_LINEAR_WEBHOOK_SECRET", - "COMPASS_FORGE_DISABLE_SCOPE_ENFORCEMENT", "COMPASS_SECRET_PROVIDER", "LINEAR_FORGE_CLIENT_ID", "LINEAR_FORGE_CLIENT_SECRET", "LINEAR_FORGE_WEBHOOK_SECRET", + "COMPASS_FORGE_DISABLE_SCOPE_ENFORCEMENT", "COMPASS_FORGE_SCOPE_GRANTS", "COMPASS_FORGE_CA", "COMPASS_SECRET_PROVIDER", "LINEAR_FORGE_CLIENT_ID", "LINEAR_FORGE_CLIENT_SECRET", "LINEAR_FORGE_WEBHOOK_SECRET", "COMPASS_S3_ENDPOINT", "COMPASS_S3_BUCKET", "COMPASS_S3_ACCESS_KEY", "COMPASS_S3_SECRET_KEY", "COMPASS_S3_REGION", "COMPASS_S3_USE_TLS", "OTEL_EXPORTER_OTLP_ENDPOINT", }