Files
alktls/tasks/acme-event-loop-test.md
T
glm-5.3-flash 23893d6236 re-baseline the four pending remediation tasks against the ADR-007/008 tree
The tasks were decomposed (d7db6b1) before 49d4432/ac440f3 landed;
both commits touched exactly the areas the tasks reference. Fixes
grounded in verified sources (vendored rustls-acme 0.12.1, rustls-pemfile
2.2.0, rustls-native-certs 0.8.4) and a fresh cargo llvm-cov run:

- acme-event-loop-test: the termination assertion was impossible —
  rustls-acme's Stream for AcmeState never yields None
  (state.rs:407-412, poll_next_infinite + 2^16s backoff); the review's
  U-1 exit-condition premise is withdrawn and corrected in place.
  Replaced with timeout-bounded event collection, a reachable-arm
  inventory (Order warn, AccountCacheStore, Load/Parse error arms,
  DeployedCachedCert/CertCacheStore via deterministic DirCache file
  pre-seeding), and an explicit mark for the full-fake-CA arms.
  server.rs:135 flagged as unreachable dead code (delete or accept).
- coverage-cheap-closes: re-baselined per-line ground truth — original
  groups 1 and 5 are already closed by the handshake suites; group 2's
  TLS 1.3 half is covered, leaving the TLS 1.2 else-arm (client.rs:316);
  new group added for VerifyPresentedCertVerifier::verify_tls12_signature
  (server.rs:349-366, opened by ADR-008; required for the >=98% bar);
  AcceptAnyCertVerifier refs moved to server.rs:468-475 with the stale
  OQ-TLS-09 coordination caveat retired.
- docs-pin-c1-c4-n3-n4: N-4 re-scoped (the mechanism analysis already
  lives in ADR-007 + the resolver doc block; what remains is a short
  server-verifier note covering both verifiers); C-4 updated for
  ADR-007's negotiation-earlier failure point; added the
  FingerprintPinVerifier pop cross-ref update (post-ADR-008 the default
  verifier does verify possession).
- config-validation-and-trivia: added the feature-gate mechanics note
  for the C-3 test (a non-gated test passes vacuously under default
  features); refreshed drifted line refs with a re-grep advisory.
- review 001: Status block records OQ-TLS-09/-10 resolutions; U-1
  carries the termination correction; U-2 carries the supersession
  note. ADR-008 gains the suite-number-to-test-name mapping.

Verification: taskgraph validate 14 tasks; cargo doc --no-deps
warning-free; all edits docs-only (no code paths touched).
2026-09-12 02:42:20 +00:00

6.9 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
acme-event-loop-test ACME event-loop coverage — fake-directory integration test (U-1) pending
coverage-cheap-closes
moderate medium component implementation
tests
acme
review-001
u1

Description

The ACME event-loop body (src/server.rs:94-136) is unreachable by tests: 23 uncovered lines — every EventOk/EventError arm, the debug/warn/error mapping. The acme_lifecycle tests construct the config and assert spawn + ALPN + resolver wiring; the spawned task runs against a blackhole URL and its events are never observed. A refactor that drops or mislevels an arm (e.g. EventError::Order warn → error) lands green.

Correction to this task (2026-09-12, verified against the vendored rustls-acme 0.12.1 sources) — the review's §U-1 termination premise is wrong, and so was this task's original work item 3. rustls-acme 0.12.1's Stream for AcmeState never terminates: poll_next is Poll::Ready(Some(ready!(self.poll_next_infinite(cx)))) (state.rs:407-412) — it can never yield None. Order errors enter an exponential backoff (Timer::after(1 << backoff_cnt), capped at 2^16 seconds — state.rs:371-375) and retry forever. Two consequences:

  1. There is no "loop ends when the stream ends" — the while let Some(event) = state.next().await loop in new_acme never exits on its own. Do NOT assert that the JoinHandle resolves; such a test hangs until timeout. The original item 3 is removed.
  2. src/server.rs:135 ("ACME: state machine ended") is unreachable dead code under this dependency. It cannot be covered by any test through the real stream. Either delete the line in this task (and note the removal) or explicitly accept it as uncovered — do not leave an implementer to burn hours rediscovering that it is unreachable. (The review's §U-1 framing "if upstream changes the stream's termination semantics, nothing notices" described a termination semantics that does not exist.)

The acme feature's only runtime surface is this loop; it deserves one real integration test.

Work

  1. Drive a DirCache-backed AcmeState against a local fake directory: a stub HTTP server (tokio, std::net::TcpListener on an ephemeral port — no new deps) that serves a directory JSON with no usable endpoints, forcing the error path through the real event stream. (Mechanically verified: rustls-acme's HTTP layer, async_web_client 0.6.3, is plain async_net::TcpStream for http:// URLs and self-drives via async-io 2's reactor thread — it works under a tokio runtime, and a plain-HTTP localhost stub is reachable.)
  2. Assert the log events fire. Two shapes, pick one (or both):
    • tracing test subscriber capturing the warn!/error! events (tracing-subscriber with a test layer — add as dev-dependency only), or
    • extract the event-mapping match into a helper fn taking the event, returning (level, message-class), and test the helper directly (no subscriber needed; cheaper, but the loop body itself stays uncovered — prefer the subscriber shape if the dev dep is acceptable).
  3. Bound event collection by time, never by stream end: collect events under tokio::time::timeout (or a bounded channel + a drop-with-timeout teardown) and assert on what arrived. The spawned task never resolves its JoinHandle — see the correction above — so any "await the handle" shape hangs. Note: with #[tokio::test] (current-thread runtime) the spawned loop runs on the test thread, so a thread-local default tracing subscriber is visible to it; keep the test single-threaded or the subscriber captures nothing.
  4. Which arms are reachable, per rustls-acme 0.12.1's state machine (state.rs poll_next_infinite / order):
    • EventError::Order (warn) — fires against the erroring stub.
    • EventOk::AccountCacheStore — the first event, generated before any network I/O (the account key is generated and stored eagerly, state.rs:377-395); fires even against a dead address.
    • EventError::AccountCacheLoad / CertCacheLoad / CachedCertParse (error) and EventOk::DeployedCachedCert / CertCacheStore (ok) — reached by pre-seeding the DirCache directory. The cache file names are deterministic (caches/dir.rs:38-53): cached_account_{base64url(SHA256( contact-els…directory_url))} and cached_cert_{base64url( SHA256(domain-els…directory_url))}. A corrupt pre-seeded file gives the Load/Parse error arms; a valid PEM (an ECDSA-P256 chain, PKCS#8 key first — parse_cert wants key-then-cert-chain, state.rs:192-214) gives the deployed/stored ok arms. The PEM can be generated with rcgen in-test (rcgen is already a dependency on the acme feature path via the server's self-signed helper — it is a [dependencies] member).
    • EventOk::DeployedNewCert / EventError::NewCertParse — reachable only through a successful order, i.e. a full fake ACME CA (newNonce/newAccount/newOrder/authorizations/finalize/ certificate endpoints). Out of scope at this effort level; mark explicitly not-covered rather than silent.
  5. Keep TlsError out of it: per ADR-002/ADR-006, ACME runtime errors are stream events, not error variants — the test asserts events, never a TlsError.

Verification

  • server.rs 94-136 covered under cargo llvm-cov --all-features (line 135 either deleted or explicitly accepted as uncovered, per work item 2's second consequence)
  • The test performs no real network I/O (binds localhost only)
  • cargo test --features acme, --all-features green; default build unaffected
  • clippy/fmt/doc green

Acceptance Criteria

  • Every reachable arm per work item 4 is executed by a test (Order warn, AccountCacheStore, the Load/Parse error arms, DeployedCachedCert/CertCacheStore); DeployedNewCert/ NewCertParse are explicitly marked not-covered (full fake CA needed — out of scope)
  • No test awaits the event loop's JoinHandle (it never resolves)
  • The ACME feature's runtime surface is no longer refactor-fragile

References

  • docs/reviews/001-implementation-review.md §U-1, Part C (§U-1's termination premise is corrected — see this task's Description)
  • src/server.rs:64-145 (new_acme + the spawned loop)
  • tests/acme_lifecycle.rs (the existing construction-level tests)
  • ADR-006 (acme feature layout), ADR-002 (TlsError scope boundary)
  • rustls-acme 0.12.1 sources (vendored): state.rs:407-412 (the never-ending Stream impl), state.rs:371-375 (order-error backoff + retry), state.rs:192-214 (parse_cert — key first, then ≥2 PEMs), caches/dir.rs:38-53 (the deterministic cache file names)

Notes

Agent fills this during implementation.

Summary

Agent fills this on completion.