How the integration test harness works
The internal/testsupport/integrationtest package is the canonical harness for
HoloMUSH full-stack integration tests that need a real in-process holomush
stack (Postgres + embedded NATS JetStream + production CoreServer) rather than a
mocked surface. This page explains how that stack is composed, the conventions
the suites follow, and why. For when and how to use it, see
Integration test harness; for the
helper catalog, see
Integration test harness reference.
Tier vocabulary: “E2E” is reserved for the Playwright browser suite (
task test:e2e). Ginkgo suites run viatask test:intare integration tests — including this harness, which is “full-stack integration”. See the canonical tier table in.claude/rules/testing.md.
Unit tests use a lighter helper
Section titled “Unit tests use a lighter helper”If a test only needs a session.Store — not the whole stack — it should reach
for internal/testsupport/sessiontest.NewStore(t). It returns a store backed by
a fresh database on the shared Postgres testcontainer and deliberately skips the
//go:build integration tag, which makes it the one sanctioned exception to
“anything using SharedPostgres is integration-tagged.” Because task test
runs these tests, the packages that use them — internal/grpc/,
internal/grpc/focus/, internal/command/handlers/, and internal/session/ —
need Docker running locally. (session.Store has a single implementation,
store.PostgresSessionStore, so there is no in-memory fake to test against.)
For a session that carries a PlayerSessionID, seed its foreign-key parents
with sessiontest.NewStoreWithPool(t) + SeedPlayerSession. The full rationale
is in the design spec at
docs/superpowers/specs/2026-05-23-remove-session-memstore-design.md.
Stack composition
Section titled “Stack composition”integrationtest.Start(t) boots:
- Postgres: shared testcontainer (via
test/testutil.SharedPostgres) + per-test database (testutil.FreshDatabase) with all migrations applied. - NATS JetStream: in-memory embedded server via
internal/eventbus/eventbustest.New(t). Per-test isolation; safe for parallel tests. - CoreServer: real production constructor
holoGRPC.NewCoreServer(...)wired withWithAuthService,WithPlayerSessionRepo,WithCharacterRepo,WithCharacterNameResolver,WithSessionStore,WithGuestService,WithSubscriber,WithHistoryReader,WithAccessEngine. Same options production passes — same code paths exercised. - ABAC engine: defaults to
allowAllPolicyEngine(privacy tests focus on session/history gates, not role enforcement). Override viaWithPolicyEngine(eng)when denial-path coverage is needed.
Conventions
Section titled “Conventions”*testing.T propagation
Section titled “*testing.T propagation”Each test package’s *_suite_test.go declares a package-level
var suiteT *testing.T that TestPrivacy / TestPresence / etc. populates.
Ginkgo Describe blocks pass suiteT to integrationtest.Start(suiteT) —
Ginkgo’s GinkgoT() does NOT satisfy *testing.T and cannot be passed
directly.
Cleanup context
Section titled “Cleanup context”BeforeEach derives ctx with context.WithTimeout(context.Background(), 90*time.Second) (cancel is suppressed via //nolint:govet — pre-existing
project convention). AfterEach derives a SEPARATE cleanupCtx from
context.Background() for Logout calls so cleanup can run even if ctx is
about to expire.
Per-test harness
Section titled “Per-test harness”Most Describe blocks construct a fresh Server in BeforeEach and tear
down in AfterEach. This guarantees per-spec isolation (fresh Postgres DB,
fresh embedded NATS) at the cost of ~1-2 seconds setup per spec. Sharing a
single Server across specs is possible but requires manual cleanup between
specs — not a pattern currently used in the codebase.
Direct event injection
Section titled “Direct event injection”Session.EmitDirectEvent is the canonical way to plant events from a test —
the harness’s dispatcher has an empty command registry, so production
SendCommand("pose hello") returns COMMAND_REJECTED. EmitDirectEvent
goes through the same eventbus.Publisher.Publish path production uses,
so JetStream ack + audit semantics match.
How the whole-system tier works
Section titled “How the whole-system tier works”WithInTreePlugins() reuses production’s setup.PluginSubsystem (which calls
Manager.LoadAll) — it does NOT construct plugins.NewManager directly in the
harness (INV-WS-1). The harness assembles a temporary plugins/ overlay (the
source tree’s plugins/ directory + compiled artifacts from build/plugins/),
then calls PluginSubsystem.Start(ctx), which DAG-resolves and loads all
in-tree plugins.
Event emission is NOT wired. WithInTreePlugins does not call
Manager.ConfigureEventEmitter. The whole-system census reads plugin load
state and the command registry only. A plugin command that emits events will
fail with “plugin event emitter is not configured” until a future suite wires
it explicitly.
Real ABAC role semantics
Section titled “Real ABAC role semantics”Under WithRealABAC, the harness seeds the production seed:* policy set
(policy.Bootstrap) and boots the real engine. character_roles are then
evaluated by the engine: ConnectAuthedWithRoles(ctx, name, []string{"admin"})
grants role-based permits (e.g. seed:admin-full-access); a roleless
ConnectAuthed receives only what seed:* grants a roleless character. Tests
that pass under allow-all may see denials under WithRealABAC until they seed
the roles their actions require.