Skip to content

JAR-430: cover the hook scripts and their process contracts - #54

Merged
JartanFTW merged 1 commit into
agent/ferro/c4a8e65ffrom
agent/ferro/c4a8e65f-hooks
Aug 18, 2026
Merged

JAR-430: cover the hook scripts and their process contracts#54
JartanFTW merged 1 commit into
agent/ferro/c4a8e65ffrom
agent/ferro/c4a8e65f-hooks

Conversation

@Jartans-Familiar

@Jartans-Familiar Jartans-Familiar commented Aug 17, 2026

Copy link
Copy Markdown
Member

Second of two for JAR-430. Purely additive: no Makefile or CI change, since #52 wired the gate. Stacked on #52 -- base is agent/ferro/c4a8e65f, and GitHub retargets this to main when that merges. Review #52 first.

Unit tests

113 Node tests over the four modules CONTRIBUTING.md names as class 1 -- pure logic behind a silent-wrong failure mode:

  • lib/transcript-context.js (24) -- extractUsageTokens, including the iterations-versus-top-level double count that reports a context 2.00x its real size; readFileTail; readLatestContextTokens, including the truncated-tail guard; resolveContextThreshold (0 disables, out-of-range falls back) and resolveContextInterval (0 does not disable, which is the contrast worth pinning).
  • lib/session-state.js (17) -- sessionId sanitising and its fallback chain, sweepStaleState (TTL sides, keep preserved, prefix isolation, never throws), stateTtlDays.
  • check-console-log.js (10) -- applyCooldown: fresh versus repeat, the cooldown reset, a finding that disappears dropping out of seen, and a reintroduced statement counting as new again.
  • lib/utils.js (24) -- filterByPatterns including the all-invalid case that must not filter everything away; the regex-flag handling in countInFile and grepFile, both lastIndex traps; findFiles's glob-to-regex escaping.

The sweep deletes files, so its tests redirect TMPDIR at a throwaway directory first. os.tmpdir() reads the variable on each call rather than caching it, so that is enough, and it is the only way to keep a test that deletes files away from the real temp directory without mocking the module out from under the code being tested.

Process contracts

One per shipped hook entry point, each spawned with malformed JSON, empty stdin and a payload with no transcript_path, asserting exit 0 and either no stdout or exactly one parseable JSON object (JSON.parse is the "exactly one" half -- two concatenated objects do not parse). Each also has one positive case, so the "exactly one object" branch is actually exercised rather than only the silent one.

sync-codex-agents.js is spawned rather than imported for the reason CONTRIBUTING.md gives: hooks.json invokes it via require and it works at require time, so a require.main guard would turn the hook into a no-op. PLUGIN_ROOT and CODEX_HOME point at temporary directories throughout. Both plugins' copies are covered from the one file, discovered rather than hardcoded so a third copy is picked up -- a second file would only duplicate it, since the copies must be byte-identical and make verify already asserts that.

Seam

Three functions the suite consumes are added to their modules' exports: readFileTail and extractUsageTokens in transcript-context.js, filterByPatterns in utils.js. That is the praxis 1.4.1 -> 1.4.2 bump and the regenerated .codex-plugin/plugin.json.

Verification

Every test was checked to be load-bearing by reverting the behaviour it pins, one at a time, and confirming it reddens. 18 such mutations were run; each reddened the intended test and only it.

Three did not, and the reasons are worth recording rather than papering over:

  1. My own test bug, fixed here. The PLUGIN_ROOT ending in .. case was built with path.join, which normalizes the .. away -- so it passed against the very bug it exists to catch. Rebuilt by concatenation, and it now catches it.
  2. sweepStaleState's strict >= is not observable. The cutoff is the clock at call time, so no mtime can be set to land exactly on it. The test pins the two sides one minute either way, and its comment says so rather than claiming the boundary itself.
  3. Two guards are provably unobservable no-ops. In applyCooldown, state.seen.filter(key => present.has(key)) cannot change any answer: seen is only ever queried with keys drawn from findings, which are exactly present, and the persisted seen comes from [...present] regardless. In sweepStaleState, dropping if (!entry.isFile()) continue; changes nothing either, since rmSync without recursive throws on a directory and the throw is already caught. Both are harmless and defensive. Neither is touched here -- editing behaviour-adjacent code in an additive test PR is the wrong place for that call, and they are reported rather than silently tidied.

make test (139 Node + 40 Python, all pass) and make verify pass locally. make lint passes every hook that can run here; lychee, actionlint and zizmor need Docker, which is not reachable from this runtime, so the lint job covers them.

Refs #34

Closes JAR-430 -- this is the second and last PR the issue carries.

🤖 Generated with Claude Code

Second of two, purely additive: no Makefile or CI change, since PR 1 wired
the gate. Adds 113 Node tests over the token accounting and setting
resolvers in lib/transcript-context.js, the sweep and session-id sanitising
in lib/session-state.js, the cooldown split in check-console-log.js, and
the regex-flag and glob handling in lib/utils.js.

Process contracts for all four shipped hook entry points: exit 0 and either
no stdout or exactly one well-formed JSON object, whatever arrives on stdin.
sync-codex-agents.js is spawned rather than imported because hooks.json
invokes it via require and it works at require time, so a require.main
guard would make the hook a no-op; both plugins' copies are covered.

The seam widens by three functions the suite consumes -- readFileTail and
extractUsageTokens in transcript-context, filterByPatterns in utils --
which is the praxis version bump.

Co-authored-by: multica-agent <github@multica.ai>
@JartanFTW
JartanFTW merged commit 8aa84ec into agent/ferro/c4a8e65f Aug 18, 2026
@JartanFTW
JartanFTW deleted the agent/ferro/c4a8e65f-hooks branch August 18, 2026 05:59
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.

2 participants