Repository navigation
[CLAUDE ROUTINE]: Test-coverage enhancement — add Cursor stdout-shape coverage to policy-evaluator.test.ts (deny / instruct / allow across PreToolUse, PostToolUse, PermissionRequest, Stop) #262
Description
Summary
src/hooks/policy-evaluator.ts carries dedicated Cursor branches at every decision shape — deny (line 130), instruct (line 250), allow-with-info (line 321) — emitting Cursor's flat {permission, user_message, agent_message} / {permission, additional_context} / {followup_message} response format. These branches were added with the Cursor Agent CLI integration in #245 and are now in production for every Cursor user.
__tests__/hooks/policy-evaluator.test.ts (706 lines, 49 test cases) currently has zero test cases that exercise session.cli === "cursor". The Claude / Codex / Copilot shapes are well covered; pinning the Cursor shape closes the parity gap so a future refactor of the evaluator can't silently regress the response Cursor expects.
This complements #156 (per-policy tests), #157 (loader edge cases), and #155 (auth/relay tests) by completing the per-CLI evaluator matrix.
Where
Code paths needing coverage in src/hooks/policy-evaluator.ts:
| Decision | Event | Code | Cursor response shape |
|---|---|---|---|
| deny | PreToolUse | line 130-144 | {permission: "deny", user_message, agent_message} |
| deny | PostToolUse | line 130-144 (same branch) | same |
| deny | PermissionRequest | line 130-144 (same branch) | same |
| deny | Stop | line 130-144 (same branch) | same |
| instruct | non-Stop | line 265-278 | {permission: "allow", additional_context} |
| instruct | Stop | line 250-264 | {followup_message} |
| allow + info | any event | line 321-338 | {permission: "allow", additional_context} + stderr lines |
Test file with the gap: __tests__/hooks/policy-evaluator.test.ts — a grep -n cursor returns 0 hits today.
Why this helps
flowchart LR
subgraph Coverage today
Claude[Claude shapes ✅] --- Codex[Codex shapes ✅] --- Copilot[Copilot shapes ✅] --- Cursor1[Cursor shapes ❌]
end
subgraph Coverage proposed
Claude2[Claude shapes ✅] --- Codex2[Codex shapes ✅] --- Copilot2[Copilot shapes ✅] --- Cursor2[Cursor shapes ✅]
end
The Cursor branches re-shape stdout in subtle ways (e.g. additional_context vs Claude's nested hookSpecificOutput.additionalContext; followup_message only used for Stop). Without locked-in tests, an innocent-looking refactor — e.g. moving the per-event branches into a lookup table, or unifying the deny response shape — could silently emit Claude-shaped JSON to a Cursor session. Cursor would then treat the deny as a malformed response and the policy wouldn't fire.
Adding the test matrix is a one-time investment that protects every Cursor user going forward.
Proposed enhancement
Add a new describe("Cursor response shapes", () => {...}) block in __tests__/hooks/policy-evaluator.test.ts covering the seven rows in the table above. For each, build a SessionMetadata with cli: "cursor", run evaluatePolicies with a stubbed policy returning the relevant decision, and assert against the parsed stdout — never against the raw string — so future field ordering is not pinned.
Sketch:
describe("Cursor response shapes", () => {
const cursorSession: SessionMetadata = { cli: "cursor", cwd: "/proj" };
it("deny on PreToolUse emits {permission: 'deny', user_message, agent_message}", async () => {
registerPolicy("test/cursor-deny", "", async () => deny("test reason"), { events: ["PreToolUse"] }, 0);
const result = await evaluatePolicies("PreToolUse", { tool_name: "Bash" }, cursorSession);
const stdout = JSON.parse(result.stdout);
expect(stdout).toMatchObject({
permission: "deny",
user_message: expect.stringContaining("test reason"),
agent_message: expect.stringContaining("test reason"),
});
expect(stdout.hookSpecificOutput).toBeUndefined(); // Cursor does NOT use Claude's nested shape
});
it("instruct on Stop emits {followup_message}", async () => { /* … */ });
it("instruct on PreToolUse emits {permission: 'allow', additional_context}", async () => { /* … */ });
it("allow-with-info emits {permission: 'allow', additional_context} + stderr", async () => { /* … */ });
// …
});Acceptance criteria
- At least seven new test cases (one per row in the table above) in
__tests__/hooks/policy-evaluator.test.ts. - Each test asserts on
JSON.parse(result.stdout)and explicitly asserts that the Claude-shapedhookSpecificOutputkey is absent. - Tests pass on the existing implementation (no production code changes required — this is purely a coverage enhancement).
-
vi.useFakeTimersis not used in these tests (avoids the leakage issue tracked in [CLAUDE ROUTINE]: Test-coverage enhancement — wrapvi.useFakeTimers/useRealTimersin try/finally (or afterEach) to prevent leaking fake timers across tests #173). -
CHANGELOG.mdentry under Unreleased → Tests.
Severity
Low — production code already works; this is an investment in regression safety as the evaluator continues to evolve.
Activity