Repository navigation
[AI-910][AI-1516] Fix two kcap-cli parallel-load flakes - #364
Conversation
AI-910 — ClaudeHookStdoutTests stdout-capture contamination: Thread an optional TextWriter through ClaudeHookCommand.Handle -> HandleWithDeps -> HandleCore (default Console.Out); the SessionStart hookSpecificOutput envelope and the auth-lapsed systemMessage nudge write to it. The test passes its own StringWriter instead of redirecting the process-global Console.Out, so a concurrently-running test's Console write can no longer leak into the capture (the JsonReaderException flake). Kept [NotInParallel] — these tests still touch process-global config + the auth-provider cache + real hook HTTP timing. AI-1516 — HandleStopAgent reap-by-pid-record confirmation race: ProcessReaper.KillConfirmAsync treated a transient Ambiguous (a proven-ours leader momentarily unreadable mid-death-transition) as an immediate spare in its grace window, and checked once (250ms) after SIGKILL. Under parallel load the reaper reported a killed process as unconfirmed, stranding its PID record (dead-but-retained). Both confirmation waits now poll via PollForConfirmedDeathAsync for a definitive Dead/Recycled, keeping-polling on a transient Ambiguous; the pre-signal and pre-SIGKILL gates still spare on Ambiguous (safety invariant unchanged: only a proven-ours leader is signalled). Corrected the test's macOS branch — which asserted "always spares" but was in fact winning a race against its own SIGTERM — to assert the real invariant: the record is deleted IFF the process was confirmed gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by QodoFix kcap-cli parallel-load flakes in hook stdout capture and process reaping
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
Context used 1.
|
- ProcessReaper: on Windows the hard-kill SignalGroup is a no-op (the soft signal already tree-killed and the grace poll gave it the full window), so cap the post-SIGKILL confirmation poll at a single 250ms tick there instead of the 5s ConfirmAfterKill window — avoids adding up to 5s of latency to a non-terminating pid's unconfirmed path. Unix keeps the full poll. - Trim the verbose narrative comments on KillConfirmAsync / PollForConfirmedDeathAsync and in the two test files to concise intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed all three Qodo findings in 898246b:
Re-verified locally: |
…th writer - ProcessReaper.PollForConfirmedDeathAsync: the classify-then-delay loop skipped a classify after the final delay, so a death reflected during the last interval (the whole window on Windows' 250ms cap) was dropped and reported unconfirmed. Restructured to also classify at the window boundary (delay-then-check for the final interval), restoring the pre-change delay-then-check confirmation semantics. - Complete the injected-stdout contract: thread the writer through the permission-request path (HandleCore -> PermissionRequestCommand.Handle -> HandleRenderedAgent -> PostAsync) so its decision output honours it too, not just the SessionStart envelope. - Fix the stale StopAgentPidFallbackTests class summary (macOS outcome is now identity-readability dependent, not "always spares"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes two kcap-cli test flakes. Linear: AI-910, AI-1516.
AI-910 —
ClaudeHookStdoutTestsstdout-capture contaminationThe three tests captured hook stdout by redirecting the process-global
Console.Outto aStringWriter. A concurrently-running test whose SUT writes toConsole.Out(e.g.CodexHookCommandemitting{"continue":true}) leaked a second JSON object into the capture →JsonReaderException: '{' is invalid after a single JSON value.Fix (issue's preferred option 1): thread an optional
TextWriter? stdoutthroughClaudeHookCommand.Handle → HandleWithDeps → HandleCore(defaultConsole.Out). The SessionStarthookSpecificOutputenvelope and the auth-lapsedsystemMessagenudge now write to that writer. Each test passes its ownStringWriter— noConsole.SetOut, so nothing another test writes toConsolecan contaminate it. This is the durable fix for the class (a keyed[NotInParallel]couldn't prevent it; the writer does, regardless of scheduling).[NotInParallel]is kept: the injection ends the contamination flake, but these tests still exercise process-global state (KCAP_CONFIG_DIRconfig + the in-process auth-provider cache) and real hook HTTP timing. (Verified that fully parallelizing them exposes an unrelated empty-envelope flake under load — out of scope.)AI-1516 —
HandleStopAgentreap-by-pid-record confirmation raceSurfaced on PR #360's first ubuntu run:
HandleStopAgent_unknown_id_reaps_by_pid_record_where_env_is_readableintermittently failed with the PID record still present after the process had exited.Root cause:
ProcessReaper.KillConfirmAsync'sClassifyreadsIsAlive(kill(0)) then the start token (/proc/{pid}/stat). A SIGTERM'd, proven-ours process in its death transition readsalive=truebut its token momentarily unreadable →Ambiguous. The grace loop returned false on that transientAmbiguous, so the reaper reported unconfirmed even though the process was (about to be) dead → the record was stranded (ReapByRecordAsyncdeletes only on confirmed-gone). The single fixed 250ms post-SIGKILL check had the same fragility under OS-reflection lag.Fix: both confirmation waits (grace + post-SIGKILL) poll via a shared
PollForConfirmedDeathAsyncfor a definitiveDead/Recycled, treating a transientAmbiguousas keep polling (we've already proven ownership and signalled). The pre-signal and pre-SIGKILL gates still spare onAmbiguous— the safety invariant is unchanged: only a proven-ours leader is ever signalled, and a persistently-ambiguous target is still retained for the next sweep.Test correction (see the Linear comment). The test's macOS branch asserted "always spares (
HasExited == false)", but tracing showed that's a race, not a real spare: on macOS 26.5.2Classify = Ours(identity is readable) so the reaper does SIGTERM the dummy — the old code only "passed" by returning on the first transientAmbiguousbefore the SIGTERM landed, leaving the process dead-but-record-retained (the exact bug). The macOS branch now asserts the real invariant — record deleted IFF the process was confirmed gone — making it a true regression guard (fails on the pre-fix dead-but-retained state, passes on the fix) and robust across macOS versions.Verification (local, macOS 26.5.2)
ClaudeHookStdoutTests3/3; fullCapacitor.Cli.Tests.Integration144/144;ClaudeHookCommandTests35/35;SpoolOutageRecoveryTests2/2.AgentOrchestratorVendorTests84/84;OrphanReaperTests9/9.check-linear-ids.shpasses.🤖 Generated with Claude Code