Skip to content

test: mock Claude CLI integration tests + dead export cleanup#8

Merged
JonyanDunh merged 1 commit into
mainfrom
test/mock-haiku-integration
Apr 10, 2026
Merged

test: mock Claude CLI integration tests + dead export cleanup#8
JonyanDunh merged 1 commit into
mainfrom
test/mock-haiku-integration

Conversation

@JonyanDunh
Copy link
Copy Markdown
Owner

Summary

Closes the coverage gap around the real `spawnSync('claude', ...)` subprocess code path in `hooks/stop-hook.js`, and cleans up 4 dead exports flagged by a grep audit.

Why

Before this PR, the 53 tests in `test/stop-hook.test.js` exercised every branch of the stop hook except the one where it actually spawns `claude -p --model haiku ...`. Every test deliberately took the "no tool uses", "max iterations", "missing transcript", "recursion guard", or "corrupt state" branches to avoid burning real Haiku tokens in CI. That left the single riskiest path — the Windows `cmd.exe` shell-quoted subprocess invocation with complex JSON prompts — completely untested.

What

New test file: `test/stop-hook-haiku.test.js` (6 tests)

Creates a temporary bin directory with a cross-platform mock Claude CLI:

  • `fake-claude.js` — shared Node.js implementation. Distinguishes `claude --version` (used by the `claudeCliAvailable()` pre-flight check, always succeeds) from `claude -p` (the real verdict call, emits a verdict controlled by the `WATCHDOG_FAKE_HAIKU_VERDICT` env var).
  • `claude` — POSIX shell wrapper (`#!/bin/sh` + exec node), resolved by `spawnSync('claude', ...)` on Linux/macOS.
  • `claude.cmd` — Windows batch wrapper, resolved via `PATHEXT` when the hook uses `shell: true` on Windows.

The test prepends this bin dir to `PATH` and runs the real `hooks/stop-hook.js` subprocess with realistic `HOOK_INPUT` + state file + transcript, then asserts on every verdict branch:

Verdict Expected behavior
`FILE_CHANGES` `{decision: "block"}`, iteration bumped, state preserved
`NO_FILE_CHANGES` State file removed, allow stop
Ambiguous (both markers) Continue loop as safety
Ambiguous (neither) Continue loop as safety
CLI failure (exit 1) Continue loop as safety

Plus a sanity test that the mock is actually resolvable on PATH.

Dead export cleanup

`lib/constants.js`: `STATE_DIR`, `STATE_FILE_PREFIX`, `STATE_FILE_SUFFIX` were used internally by `stateFilePath()` but exported to no one. Demoted to module-local `const`.

`lib/judge.js`: `claudeCliAvailable()` is used internally by `askHaiku()` but exported to no one. Removed from `module.exports`.

Doc updates

  • README test count: 53 → 59 across all 7 languages
  • CI workflow explicit file list includes the new test file
  • CONTRIBUTING.md describes the new test file and mock-CLI approach

Test plan

  • `node --test ...` locally — 59/59 passing (up from 53)
  • New test file found the real bug in my own fake script (fake`--version` was being affected by the FAIL env var, routing hook into CLI_MISSING instead of CLI_FAILED). Fixed.
  • CI will confirm across 3 OSes × 3 Node versions = 9 matrix jobs

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Previously the 53 tests in test/stop-hook.test.js exercised every
branch of hooks/stop-hook.js EXCEPT the one where it actually spawns
`claude -p --model haiku ...`. All tests deliberately took the
"no tool uses", "max iterations", "missing transcript", "recursion
guard", or "corrupt state" branches to avoid burning real Haiku
tokens. That left the single riskiest path — the Windows cmd.exe
shell-quoted subprocess invocation with complex JSON prompts —
completely uncovered.

Fix: add test/stop-hook-haiku.test.js which creates a temporary
directory with a cross-platform mock Claude CLI (a POSIX shell
wrapper `claude` + a Windows `claude.cmd` batch wrapper + a shared
Node.js implementation that reads WATCHDOG_FAKE_HAIKU_VERDICT from
env), prepends it to PATH, and runs the real hooks/stop-hook.js
subprocess with realistic HOOK_INPUT + state file + transcript.

New test cases exercise every verdict branch end-to-end through
the real spawnSync('claude', ...) code path:

  1. FILE_CHANGES  → hook emits {decision:"block"}, iteration bumped
  2. NO_FILE_CHANGES → hook removes state file, exits silently
  3. ambiguous (both markers present) → continue loop as safety
  4. ambiguous (neither marker) → continue loop as safety
  5. CLI failure (exit 1 from `claude -p`) → continue loop as safety

The mock correctly distinguishes `claude --version` (used by the
claudeCliAvailable() pre-flight check) from `claude -p` (the real
verdict call) so the CLI-failure branch is not confused with
CLI-missing.

Test count: 53 → 59. Suite runtime: ~800ms → ~1250ms. No external
dependencies added. Runs on the full CI matrix (Ubuntu/macOS/Windows
× Node 18/20/22).

Also clean up 4 dead exports flagged by a grep audit:

  - lib/constants.js: STATE_DIR, STATE_FILE_PREFIX, STATE_FILE_SUFFIX
    were used internally by stateFilePath() but exported to nobody.
    Demoted to module-local const.
  - lib/judge.js: claudeCliAvailable() is used internally by
    askHaiku() but exported to nobody. Removed from module.exports.

Propagated the 53 → 59 count to all 7 READMEs (English + zh/ja/ko/
es/vi/pt) and updated the Testing section to mention the mock-CLI
integration coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JonyanDunh JonyanDunh merged commit 332c3b9 into main Apr 10, 2026
11 checks passed
@JonyanDunh JonyanDunh deleted the test/mock-haiku-integration branch April 10, 2026 23:11
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