Tests and CI
An e2e suite needs the same stack you develop against: started, healthy, and gone when the suite ends. stackctl gives it one in a single call, in a mode of its own so it never touches the session you are working in.
A test mode
Create .env.test next to .env.development, with different ports:
# .env.test
API_PORT=21060
DB_PORT=21061--mode test then runs a second, separate session: its own processes, its own infra containers and database, its own state. See Modes.
In a test runner
Call startStack() from the suite's global setup. It starts a headless session and resolves once the infra and every service are healthy:
import { startStack } from 'stackctl';
// in the global setup
const stack = await startStack({ mode: 'test', services: ['web'] });
// hand stack.origins.web to the specs
// in the global teardown
await stack.stop();- The session ends with the process that started it, however that ends:
stop(), a runner that exits, akill -9. Nothing is left running. - It fails rather than share. If a
testsession is already running, the call rejects, naming its pid. PasskillOthers: trueto replace it. - On failure it rejects with the reason and the log's path, after stopping the session:
not healthy after 300s: worker.
stack.origins has every service's URL, and stack.control is a control API client for restarting a service mid-suite. See startStack().
In CI
stackctl start --detach starts the stack, waits until it is healthy, prints one line and exits, leaving the session running for the next steps:
- run: pnpm stackctl start --detach -s web
- run: pnpm test:e2e
- if: always()
run: pnpm stackctl stopIn CI the mode defaults to test, so both commands act on the test session. If the stack doesn't come up, --detach stops it, prints the error and the log's last 50 lines, and exits 1. See stackctl start.
A suite that calls startStack() itself needs neither step.
From a shell script
--headless runs a session in the foreground with no UI, streaming its log to stdout. Wait for it on the control API's event stream:
stackctl --mode test --headless -s web &
session=$!
trap 'kill $session 2>/dev/null' EXIT # SIGTERM shuts it down gracefully
record=node_modules/.stackctl/modes/test/running.json
until [ -f "$record" ]; do sleep 0.2; done
port=$(node --input-type=module -e "import r from './$record' with { type: 'json' }; console.log(r.port)")
curl -sN "http://127.0.0.1:$port/events" \
| grep -m1 -qE '"sessionReady":true|"type":"session","ready":true' || exit 1
pnpm test:e2eOne-off test commands
For a suite that needs the environment but not the services, run it through sctl run:
sctl --mode test run api test