Skip to content

refactor(env): inject hook-handler env via capability traits#458

Open
akesling wants to merge 11 commits into
mainfrom
akesling/session-start-test-hermeticity
Open

refactor(env): inject hook-handler env via capability traits#458
akesling wants to merge 11 commits into
mainfrom
akesling/session-start-test-hermeticity

Conversation

@akesling
Copy link
Copy Markdown
Collaborator

@akesling akesling commented May 13, 2026

Summary

Routes the four hook entry points (handle_session_start, PreToolUse, PostToolUse, Stop) through a small Env struct that bundles three capability traits: PolicyStore, SessionRecorder, SandboxProbe. Production wires Env::prod() from zero-sized static impls that delegate to the existing free functions; tests construct a hermetic TestEnv whose session writes land in a TempDir and whose sandbox probe returns canned Full. The exact same handle_* code runs in tests as in production — only the env differs.

A repo-root clippy.toml denies direct calls to every wrapped function so the abstraction doesn't decay. clash/src/env.rs is the lone module exempt (top-level #![allow(clippy::disallowed_methods)]); CLI commands that legitimately bypass Env (e.g. clash doctor --sandbox) annotate their callsite with a per-site allow + rationale.

Commits

docs(plans): plan hook handler env injection
refactor(env): scaffold Env capability traits
refactor(env): implement SandboxProbe
refactor(env): implement PolicyStore
refactor(env): implement SessionRecorder
refactor(env): add hermetic TestEnv
refactor(handlers): inject Env into handle_session_start
refactor(cmd/hooks): route PreToolUse through Env
refactor(cmd/hooks): route PostToolUse through Env
chore(lint): deny direct calls to wrapped env functions
docs(env): note the clippy guardrail in module header

Test plan

  • cargo test --package clash --lib — 392 tests passing
  • cargo clippy --package clash --all-targets — zero disallowed_methods violations
  • handlers::tests::test_session_start_reports_* — all three pass against the same handle_session_start production runs, via TestEnv, on a machine with a legacy ~/.clash/policy.json (previously panicked)
  • TestEnv::new writes confined to a TempDir (asserted in env::tests::test_env_is_hermetic)

Out of scope (explicit non-goals)

  • handle_permission_request migration — needs a Notifier trait for desktop + Zulip; no current test asks for it.
  • Clock, Random, Stdio, HttpClient, Subprocess ports — no tests assert on these.
  • Crate split (clash-core / clash-adapters / clash-bin) — the lint guardrail gives most of the same enforcement without the disruption.
  • Effects-as-data on handlers — trait DI is enough for the current test surface.

Plan document

The implementation follows docs/superpowers/plans/2026-05-13-hook-handler-env-injection.md, which is committed in this PR as the first commit for traceability.

@akesling akesling force-pushed the akesling/session-start-test-hermeticity branch from 858263c to f149616 Compare May 13, 2026 18:50
@akesling akesling changed the title test(handlers): bypass policy resolution in session-start tests refactor(handlers): inject session-start env via trait so tests share the production path May 13, 2026
Plan for migrating handle_session_start, the PreToolUse path, and the
PostToolUse path off direct env access (filesystem, sandbox probe,
session bookkeeping) onto three capability traits — PolicyStore,
SessionRecorder, SandboxProbe — bundled in an Env<'a> struct. Tests
substitute an in-memory Env; production constructs Env::prod() at the
hook command dispatcher. A clippy.toml at the repo root locks in the
boundary by denying direct calls to every wrapped function outside of
clash/src/env.rs (the only adapter module).

Replaces the earlier single-handler `SessionStartEnv` design with one
shared trait vocabulary across all three hook entry points, plus
enforcement to keep new handlers from drifting back to direct calls.
@akesling akesling force-pushed the akesling/session-start-test-hermeticity branch from f149616 to 1bf36c3 Compare May 13, 2026 20:56
@akesling akesling changed the title refactor(handlers): inject session-start env via trait so tests share the production path docs(plans): plan hook handler env injection May 13, 2026
akesling added 10 commits May 13, 2026 19:04
Repo-root clippy.toml lists every function wrapped by clash::env so that
new handler code can't accidentally bypass the trait layer. env.rs is
the lone module exempt (top-level `#![allow(clippy::disallowed_methods)]`).

Two legitimate-bypass categories are explicitly annotated:
  - CLI commands (doctor, session, trace, sandbox_cmd, shell_cmd) that
    are not on the hook handler path and have no testability target;
    each callsite carries a single-line `#[allow]` plus rationale.
  - In-module tests in audit/trace/session_policy that exercise the
    very functions the module defines; each module carries a
    `#![cfg_attr(test, allow(clippy::disallowed_methods))]` at the top.

Also migrates the Stop hook subcommand to route trace::sync_trace
through Env::prod for consistency with the other three hook arms.
@akesling akesling changed the title docs(plans): plan hook handler env injection refactor(env): inject hook-handler env via capability traits May 14, 2026
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