# Test Suite Review - qc-trace

**Date:** 2026-02-07
**Scope:** All test files under `/Users/sagar/work/all-things-quickcall/trace/tests/`
**Total test files:** 12 (plus `__init__.py` and 7 fixture files)

---

## Critical

### C1. Tests depend on developer's local filesystem and will fail in CI

**Files:**
- `tests/test_transforms.py:451-484` (`TestClaudeCodeRealSession`)
- `tests/test_transforms.py:487-528` (`TestGeminiCliRealSession`)
- `tests/test_cursor_parser.py:164-209` (`TestRealCursorData`)
- `tests/test_cursor_transform.py:245-279` (`TestCursorRealSession`)
- `tests/test_repo_resolver.py:86-93` (`TestResolveRepo.test_real_git_repo`)

These tests depend on the existence of specific files on the developer's machine (e.g., `~/.claude/projects/`, `~/.cursor/projects/`, `~/.gemini/tmp/`). While some use `pytest.skip`, others make assumptions about local state.

`test_repo_resolver.py:86-93` is the worst offender -- `test_real_git_repo` calls `resolve_repo("/home/sagar/trace")` which is a hardcoded path on the developer's machine. This will either fail or skip on any other machine, and the path `/home/sagar/trace` suggests a Linux path while the current machine is macOS.

`test_transforms.py:496-497` hardcodes a specific Gemini session file path:
```python
real_session = Path(
    os.path.expanduser(
        "~/.gemini/tmp/1e01272821378e1687d1411198dfc72cf42d5b784d1f332393cc02a6e6df260f/chats/session-2026-02-04T11-11-3e72f9b9.json"
    )
)
```
This references a specific session hash and timestamp. Unlike the Claude test which uses glob discovery, this will only ever work on one specific machine at one specific point in time.

### C2. No tests for `Daemon.run()`, `_poll_cycle()`, or the main daemon loop

**Source file:** `qc_trace/daemon/main.py:37-59` (run), `qc_trace/daemon/main.py:108-149` (_poll_cycle)

The `Daemon` class is the core orchestrator that ties together watcher, collector, pusher, and state management. The `_poll_cycle()` method contains critical logic (error handling, retry count increment, batch pushing with backoff-aware sleeping, shutdown checks). None of this is tested.

The only `Daemon` test coverage is the `_reconcile()` method (in `test_daemon_reconcile.py`), which tests a narrow slice. The full cycle -- discovering changed files, collecting messages, pushing them via HTTP, saving state, and handling errors with retry increments -- has zero automated test coverage.

### C3. No tests for `db.reader` module (5 query functions, 0 tests)

**Source file:** `qc_trace/db/reader.py` (152 lines, 5 functions)

Functions `get_stats`, `get_sessions`, `get_messages`, `get_file_sync_state`, and `get_feed` are entirely untested. These are the dashboard API backend queries. The server handler tests (`test_ingest_server.py`) test the handler wrappers (`handle_api_stats`, `handle_api_sessions`, etc.) but only `handle_api_sync` and `handle_health` have actual tests -- the remaining handlers (`handle_api_stats`, `handle_api_sessions`, `handle_api_messages`, `handle_api_feed`) have no test coverage at all.

### C4. No tests for `db.connection` module

**Source file:** `qc_trace/db/connection.py` (59 lines)

The `ConnectionPool` class wraps `psycopg_pool.AsyncConnectionPool` and has `open()`, `close()`, async context manager support, and DSN resolution from environment variables. None of this is tested, not even the DSN default/env-var logic.

### C5. No tests for `db.migrations` module

**Source file:** `qc_trace/db/migrations.py` (51 lines)

`ensure_schema()` and `get_schema_version()` have no dedicated tests. The `test_db_writer.py` calls `ensure_schema` indirectly in its setup fixture, but there are no tests for version checking logic, idempotent re-application, or the migration path from version 1 to version 2.

---

## High

### H1. `test_daemon_reconcile.py` uses hardcoded ports, creating flaky test behavior

**File:** `tests/test_daemon_reconcile.py:30-50`, lines 86, 110, 139, etc.

Every reconcile test spins up an HTTP server on a hardcoded port (18881-18887). If any of these ports are in use (or if tests run in parallel), the tests will fail with `OSError: [Errno 48] Address already in use`. The `_make_sync_server` helper should use port `0` (random available port) as `test_daemon_pusher.py:123` correctly does with `HTTPServer(("127.0.0.1", 0), Handler)`.

### H2. No tests for `utils/schema_extractor.py` (467 lines, 0 test coverage)

**Source file:** `qc_trace/utils/schema_extractor.py`

This is the largest untested module in the codebase. It contains `extract_schema`, `extract_jsonl_schema`, `extract_json_schema`, `FieldInfo`, `SchemaStats`, `print_schema_tree`, `schema_to_dict`, and a CLI `main()`. It is a utility that analyzes JSON/JSONL files to infer their schema. While it may be considered a development tool, it is a full module in the package with complex logic (recursive schema merging, array item inference, type grouping).

### H3. No tests for `utils/sqlite.py` (391 lines, 0 test coverage)

**Source file:** `qc_trace/utils/sqlite.py`

Contains 8 public functions for reading Cursor's SQLite databases (`read_vscdb_item_table`, `read_vscdb_cursor_kv`, `read_sqlite_table`, `find_workspace_db_for_folder`, `get_global_state_db_path`, `get_ai_tracking_db_path`, `list_workspace_folders`, `_compute_workspace_hash`). These deal with real filesystem paths, SQLite connections, and URI hashing logic. Zero test coverage. The `_compute_workspace_hash` function (MD5 of lowercase URI) would be trivial and valuable to test.

### H4. Server handler tests for dashboard endpoints are missing

**Source file:** `qc_trace/server/handlers.py:246-328`

The following handler functions have no tests:
- `handle_api_stats` (line 246)
- `handle_api_sessions` (line 260)
- `handle_api_messages` (line 279)
- `handle_api_feed` (line 312)
- `_parse_qs` (line 226)
- `_serialize_row` (line 235)

These are live API endpoints. The ingest and health handlers have good coverage, but the dashboard read endpoints were added later and were never tested.

### H5. `test_ingest_server.py` uses `asyncio.new_event_loop()` antipattern throughout

**File:** `tests/test_ingest_server.py` (throughout all test classes)

Every test manually creates an event loop with `asyncio.new_event_loop()`, runs coroutines with `loop.run_until_complete()`, and manually closes the loop in a `finally` block. This is verbose, error-prone, and ignores the `asyncio_mode = "auto"` configuration in `pyproject.toml:32`. These tests should use `async def` test methods and let pytest-asyncio manage the loop. The pattern is repeated approximately 20 times in the file.

Compare with `test_db_writer.py` which correctly uses `async def test_*` methods.

### H6. `test_daemon_reconcile.py` -- the reconcile feature IS still in the codebase and tests are valid

**Analysis:** The reconcile logic lives in `qc_trace/daemon/main.py:61-97` (`Daemon._reconcile`). It is called on daemon startup (`run()` at line 42) before entering the poll loop. The feature compares local file-processing state against the ingest server's `/api/sync` endpoint and rewinds or resets local state when the server has fewer messages than the daemon thinks it has pushed. This handles cases like server database resets or data loss.

The tests in `test_daemon_reconcile.py` are well-structured and cover the key scenarios (server unreachable, JSONL rewind, hash-based reset, mixed sources). However, they have the hardcoded port issue noted in H1.

---

## Medium

### M1. No tests for `DaemonConfig` defaults and env var resolution

**Source file:** `qc_trace/daemon/config.py`

`DaemonConfig` reads `QC_TRACE_INGEST_URL` from environment variables (line 16). The default values for paths, intervals, glob patterns, and derived properties (`state_file`, `pid_file`) are never tested. A test that constructs a `DaemonConfig` and verifies its defaults would catch regressions if the config structure changes.

### M2. `test_daemon_pusher.py` does not test retry/drain ordering semantics

**File:** `tests/test_daemon_pusher.py`

The `test_drains_queue_on_success` test (line 141) verifies that queued messages are drained after a successful push, but does not verify the order of delivery (new messages sent first, then queued messages). It also does not test the scenario where draining the queue itself fails.

### M3. `test_daemon_watcher.py` does not test windsurf source patterns

**File:** `tests/test_daemon_watcher.py:17-39`

The `tmp_home` fixture creates directories for claude_code, codex_cli, gemini_cli, and cursor. But if windsurf or other new sources are added to `DaemonConfig`, the tests would not catch missing discovery. This is a minor coverage gap but worth noting for future-proofing.

### M4. Duplicate `_make_message` / `_sample_message_dict` helper patterns across test files

**Files:**
- `tests/test_ingest_server.py:36-53` (`_sample_message_dict`)
- `tests/test_daemon_pusher.py:15-27` (`_make_message`)
- `tests/test_db_writer.py:55-76` (`_make_message`)

Three different test files each define their own helper to construct `NormalizedMessage` instances. These share roughly the same fields but use slightly different APIs (dict vs dataclass, different defaults). A shared `conftest.py` fixture or helper module would reduce duplication and ensure consistency.

### M5. `test_repo_resolver.py:85-93` assumes specific git repo exists at hardcoded path

**File:** `tests/test_repo_resolver.py:86-93`

```python
def test_real_git_repo(self):
    info = resolve_repo("/home/sagar/trace")
    assert isinstance(info, RepoInfo)
    assert info.cwd == "/home/sagar/trace"
    assert info.repo_url is not None
```

This hardcodes `/home/sagar/trace` (a Linux path). On this macOS machine, this path likely does not exist, so the assertions about `repo_url is not None` would fail. Unlike other "real data" tests, this one does NOT have a `pytest.skip` guard. It will either silently pass (returning `None` for all git fields) or fail depending on the environment, making the test assertions unreliable.

### M6. Schema definition files (`v1.py`) have no tests

**Source files:**
- `qc_trace/schemas/claude_code/v1.py`
- `qc_trace/schemas/codex_cli/v1.py`
- `qc_trace/schemas/gemini_cli/v1.py`
- `qc_trace/schemas/cursor/v1.py`

These contain TypedDict definitions. While TypedDicts are not directly testable as runtime validators, the test suite never validates that fixture data conforms to these types. A property-based or schema-validation test that checks fixture files against the TypedDict definitions would catch schema drift.

### M7. `test_ingest_server.py` HTTP integration tests use hardcoded ports

**File:** `tests/test_ingest_server.py:597-672`

The `TestHTTPIntegration` class uses hardcoded ports 17771, 17772, 17773. Same issue as H1 -- these will fail if the ports are in use. Use `port=0` and read the assigned port from `server.server_address[1]`.

### M8. No negative test for unknown source type in `collect_file`

**Source:** `qc_trace/daemon/collector.py:48-55`

`collect_file` uses a dict lookup `collectors[changed.source]` without error handling. If an unknown source is passed, it will raise a `KeyError` with no useful error message. No test verifies this failure mode.

---

## Low

### L1. `test_transforms.py:453` imports `glob` but never uses it

**File:** `tests/test_transforms.py:453`

```python
import glob
```

The `TestClaudeCodeRealSession.test_can_parse_real_session_with_thinking` method imports `glob` at line 453 but uses `Path.glob()` at line 458. The `glob` import is dead code.

### L2. `test_daemon_collector.py` fixture `changed` imports `os` inside the method body

**File:** `tests/test_daemon_collector.py:26-34` (and repeated at lines 66, 98, 134, 162, 170)

```python
@pytest.fixture
def changed(self, claude_file: str) -> ChangedFile:
    import os
    stat = os.stat(claude_file)
```

The `os` module is imported at the function level six times. It should be imported at the module level. This is a style issue, not a correctness issue.

### L3. `test_repo_resolver.py:104-109` tests are trivial

**File:** `tests/test_repo_resolver.py:104-109`

```python
class TestResolveGlobalIdentity:
    def test_returns_tuple(self):
        email, name = resolve_global_identity()
        assert email is None or isinstance(email, str)
        assert name is None or isinstance(name, str)
```

This test only verifies the return type, not any actual behavior. It will pass on any machine regardless of git configuration. A more useful test would mock `subprocess.run` to verify that `git config --global user.email` is being called correctly.

### L4. `test_cursor_parser.py:156-162` `TestParseMcpServerMetadata` only tests nonexistent file

**File:** `tests/test_cursor_parser.py:156-162`

The `TestParseMcpServerMetadata` class has exactly one test: `test_nonexistent_file`. Unlike the sibling `TestParseMcpToolDefinition` which has 4 tests including parsing a real fixture, there is no fixture file for MCP server metadata and no positive test case.

### L5. `tests/fixtures/cursor_sqlite_sample.json` is present but unused

**File:** `tests/fixtures/cursor_sqlite_sample.json`

This fixture file exists in the fixtures directory but is not referenced by any test file. It may be a leftover from development or intended for future use.

### L6. `test_db_writer.py` is entirely skipped without PostgreSQL

**File:** `tests/test_db_writer.py:21, 49-52`

The entire `TestBatchWriter` class (10 tests) is gated behind a live PostgreSQL check (`_can_connect()`). This means that in CI without a database, these tests are silently skipped. There should be at least unit tests for `BatchWriter` that mock the connection, so the serialization and query-building logic is tested independently of the database.

### L7. Missing `conftest.py` for shared fixtures

**Observation:** There is no `tests/conftest.py` file. Each test file independently defines its own `FIXTURES_DIR`, message-building helpers, and event loop management. A `conftest.py` would centralize:
- `FIXTURES_DIR` path constant
- `_make_message()` / `_sample_message_dict()` helpers
- Shared pytest fixtures for `DaemonConfig`, `StateManager`, etc.

---

## Summary Table

| Severity | Count | Key Themes |
|----------|-------|------------|
| Critical | 5 | Missing test coverage for core daemon loop, DB reader/connection/migrations, local-path dependencies |
| High | 6 | Hardcoded ports, missing dashboard handler tests, asyncio antipatterns, untested utils (858 lines) |
| Medium | 8 | Config defaults, duplicate helpers, no negative tests, schema type validation |
| Low | 7 | Dead imports, trivial tests, unused fixtures, missing conftest.py |

### Coverage Gap Summary

| Source Module | Lines | Has Tests? | Notes |
|---|---|---|---|
| `daemon/main.py` (run/poll_cycle) | 179 | Partial | Only `_reconcile` tested |
| `daemon/collector.py` | 328 | Yes | Good coverage |
| `daemon/config.py` | 58 | No | Used in tests but not directly tested |
| `daemon/pusher.py` | ~160 | Yes | Good coverage |
| `daemon/state.py` | ~120 | Yes | Good coverage |
| `daemon/watcher.py` | 83 | Yes | Good coverage |
| `daemon/progress.py` | ~100 | Yes | Good coverage |
| `db/connection.py` | 59 | No | Zero coverage |
| `db/migrations.py` | 51 | No | Zero coverage |
| `db/reader.py` | 152 | No | Zero coverage |
| `db/writer.py` | ~200 | Yes (skip) | Requires live PG |
| `server/app.py` | 232 | Partial | Integration tests only |
| `server/batch.py` | 123 | Yes | Good coverage |
| `server/handlers.py` | 328 | Partial | Ingest/health yes; dashboard no |
| `schemas/*/transform.py` | ~800 | Yes | Good coverage |
| `utils/schema_extractor.py` | 467 | No | Zero coverage |
| `utils/sqlite.py` | 391 | No | Zero coverage |
| `utils/cursor_parser.py` | ~250 | Yes | Good coverage |
| `utils/repo_resolver.py` | ~120 | Partial | Some hardcoded paths |

**Estimated untested source lines:** ~1,500 out of ~4,000 total (~37%)
