# Test before pushing to production

Multi-layer testing strategy for qc-trace. Run all layers before merging to main or releasing.

## Input

The user may provide a scope like `/test all`, `/test unit`, `/test e2e`, or `/test <file>`.
If no argument: run all layers in order.

## Layer 1: Unit tests (pytest)

Fast, no external dependencies. Always run first.

```bash
uv run pytest tests/ -q
```

**STOP if tests fail.** Fix before proceeding.

This covers:
- Schema transforms (Claude, Codex, Gemini, Cursor)
- Daemon collector, config, state, watcher, pusher
- Reconciliation logic
- DB reader/writer (mocked)
- Ingest server handlers (mocked)

## Layer 2: Integration tests with dummy HTTP server

Tests that spin up a real `http.server.HTTPServer` in-process to verify actual HTTP push/pull behavior. These live in the test suite (e.g., `test_daemon_pusher.py`, `test_backfill.py`).

Already covered by Layer 1 (`pytest`), but call them out explicitly if the user asks for integration-only:

```bash
uv run pytest tests/test_daemon_pusher.py tests/test_backfill.py tests/test_heartbeat.py -v
```

## Layer 3: End-to-end against real local server

Requires Docker (Postgres) and the real ingest server. This is the critical layer that catches issues mocks can't — real DB writes, COALESCE upserts, schema migrations, auth.

### 3a. Start infrastructure

```bash
docker compose up -d postgres
sleep 3
pg_isready -h localhost -U qc_trace -d qc_trace
```

### 3b. Start the server on a TEST port (not 19777 — that's the user's real daemon)

```bash
# Use port 19888 for testing to avoid conflicts with the running daemon
export QC_TRACE_DSN="postgresql://qc_trace:qc_trace_dev@localhost:5432/qc_trace"
export QC_TRACE_HOST="127.0.0.1"
export QC_TRACE_PORT="19888"
python -m qc_trace.server.app
```

Run this in background. Verify with:

```bash
curl -s http://127.0.0.1:19888/health
# Should return: {"status": "ok", "db": "connected"}
```

### 3c. Test the feature end-to-end

Write an inline Python script that:

1. Creates temp JSONL files with known session data
2. Configures a `DaemonConfig` pointing at `http://127.0.0.1:19888/ingest`
3. Runs the feature code (e.g., `daemon._backfill_session_metadata()`)
4. Queries the server API to verify data landed in the DB correctly
5. Tests with BOTH mocked data AND real data (e.g., real `resolve_repo()` against the current repo)

Example pattern:

```python
timeout 20 python -c "
import json, tempfile
from pathlib import Path
from qc_trace.daemon.config import DaemonConfig
from qc_trace.daemon.main import Daemon
from qc_trace.daemon.state import FileState

with tempfile.TemporaryDirectory() as tmpdir:
    # Create test data, configure daemon, run feature, verify via API
    ...
"
```

### 3d. Verify data in DB

Query the server API or DB directly to confirm:

```bash
# Via API
curl -s 'http://127.0.0.1:19888/api/sessions?session_id=<test-id>' | python -m json.tool

# Or via psql
PGPASSWORD=qc_trace_dev psql -h localhost -U qc_trace -d qc_trace -c "SELECT id, git_branch, repo_name FROM sessions WHERE id LIKE 'e2e-%';"
```

### 3e. Clean up test data

Always clean up after e2e tests:

```bash
PGPASSWORD=qc_trace_dev psql -h localhost -U qc_trace -d qc_trace -c "DELETE FROM messages WHERE session_id LIKE 'e2e-%'; DELETE FROM sessions WHERE id LIKE 'e2e-%';"
```

Then stop the test server (but leave the user's real daemon alone).

## Layer 4: Test against real session files

If the feature touches collection/parsing, test against real files on disk:

```python
from qc_trace.daemon.collector import extract_session_meta
from pathlib import Path

# Test against actual Claude Code sessions
for f in list(Path.home().glob(".claude/projects/**/*.jsonl"))[:3]:
    session_id, cwd = extract_session_meta(str(f), "claude_code")
    print(f"{f.name}: session={session_id}, cwd={cwd}")
```

This catches edge cases that synthetic test data misses (encoding issues, unexpected fields, large files).

## Layer 5: CLI smoke test

If the feature adds or modifies a CLI command:

```bash
quickcall status
quickcall <new-command> --help
# or run the command handler directly:
python -c "import argparse; from qc_trace.cli.traced import cmd_<name>; cmd_<name>(argparse.Namespace())"
```

## Important rules

- **NEVER skip Layer 1.** It's fast and catches most regressions.
- **NEVER test e2e on port 19777** — that's the user's real running daemon. Use 19888.
- **ALWAYS clean up test data** from the DB after e2e tests.
- **ALWAYS stop the test server** when done. Don't leave orphan processes.
- **NEVER push if any layer fails.**
- If Docker isn't running, ask the user to start it before running Layer 3.

### CRITICAL: Never kill the user's real daemon

The user's production daemon runs via launchd (`com.quickcall.traced`). It MUST NOT be killed, stopped, or interfered with during testing.

**When killing test server processes, ONLY kill by the exact PID you started or by the test port (19888). NEVER use broad patterns like:**
- `pkill -f qc_trace` — this kills the real daemon too
- `pkill -f quickcall` — same problem
- `kill $(lsof -ti :19777)` — this is the real daemon's port
- `killall python` — nuclear option, never do this

**Safe ways to stop the test server:**
```bash
# Option 1: Kill by PID (best — you started it, you know the PID)
kill $TEST_SERVER_PID

# Option 2: Kill by test port only
lsof -ti :19888 | xargs kill 2>/dev/null

# Option 3: Kill by specific working directory (if running from worktree)
pgrep -f "qc_trace.server.app.*19888" | xargs kill 2>/dev/null
```

**Before Layer 3 starts**, record the real daemon status:
```bash
DAEMON_WAS_RUNNING=$(launchctl print gui/$(id -u)/com.quickcall.traced 2>/dev/null && echo "yes" || echo "no")
```

**After Layer 3 ends (success or failure)**, verify the real daemon is still alive:
```bash
if ! launchctl print gui/$(id -u)/com.quickcall.traced >/dev/null 2>&1; then
  echo "WARNING: Real daemon was killed during testing! Restarting..."
  launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.quickcall.traced.plist
fi
```

**This check is MANDATORY — run it even if tests fail or you hit errors. The user's daemon being down means production data stops flowing.**
