Problem
All dispatched agents are asyncio tasks running inside the single agentception-app container. When an agent runs npm run test:e2e, its Playwright tests hit the live server on localhost:1337 — the same process that is simultaneously serving other agents' requests and managing their runs.
When two agents run E2E tests concurrently:
- Both test suites mutate the same application state
- One agent's test setup (seeded data, triggered dispatches, SSE subscriptions) can corrupt another agent's assertions
- Failures are non-reproducible and hard to attribute to the correct agent's change
Proposed solution
Spin up a lightweight, isolated test server per worktree at E2E test time: a second uvicorn process bound to a dynamically allocated port, running in a clean state against a test-scoped database, reachable only for the duration of that agent's test run.
The canonical AgentCeption server on :1337 is never targeted by agent E2E tests.
Architecture
agentception-app container
uvicorn :1337 → live orchestrator (persistent)
uvicorn :dynamic_port_A → test server for issue-123 (spawned at test start, torn down after)
uvicorn :dynamic_port_B → test server for issue-456
Implementation steps
-
Port registry (agentception/services/port_registry.py)
- Async in-memory registry backed by a Postgres table (
ac_worktree_ports)
allocate(run_id) -> int: atomically reserves an unused port in a configurable range (default 50000–60000)
release(run_id): marks the port free
- Config vars:
AC_WORKTREE_PORT_RANGE_START, AC_WORKTREE_PORT_RANGE_END
-
Test server lifecycle (agentception/services/test_server.py)
start_test_server(run_id, port) -> asyncio.subprocess.Process: spawns uvicorn agentception.main:app --port {port} --env AC_WORKER_MODE=true
AC_WORKER_MODE=true disables the agent loop and the worktree reaper, leaving only the HTTP API layer for E2E target
stop_test_server(run_id): kills the spawned process and calls port_registry.release(run_id)
-
E2E test integration
playwright.config.ts: read AC_BASE_URL from env; default to http://localhost:1337
- The agent's worktree env gets
AC_BASE_URL=http://localhost:{dynamic_port} injected at dispatch time so npm run test:e2e targets the correct test server
-
Teardown (agentception/services/teardown.py, agentception/services/worktree_reaper.py)
stop_test_server + port_registry.release called in both normal teardown and reaper cleanup
-
DB migration
dynamic_port: int | None column on ac_agent_runs
ac_worktree_ports(run_id TEXT PK, port INT UNIQUE, allocated_at TIMESTAMPTZ) table
-
Config additions (agentception/config.py)
AC_WORKTREE_PORT_RANGE_START: int = 50000
AC_WORKTREE_PORT_RANGE_END: int = 60000
AC_WORKER_MODE: bool = False
Acceptance criteria
Notes
Problem
All dispatched agents are
asynciotasks running inside the singleagentception-appcontainer. When an agent runsnpm run test:e2e, its Playwright tests hit the live server onlocalhost:1337— the same process that is simultaneously serving other agents' requests and managing their runs.When two agents run E2E tests concurrently:
Proposed solution
Spin up a lightweight, isolated test server per worktree at E2E test time: a second
uvicornprocess bound to a dynamically allocated port, running in a clean state against a test-scoped database, reachable only for the duration of that agent's test run.The canonical AgentCeption server on
:1337is never targeted by agent E2E tests.Architecture
Implementation steps
Port registry (
agentception/services/port_registry.py)ac_worktree_ports)allocate(run_id) -> int: atomically reserves an unused port in a configurable range (default50000–60000)release(run_id): marks the port freeAC_WORKTREE_PORT_RANGE_START,AC_WORKTREE_PORT_RANGE_ENDTest server lifecycle (
agentception/services/test_server.py)start_test_server(run_id, port) -> asyncio.subprocess.Process: spawnsuvicorn agentception.main:app --port {port} --env AC_WORKER_MODE=trueAC_WORKER_MODE=truedisables the agent loop and the worktree reaper, leaving only the HTTP API layer for E2E targetstop_test_server(run_id): kills the spawned process and callsport_registry.release(run_id)E2E test integration
playwright.config.ts: readAC_BASE_URLfrom env; default tohttp://localhost:1337AC_BASE_URL=http://localhost:{dynamic_port}injected at dispatch time sonpm run test:e2etargets the correct test serverTeardown (
agentception/services/teardown.py,agentception/services/worktree_reaper.py)stop_test_server+port_registry.releasecalled in both normal teardown and reaper cleanupDB migration
dynamic_port: int | Nonecolumn onac_agent_runsac_worktree_ports(run_id TEXT PK, port INT UNIQUE, allocated_at TIMESTAMPTZ)tableConfig additions (
agentception/config.py)AC_WORKTREE_PORT_RANGE_START: int = 50000AC_WORKTREE_PORT_RANGE_END: int = 60000AC_WORKER_MODE: bool = FalseAcceptance criteria
npm run test:e2econcurrently each target a distinct port:1337server is not targeted by any agent E2E testmypyclean, zeroAny, unit + integration testsNotes
AC_WORKER_MODE=trueservers share the same Postgres but in practice do not conflict — each agent's test fixtures are keyed by the agent'srun_id.