## Test Coverage (happy path)

#### TC-HAP-1.29-1: `cqs serve` data endpoints (`build_graph`, `build_chunk_detail`, `build_hierarchy`, `build_cluster`) never tested with populated data
- **Difficulty:** medium
- **Location:** `src/serve/data.rs:192` (`build_graph`), `:452` (`build_chunk_detail`), `:586` (`build_hierarchy`), `:825` (`build_cluster`), `:933` (`build_stats`). All tests are in `src/serve/tests.rs` and use `fixture_state()` at line 25 which creates an empty init-only store.
- **Description:** The entire `cqs serve` subsystem (new in v1.29.0) has 14 endpoint tests, but every test runs against an empty store. Result: `graph_returns_empty_for_fresh_store` asserts `nodes.len() == 0`, `chunk_detail_unknown_id_returns_404` asserts 404, `hierarchy_unknown_root_returns_404` asserts 404, `cluster_returns_empty_for_fresh_store` asserts `nodes.len() == 0`. The test file comment at line 417-419 (`graph_returns_empty_for_fresh_store`) even admits this: *"Real graph rendering is exercised by manual smoke against the cqs corpus; an in-process test would need a populated fixture (~few hundred LOC of chunk inserts) which is more setup than the shape-check is worth at this stage."* The SQL queries in `build_graph` (cf. `src/serve/data.rs:219-260` correlated subquery for n_callers, edge resolution at `:300-440`), the BFS in `build_hierarchy` (`:620-655`), UMAP-coord lookup in `build_cluster` (`:829-924`), and the caller/callee enrichment in `build_chunk_detail` (`:452-577`) are all untested with actual chunks. A mistake in any of these (e.g., filter-by-file bug, max_nodes clamp off-by-one, direction=callers/callees confusion) ships silently — there is no positive test that asserts "if you index 3 chunks with 2 call edges, `/api/graph` returns 3 nodes and 2 edges".
- **Suggested fix:** Add a module in `src/serve/tests.rs` (or new file) that uses `common::InProcessFixture`-style chunk seeding to populate the store with a small call graph (e.g. `process_data` → `validate` → `format_output`, plus a test chunk). Then assert:
  - `build_graph` returns 3 data nodes + 2 call edges; `max_nodes=1` truncates; `file_filter` narrows; `kind_filter="function"` excludes tests.
  - `build_chunk_detail(store, "process_data_chunk_id")` returns `callers.len()==0, callees.len()==2, tests.len()==1`.
  - `build_hierarchy(store, root, Direction::Callees, depth=5)` returns the 3-node subtree; direction=Callers returns just the root; depth=1 truncates.
  - `build_cluster` returns N nodes with coords when UMAP coords are populated; returns `skipped=N` when they are not.

#### TC-HAP-1.29-2: Batch dispatch handlers (`dispatch_gather`, `dispatch_scout`, `dispatch_task`, `dispatch_where`, `dispatch_onboard`, `dispatch_callers`, `dispatch_callees`, `dispatch_impact`, `dispatch_test_map`, `dispatch_trace`, `dispatch_similar`, `dispatch_explain`, `dispatch_context`, `dispatch_deps`, `dispatch_related`, `dispatch_impact_diff`) have zero tests
- **Difficulty:** medium
- **Location:** `src/cli/batch/handlers/misc.rs:15, 131, 173, 209` (gather/task/scout/where); `src/cli/batch/handlers/graph.rs:24, 63, 103, 143, 233, 292, 375, 392` (deps/callers/callees/impact/test_map/trace/related/impact_diff); `src/cli/batch/handlers/info.rs:46, 100, 168, 302` (explain/similar/context/onboard).
- **Description:** Only `dispatch_search` has inline tests (5 of them in `src/cli/batch/handlers/search.rs:528-742`). The other **16** batch dispatch functions have zero tests. These are the daemon-hot-path handlers every agent hits via `cqs batch` and the socket path — a shape change to the JSON output or a regression in a chunk resolver bubbles silently until an agent notices. Grep confirms: no test file in `tests/` references any of these dispatch fns (only `real_eval_callgraph.json` mentions `dispatch_trace` as a pattern label). Batch 1 triage item P2 #48 addressed `dispatch_review/dispatch_diff/dispatch_drift/dispatch_blame/dispatch_plan`, but the read-only graph/info/search surface was not included.
- **Suggested fix:** Add a single integration file `tests/batch_handlers_test.rs` that uses `common::InProcessFixture` to seed a small corpus + call graph, then calls each handler through `BatchContext::dispatch_line("<cmd> <args>", &mut sink)` and asserts the JSON envelope's `data` field has the expected keys + a non-empty results array. One test per handler is enough. Example: `ctx.dispatch_line("callers process_data", &mut sink)` → JSON has `callers: [...]` with the seeded caller. Follows the exact same pattern `dispatch_search` tests already use (`create_test_context`, seeded chunks).

#### TC-HAP-1.29-3: `Reranker::rerank` and `Reranker::rerank_with_passages` have no tests
- **Difficulty:** medium
- **Location:** `src/reranker.rs:160` (`rerank`), `:190` (`rerank_with_passages`). Only `sigmoid` (a scalar helper) has tests (lines 450+).
- **Description:** `rerank`/`rerank_with_passages` are the two public entry points of the cross-encoder re-ranking subsystem — they take a `Vec<SearchResult>` + query and rescore via ORT. Zero tests pin their contract. The only callers in `tests/` are `eval_harness.rs:527` and `model_eval.rs:1417`, both of which use the reranker as a black box inside an evaluation loop and neither asserts behaviour of a specific (query, passages) pair. A regression in the pair-encoding (batch concat, attention mask), the sigmoid mapping to scores, or the ORT session call would surface as "eval scores moved slightly" rather than a unit-test failure. Given the reranker was flagged in recent audits as a correctness-critical scoring component (see P3 #100 "reranker over-retrieval pool ... duplicated 4x"), its own surface needs at least one happy-path pin.
- **Suggested fix:** Add two tests (likely `#[ignore]`-gated because they need the reranker model on disk — same shape as the existing ignored model tests):
  - `test_rerank_preserves_input_set_reorders_by_score`: seed 3 passages with obviously different relevance to a query ("rust async await" → ["tokio runtime docs", "how to bake sourdough", "rust futures trait"]); assert all 3 are returned and the baking passage ranks last.
  - `test_rerank_with_passages_empty_input_returns_empty_output`: pin the no-op shortcut. (This is the one test a non-model-loading run could cover.)

#### TC-HAP-1.29-4: `cmd_project { Register, Remove, Search }` — only `Register/List/Remove` has a CLI test; `Search` has no CLI integration test
- **Difficulty:** medium
- **Location:** `src/cli/commands/infra/project.rs:70` (`cmd_project`). Existing CLI tests: `tests/cli_surface_test.rs:170` `project_register_list_remove_round_trips`, `:252` `project_remove_nonexistent_succeeds_quietly`, `tests/cli_envelope_test.rs:133` `cqs --json project search anything`.
- **Description:** The `Search` subcommand (the one that does actual cross-project semantic search via `search_across_projects`, lines 119-164) runs `Embedder::new` → `embed_query` → `search_across_projects` → emit. The only existing test `tests/cli_envelope_test.rs:133` invokes `cqs --json project search anything` but asserts **only** the envelope shape, not that results were returned. There is no test that registers two projects, indexes both, runs `project search <query>`, and asserts results from both projects are interleaved and tagged with their project name. Inline tests in `src/cli/commands/infra/project.rs:178-207` only exercise `ProjectSearchResult` JSON serialization, not the end-to-end path. This is the ONLY cross-project-search surface — a regression (wrong project_name tag, dedup across projects collapsing results, weight application across indexes) ships silently.
- **Suggested fix:** Add `tests/cli_project_search_test.rs` with one test that uses `InProcessFixture` to create two temp stores (projects), writes distinct content in each, invokes `cqs project register` twice, then `cqs project search "<term>"`, and asserts: (a) at least one result comes from each project, (b) the `project` field on each result matches the registered name, (c) exit code 0. Can share the cross-project fixture from `tests/cross_project_test.rs`.

#### TC-HAP-1.29-5: `cmd_ref_add`, `cmd_ref_list`, `cmd_ref_remove`, `cmd_ref_update` (CLI) have no end-to-end test; only library-level helpers tested
- **Difficulty:** medium
- **Location:** `src/cli/commands/infra/reference.rs:88, 187, 320, 350` (add/list/remove/update). Tests in `tests/reference_test.rs` (397 lines, 11 tests) exercise `merge_results`, `search_reference`, `validate_ref_name` — all library functions. `tests/cli_drift_diff_test.rs:114` calls `cqs ref add` as setup for drift/diff tests but asserts only the drift output, not the ref add shape.
- **Description:** The 4 CLI subcommands under `cqs ref` do real work: `add` runs `enumerate_files` + `run_index_pipeline` + `build_hnsw_index` + `add_reference_to_config` (see lines 128-179); `list` reads every reference's DB for chunk counts (lines 187-280); `remove` validates existence + rewrites config (lines 320-340); `update` re-indexes from source. None have a happy-path CLI test. Impact: a regression in the TOML config round-trip (`add_reference_to_config` at `src/config.rs`), or in HNSW-per-reference path, or in the chunk-count aggregation shown by `list`, would ship unnoticed. The drift/diff tests use `ref add` but only as setup — they don't validate its output shape.
- **Suggested fix:** Add `tests/cli_ref_test.rs` with 4 tests:
  - `ref_add_then_list_shows_reference_with_chunk_count` — add a tiny reference (2 files), then `ref list --json` and assert `[{name, path, chunks: >=2}]`.
  - `ref_remove_deletes_from_config_and_disk` — add, then remove, then `ref list` returns empty.
  - `ref_update_reindexes_source_content` — add, modify a source file, `ref update`, assert chunk count changed.
  - `ref_add_weight_rejects_out_of_range` — pin `0.0..=1.0` contract (already tested at library level via `validate_ref_name` but not CLI-level).

#### TC-HAP-1.29-6: `cqs batch` daemon socket (`handle_socket_client`) — no happy-path test that a valid command round-trips through the socket
- **Difficulty:** medium
- **Location:** `src/cli/watch.rs:160` (`handle_socket_client`). Referenced by Batch 1 TC-ADV-1.29-3 for adversarial cases; this finding is the happy-path complement. `tests/daemon_forward_test.rs` has 9 tests but only covers the CLI-forwarding translator (`translate_cli_args_to_batch`) and `ping` — not a real dispatch-line round trip.
- **Description:** `handle_socket_client` is the daemon hot-path — every systemd-deployed `cqs watch --serve` instance serves ALL agent queries through it. The 9 tests in `daemon_forward_test.rs` cover only the pre-socket translation layer + `ping`. There is no test that sends `{"command":"search","args":["foo"]}` down a `UnixStream` pair, reads the response, and asserts `{data: {results: [...], total: N}}`. A regression in the framing layer (newline vs length-prefix, NUL-termination, gzip wrap), the JSON envelope wrap (P2 #28 already overlapped this for CLI; socket path is distinct), or the per-command dispatch registration in `dispatch_socket_command` would not surface until an agent noticed a broken batch response. The existing `tests/daemon_forward_test.rs:321` `test_mock_socket_round_trip_for_daemon_command` is labelled "mock socket" — it's actually a one-sided harness; see the file for details.
- **Suggested fix:** Add `tests/daemon_socket_roundtrip_test.rs` that creates a `tokio::net::UnixStream::pair()`, spawns `handle_socket_client` against the server half with a minimal `BatchContext` + seeded store, writes a newline-terminated JSON request, reads the response, and asserts envelope shape + payload. Cover one read-only command (e.g. `stats` — no embedder needed) to avoid model load cost.

#### TC-HAP-1.29-7: `cmd_similar` (CLI) has no integration test; only inline serialization tests and module-level `find_similar` tests
- **Difficulty:** easy
- **Location:** `src/cli/commands/search/similar.rs:41` (`cmd_similar`). Inline tests at line 280+ test output struct serialization only (5 tests, none exercise `cmd_similar`). Library `find_similar`/`find_related` is tested in `tests/related_impact_test.rs`.
- **Description:** `cmd_similar` is the CLI entry point for "find functions similar to X by embedding distance". No test invokes it with real input. The library `find_similar` is tested but `cmd_similar` adds: target-function lookup (`store.search_by_name`), embedding fetch, pattern filter application (line 72+), and typed-output JSON build. Each is a regression surface. Very cheap to add because `InProcessFixture` already has the scaffolding and `find_similar` can use `MockEmbedder`.
- **Suggested fix:** Add one test in `src/cli/commands/search/similar.rs::tests` using in-process fixture: seed 3 functions, call `cmd_similar(&ctx, "foo")`, capture stdout via a `Write` sink (the fn currently uses `println!`; if that makes it hard, add to the JSON path only — `--json` sinks via `emit_json`). Assert output contains the other 2 as similar results.

#### TC-HAP-1.29-8: `cmd_ci` happy path — library `ci::analyze_diff` tested, CLI function only tested in error paths
- **Difficulty:** easy
- **Location:** `src/cli/commands/review/ci.rs:9` (`cmd_ci`). Tests in `tests/ci_test.rs` cover library `ci::analyze_diff` (happy path: yes); `tests/cli_train_review_test.rs` has CLI tests for `cmd_ci` but they are error-path only (P2 #46 deliverable — see the batch 1 audit summary).
- **Description:** The CLI surface for `cqs ci` (which produces the GitHub-Actions-style review comment) runs: diff parsing → `analyze_diff` → `cqs_format` / `markdown_format` output → exit code assignment. `cli_train_review_test.rs` (line 154+) has a comment "P2 #46 (b) — `cmd_task`" and covers only failure modes. There is no test that feeds a real diff into `cmd_ci` and asserts the output markdown contains the expected sections ("High-risk changes", "Tests"). A regression in markdown formatting or exit-code mapping (lines 47-95 in ci.rs) ships silently.
- **Suggested fix:** Add one happy-path test in `tests/cli_train_review_test.rs` or a new `cli_ci_test.rs`: feed a real unified-diff string (modify a known indexed function) to `cmd_ci`, assert stdout contains "High-risk" when the diff touches a hotspot and the exit code matches the severity level set at `cli/commands/review/ci.rs:62-87`.

#### TC-HAP-1.29-9: `cmd_gather` (CLI) untested; only library `gather()` tested
- **Difficulty:** easy
- **Location:** `src/cli/commands/search/gather.rs:77` (`cmd_gather`). Inline tests at bottom cover output shape; `tests/gather_test.rs` tests library `gather()`. CLI-level `cmd_gather` has zero integration tests.
- **Description:** `cmd_gather` adds BFS depth / max_files clamping, content injection via `inject_content_into_gather_json` (unique to CLI path, not library), token budget trimming via `apply_token_budget`, and the JSON envelope wrap. A regression in any of these silently alters the output agents rely on for `/investigate` and `cqs task`. Library tests don't exercise these CLI-only steps.
- **Suggested fix:** Add one test in `tests/cli_gather_test.rs` (or reuse `tests/gather_test.rs` harness with `cqs()` spawning): seed a 3-function corpus, run `cqs gather "foo" --json --max-files 2`, assert the response envelope has `results.len() == 2` (clamp worked), `tokens_in/tokens_out` fields present when `--tokens N` passed, content injected into each file_group.

#### TC-HAP-1.29-10: `dispatch_line` has no happy-path test that verifies a valid command produces a correct JSON response
- **Difficulty:** easy
- **Location:** `src/cli/batch/mod.rs:557` (`dispatch_line`). Tests at lines 1908 (`bumps_query_counter` with a `bogus` input — error), 2124 / 2161 / 2198 (all adversarial — NUL bytes, unbalanced quotes).
- **Description:** Every existing `dispatch_line` test uses either a parse-failure input (`bogus`) or an adversarial malformed input. There is no test that sends a valid `search foo` / `stats` / `dead` down `dispatch_line`, reads the `Vec<u8>` sink, parses the JSON, and asserts envelope shape. The tests pin error_count/query_count invariants but not the actual output — a regression that produced the error envelope where it should have produced a success envelope would still pass the counter checks. Relatedly, `dispatch_search` is well-covered at the **handler** level but the **dispatch-line** wrap (clap parsing → handler routing → envelope serialization → newline emission) is not.
- **Suggested fix:** Add one test in `src/cli/batch/mod.rs::tests`: `test_dispatch_line_stats_emits_success_envelope` — run `ctx.dispatch_line("stats", &mut sink)` against an init-only store, parse the `sink` bytes as JSON, assert `json["data"]["total_chunks"].is_number()` and `json["error"].is_null()`. Small. Would catch P2-class regressions (envelope-shape changes, stats serialization drift).

#### TC-HAP-1.30-1: `spawn_hnsw_rebuild` and `drain_pending_rebuild` (non-blocking HNSW rebuild, #1113) ship in v1.30.0 with zero tests
- **Difficulty:** medium
- **Location:** `src/cli/watch.rs::spawn_hnsw_rebuild` (around line ~925, added in PR #1113), `drain_pending_rebuild` (same file). Plus the `PendingRebuild` struct fields `rx`, `delta`, `started_at` and the `RebuildOutcome` channel type (defined in WatchState section ~line 580-600).
- **Description:** The flagship daemon fix in v1.30.0 — `cqs watch --serve` no longer blocks editor saves for 10-30s during full HNSW rebuilds — has **zero** automated test coverage. The change spawns a background thread that opens its own read-only `Store` on the slot's `index.db`, builds enriched + base HNSWs from scratch, sends the `Owned` index back through a channel, and the watch loop's `drain_pending_rebuild` replays any (chunk_id, embedding) deltas captured during the rebuild window into the new index before the swap (deduping against `new_index.ids()`). This is both correctness-critical (delta replay / dedup) and concurrency-critical (TOCTOU between rebuild snapshot and channel `recv`). A regression — e.g., delta vector dropped on swap, dedup miscompare leaking duplicates into HNSW, or rx-channel miss leaving the daemon stuck on the old index — would surface as silently-stale search results in the daemon, not as a test failure. The PR commit message explicitly calls out two failure modes (post-restart Loaded/Owned mismatch, every-100 threshold trigger) but no test was added for either.
- **Suggested fix:** Add `tests/watch_hnsw_rebuild_test.rs` (or a `#[cfg(test)] mod` in `src/cli/watch.rs`) with three tests using `InProcessFixture`-style setup:
  - `rebuild_completes_and_swaps_owned_index` — seed 50 chunks, call `spawn_hnsw_rebuild(...)`, wait on the channel, assert the returned `Owned` HNSW has `len() == 50` and contains all chunk ids.
  - `delta_replayed_on_swap` — seed 50, spawn rebuild, while it's running call `WatchState::record_delta` with 5 new (id, embedding) pairs, then drain; assert the post-swap HNSW has `len() == 55` and the 5 deltas are searchable.
  - `delta_dedup_avoids_double_insert` — seed 50, spawn rebuild, push a delta whose chunk_id already exists in the rebuild's snapshot; assert post-swap `len() == 50` (no duplicate insert) and the embedding matches the delta's value (winner is the most-recent write).
  These can run with a tiny dim (e.g., 16) to keep the test fast — the rebuild path doesn't care about embedding semantics.

#### TC-HAP-1.30-2: `for_each_command!` registry macro and the four `gen_*` emitters (#1114) ship with zero compile-fail or behavioral tests
- **Difficulty:** medium
- **Location:** `src/cli/registry.rs:61` (`macro_rules! for_each_command!`), `src/cli/definitions.rs:850` (`gen_batch_support_impl`), `:897` (`gen_variant_name_impl`), `src/cli/dispatch.rs:51` (`gen_dispatch_group_a`), `:83` (`gen_dispatch_group_b`). Test counts: `registry.rs` = 0, `dispatch.rs` = 0, `definitions.rs` = 20 (but those test other things — clap arg parsing, `BatchSupport`, etc.).
- **Description:** v1.30.0 collapsed five exhaustive matches over `Commands` (~675 LOC of dispatch matches) into a single `for_each_command!` table consumed by four macro emitters. The PR comment notes "Adding a variant without a registry row is a compile error in four places" — but **no test verifies this contract**. The macros also encode group-A vs group-B classification (no-store/lifecycle/mutation vs store-using) which controls whether a command runs before or after `CommandContext::open_readonly`. A registry-row classification mistake (e.g., new mutation command added to `group_b:` instead of `group_a:`) would compile fine but silently open a read-only store before the mutation — a subtle correctness bug. Beyond classification, there is no test that walks every `Commands` variant and asserts `BatchSupport::for_command(v)` and `variant_name(v)` return non-default values, which is the only guard against a future edit silently regressing the macro emitter to a default-arm fallback.
- **Suggested fix:** Add `src/cli/registry.rs::tests` with two tests:
  - `every_command_variant_has_batch_support_entry` — iterate the `Commands` discriminants (use `strum::EnumIter` or a hand-rolled list), call `BatchSupport::for_command(&v)` for each; assert no variant returns the macro's default `_ => BatchSupport::None` arm by checking against an explicit allowlist of "should be None" variants. Same for `variant_name`.
  - `group_a_variants_disjoint_from_group_b` — derive the two sets from the registry rows (or from a debug introspection helper) and assert their intersection is empty. The `unreachable!("Group A variant `{name}` handled …")` arms in `dispatch.rs` enforce this at runtime; a test enforces it at compile-time-of-tests.
  Plus a `compile_fail` doc-test that adds a variant without a registry row, expecting a compile error (use `trybuild` if already in dev-deps, otherwise skip).

#### TC-HAP-1.30-3: `select_provider` / `detect_provider` (embedder ExecutionProvider feature split, #1120) untested
- **Difficulty:** easy
- **Location:** `src/embedder/provider.rs:171` (`select_provider`), `:188` (`detect_provider`), `:258` (`build_session_with_provider`). Test count in this file: **0**.
- **Description:** v1.30.0 PR #1120 (#956 Phase A) renamed `gpu-index` → `cuda-index` and split execution-provider selection out of `embedder/mod.rs` into `embedder/provider.rs`. The new module has zero tests. `select_provider` caches the result of `detect_provider` in a `OnceCell<ExecutionProvider>`, and `detect_provider` walks a feature-flag-conditional priority list (TensorRT > CUDA > CoreML > ROCm > CPU). A regression in the cache-on-first-call invariant, in the priority ordering, or in the `cfg(feature = "cuda-index")` gating could ship without anyone noticing — the prod path runs a single time per process and a wrong provider just shows up as "embeddings ran on CPU when CUDA was supposed to be enabled". The `Display for ExecutionProvider` impl at `src/embedder/mod.rs:207` is also untested.
- **Suggested fix:** Add `src/embedder/provider.rs::tests` with three tests (gated as needed by feature flags):
  - `cpu_when_no_features` (no feature gates, always runs) — under a thread-local override path, force `detect_provider()` into the CPU branch and assert it returns `ExecutionProvider::CPU`.
  - `cuda_selected_when_feature_enabled` (`#[cfg(feature = "cuda-index")]`) — assert that on a CUDA-enabled build, `detect_provider` returns `CUDA { device_id: 0 }` if the runtime env reports a GPU; pin via mock or by reading `select_provider()` once and checking the variant.
  - `select_provider_caches_first_call` — call `select_provider()` twice, assert both return the same enum value (the `OnceCell` invariant); flip an env var between calls and assert it's still cached.

#### TC-HAP-1.30-4: `build_hnsw_index_owned` and `build_hnsw_base_index` — core index helpers used by both `cqs index` and the new `spawn_hnsw_rebuild` — have zero direct tests
- **Difficulty:** medium
- **Location:** `src/cli/commands/index/build.rs:848` (`build_hnsw_index_owned`), `:880` (`build_hnsw_base_index`). Test count for `index/build.rs`: **0**. Both fns are `pub(crate)` and called from at least 3 sites: `run_index_pipeline`, `spawn_hnsw_rebuild` (#1113), and the post-pipeline finalizer.
- **Description:** These are the two functions every full-rebuild path goes through. `build_hnsw_index_owned` returns the enriched HNSW (used by the dual-index router); `build_hnsw_base_index` returns the base (non-enriched) HNSW (used as router fallback). The functions wrap embedding fetch + HNSW construction + on-disk save. Their callers (`cmd_index`, `spawn_hnsw_rebuild`) are integration-tested only end-to-end through `cqs index` invocations. No test asserts: (a) the returned `Owned` has the right `len()` matching chunk count, (b) the saved file at the right path can be loaded back, (c) `build_hnsw_base_index` returns `Ok(None)` when `embedding_base` rows are empty (its current shortcut), (d) the dim of the saved index matches `store.dim()`. With #1113 making these critical to the daemon-rebuild path, the absence of unit-level pins is a fresh risk.
- **Suggested fix:** Add `src/cli/commands/index/build.rs::tests` with three tests using `InProcessFixture`:
  - `build_hnsw_index_owned_returns_index_with_chunk_count` — seed 10 chunks with 16-dim embeddings, call `build_hnsw_index_owned(&store, &cqs_dir)`, assert `Some(idx)` returned and `idx.len() == 10`.
  - `build_hnsw_base_index_returns_none_when_no_base_rows` — fresh store, call `build_hnsw_base_index`, assert `Ok(None)`.
  - `build_hnsw_index_owned_round_trips_through_disk` — build, save, then `HnswIndex::load_with_dim(...)` from the saved path; assert the loaded index has the same `len()` and an arbitrary chunk-id is present in `ids()`.

#### TC-HAP-1.30-5: `hyde_query_pass` and `doc_comment_pass` (LLM passes shipping in `cqs index --hyde` / `--improve-docs`) have zero tests
- **Difficulty:** medium
- **Location:** `src/llm/hyde.rs:11` (`hyde_query_pass`, full file 98 LOC, **zero** tests), `src/llm/doc_comments.rs:135` (`doc_comment_pass`). Compare with `src/llm/summary.rs::llm_summary_pass` which has 4+ end-to-end tests in `tests/local_provider_integration.rs:113-280`.
- **Description:** Three LLM batch passes ship in production: `llm_summary_pass`, `doc_comment_pass`, and `hyde_query_pass`. Only `llm_summary_pass` has integration tests against the local mock server. Both `doc_comment_pass` and `hyde_query_pass` go through identical machinery — `collect_eligible_chunks` filter → `BatchPhase2::submit_or_resume` → store write — but their specific filter (e.g., `needs_doc_comment` for doc_comments, the HyDE eligibility predicate at `src/llm/hyde.rs`) and their result-purpose tags (`"hyde"` vs `"summary"` vs `"doc_comment"`) are unique. A regression in either one's filter or in the `set_pending_*_batch_id` resumption path (HyDE uses `s.get_pending_hyde_batch_id` / `s.set_pending_hyde_batch_id`) could ship — the existing `llm_summary_pass` tests don't cover them. Both passes are billed surfaces (real Anthropic dollars in the prod path); a regression that double-submits or fails resumption costs real money.
- **Suggested fix:** Extend `tests/local_provider_integration.rs` with two parallel tests mirroring the existing `llm_summary_pass` tests:
  - `hyde_query_pass_round_trips_through_mock_server` — seed 3 chunks, mock the local server's `/v1/chat/completions` to return canned predictions, run `hyde_query_pass`, assert `count == 3` and `store.get_summaries_by_purpose("hyde")` has 3 rows.
  - `doc_comment_pass_skips_already_documented_functions` — seed 3 chunks where 1 has a doc comment per `needs_doc_comment`'s predicate, run `doc_comment_pass`, assert `count == 2` (the 1 already-documented chunk skipped). Verifies the unique-to-doc_comments filter.

## Summary

15 findings filed (10 from initial v1.29 audit + 5 v1.30.0-specific).

The most impactful gaps in the v1.30.0-specific batch:
- **TC-HAP-1.30-1** — flagship #1113 fix (non-blocking HNSW rebuild) has **zero** tests despite shipping concurrency-critical delta/dedup logic in the daemon hot path
- **TC-HAP-1.30-2** — #1114 single-registration command registry's classification correctness (group_a vs group_b) is enforced only at compile time of the dispatch matches, not at test time; a misclassified mutation command would silently open a read-only store
- **TC-HAP-1.30-4** — `build_hnsw_index_owned` / `build_hnsw_base_index` are now critical to both `cqs index` and `spawn_hnsw_rebuild` and have no direct unit tests

The original v1.29 findings remain valid; the most impactful is still #1 — the entire `cqs serve` subsystem has 14 tests but all run against an empty store. Several of these are cheap adds (~1 test each) because the in-process fixture already exists.
