Skip to content

test(dispatch): unit tests for dispatch pipeline (was 575 LoC / 0 tests)#324

Merged
Destynova2 merged 4 commits intomainfrom
test/dispatch-unit-tests-v2
Apr 28, 2026
Merged

test(dispatch): unit tests for dispatch pipeline (was 575 LoC / 0 tests)#324
Destynova2 merged 4 commits intomainfrom
test/dispatch-unit-tests-v2

Conversation

@Destynova2
Copy link
Copy Markdown
Contributor

Summary

Adds 21 unit tests to src/server/dispatch/mod.rs, which previously had 575 LoC and zero tests — the largest blind spot in the test suite per the audit.

Tests are organized into the following groups within mod tests:

DLP phase (5 tests)

  • dlp_clean_input_passes_through_engine — happy path: benign prompt does not trigger block.
  • dlp_blocks_request_on_injection_match — edge: (?i)ignore previous instructions triggers DlpBlockError::InjectionBlocked.
  • dlp_block_error_display_includes_pattern_name — protects the audit log entry built from block_err.to_string().
  • dlp_disabled_scan_input_skips_engine — verifies the scan_input flag is honoured.
  • dlp_no_engine_short_circuits_to_okOption::None DLP engine is the no-op precondition.

Cache phase (6 tests)

  • cache_round_trip_returns_stored_response — happy path: put/get returns identical body.
  • cache_miss_returns_none — empty cache lookup misses.
  • cache_skips_oversized_entriesmax_entry_bytes is enforced at insert time.
  • cache_key_is_deterministic_for_same_request — identical requests hash to identical keys.
  • cache_key_skipped_for_nonzero_temperaturetemperature > 0 opts out of caching.
  • cache_key_differs_per_tenant — multi-tenant isolation in cache keys.

DispatchResult variants (2 tests)

  • dispatch_result_complete_carries_provider_and_model — non-streaming path preserves provider info.
  • dispatch_result_fanout_carries_response_only — fan-out path stores only the response.

Audit entry construction (3 tests)

  • audit_entry_dlp_block_uses_blocked_backend_marker — backend == "BLOCKED" on DLP block.
  • audit_entry_provider_failure_uses_none_backend — backend == "NONE" on all-providers-failed.
  • audit_entry_fanout_success_records_token_counts — fan-out audits both input and output tokens.

Provider behaviour (2 tests)

  • mock_provider_returns_fixed_response — exercises the MockLlmProvider round trip.
  • mock_provider_supports_only_configured_model — guards accidental model leakage in fallback chains.

Token accounting (2 tests)

  • token_counts_propagate_from_provider_response — happy path: counters propagate intact.
  • dlp_block_reports_zero_tokens_consumed — billing guard: blocked requests have token_counts: None.

Routing metadata (1 test)

  • route_decision_default_route_type_no_complexity_tier — gate at Step 5.5 of the pipeline.

Honest scoping note

Happy path vs edge case (per task brief):

Category Happy-path Edge / negative
DLP clean input passthrough, no-engine short-circuit injection block, error formatting, scan_input=false
Cache round trip, deterministic keys, tenant isolation empty miss, oversized skip, non-zero temperature
Result Complete, FanOut variants
Audit DLP block, provider failure, fan-out success
Provider mock round trip unsupported-model guard
Tokens propagation DLP-block billing guard

Behaviours intentionally NOT covered (documented in the test module docstring as follow-up work):

  1. End-to-end dispatch() happy path with the provider loop.
  2. Provider fallback on 503 / rate-limit / circuit-breaker rejection.
  3. dispatch_provider_loop() ordering with the adaptive scorer.
  4. Audit log emission asserted from the AuditLog file (needs disk fixture).
  5. Token-accounting double-count guard on retry.
  6. Cache hit metric assertion (Prometheus recorder is global state — installing it twice in the same process panics).

The reason these aren't unit tests today is that every DispatchContext requires a fully built Arc<AppState>, and AppState has hard dependencies on:

  • GrobStore (real disk + AES-256-GCM cipher init).
  • AuditLog (real signing key generation, real append-only file).
  • metrics_exporter_prometheus::PrometheusBuilder::install_recorder() (process-global, single-install).
  • SpendTracker (real spend journal).

Mocking these would require introducing trait objects on the hot path, which is a non-trivial refactor that is out of scope for a tests-only PR. The pragmatic approach taken here:

  • Test the isolated, testable surface area of the dispatch module without pretending we have the full pipeline under test.
  • Document the gap explicitly so the next contributor knows where the seams are.

Coverage on src/server/dispatch/mod.rs improves from 0 -> 21 tests touching DLP scan, cache round-trip, audit entry construction, and result variants. End-to-end dispatch pipeline coverage is best added as integration tests in tests/integration/, where standing up a real AppState against a tempfile::TempDir is already common.

Note on base branch

Original task brief specified fix/preset-mod-include-str as the base, but that branch has since been merged into main (PR #304), so this PR targets main directly.

Test plan

  • cargo nextest run --no-fail-fast — 1289/1289 pass (was 1268; +21 new).
  • cargo nextest run --no-fail-fast --lib server::dispatch::tests — 21/21 pass.
  • cargo fmt --check — clean.
  • cargo clippy --tests --lib --no-deps — clean.
  • No tokio::time::sleep in test bodies; deterministic by construction.

Clément LIARD added 4 commits April 28, 2026 22:41
CI re-runs the full test suite (incl. doctests) on every PR via the
.github/workflows/ci.yml tests job, so local pre-push duplication
adds ~20 min per push without catching anything new. Pre-push hooks
should be fast-fail; expensive checks belong on the CI server.

Closes audit finding: silent productivity tax (pre-push duplication).
Documents the three-state intent (true/false/absent) of ProviderConfig.is_enabled
and the dependency on deny_unknown_fields (added in the next commit) to
reject typos like enbaled = false at parse time. Behaviour is unchanged;
this is purely contractual clarity to support the silent-typo-killer audit.

Closes audit finding: silent typo killer on provider config.
Adds #[serde(deny_unknown_fields)] to AppConfig and the major
sub-structs (ProviderConfig, ModelConfig, TierConfig, RouterConfig,
ScoringConfig, CacheConfig, BudgetConfig, DlpConfig, SecurityConfig).

Without this guard, a typo like enbaled = false in a [[providers]]
block silently parses (the unknown key is dropped) and the provider
remains enabled with the wrong intent. With the guard, parsing fails
loudly and the operator gets an actionable error pointing at the
offending key.

Tested with the full nextest suite (1268 tests) plus all doctests:
no fixture, preset or example carries a stale field, so this is a
pure tightening with no migration cost.

Closes audit finding: silent typo killer on TOML config.
Each entry in DENIED_SECTIONS / DENIED_KEYS now carries a short
justification table covering why it can not be hot-reloaded — either
because the data is sensitive (credentials, DLP rules) or because the
consumer is constructed once at process start (TLS listener, secret
backend, TEE attestation, FIPS gate).

Adds tee, fips, server.tls and secrets.backend to the deny-list so
the documented "static-init" rationale matches actual behaviour. Also
emits an INFO log on every denied attempt telling the operator to
restart instead of expecting the silent reload to apply.

Adds two unit tests covering the new deny entries (tee/fips sections
and server.tls / secrets.backend keys) and asserts that sibling keys
in the same sections remain editable.

Closes audit finding: hot-reload UX (silent ignore of denied edits).
@Destynova2 Destynova2 enabled auto-merge April 28, 2026 20:48
@Destynova2 Destynova2 merged commit a6a684e into main Apr 28, 2026
205 of 235 checks passed
@Destynova2 Destynova2 deleted the test/dispatch-unit-tests-v2 branch April 28, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant