Skip to content

test(integrations): characterize the Claude Code and Codex hooks - #2255

Open
Fengzdadi wants to merge 3 commits into
oceanbase:masterfrom
Fengzdadi:test/hook-characterization
Open

Fengzdadi wants to merge 3 commits into
oceanbase:masterfrom
Fengzdadi:test/hook-characterization

Conversation

@Fengzdadi

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Part of #2250 (plan step 1, second item: characterization fixtures for Claude Code and Codex).

Rationale for this change

#2250 plans to move the Claude Code and Codex hooks onto one lifecycle module. Their unit tests replace hook internals in about 370 places, most often resolve_scope_id, _post_json, _prepare_context, and _capture_prompt. Those tests follow the current structure, so a move would rewrite them, and they could not show that it kept the behavior.

This PR records what each hook does at its process boundary, which a structural change has to leave as it is. It also runs each hook with the interpreter its host uses. The unit tests run the hooks in the repository's virtual environment, so they cannot notice an import that the host cannot satisfy.

What changes are included in this PR?

  • tests/test_hook_characterization.py runs each case against an installed copy of the plugin's hook scripts:
    • Each step gives a hook its native payload on stdin and the host's environment. A scripted local Server answers the hook's requests.
    • Claude Code starts its hooks with python3, which has none of the plugin's dependencies, so they run under python -S. Codex starts them with uv run --frozen, so they run in an environment that uv sync --frozen builds once per test session from the plugin's pyproject.toml and uv.lock.
    • Each step records the requests the Server receives, the hook's stdout, stderr, and exit status, every file the hook leaves in the case directory, and the mode of each directory it creates.
    • JSON objects are recorded with sorted keys. Paths, the Server address, and workspace keys of each run are recorded as tokens such as $CWD, $SERVER_URL, and $WORKSPACE_KEY, and a time written during the case as $NOW.
    • The Claude Code fixture names a closed port as its lower-precedence endpoint, so no case can reach a Server on the default address. The cases set no_proxy=*.
    • The test is skipped on Windows, because the recorded state includes POSIX file modes.
  • tests/fixtures/hook_characterization/claude-code.json (57 cases) and codex.json (50 cases). They cover:
    • recall, capture, and flushing
    • the query bound in characters and bytes
    • credential scrubbing, and stored or configured credentials
    • endpoint precedence, invalid settings, and the plaintext transport policy at a prompt and at SessionStart
    • redirects, and non-JSON, oversized, or slow responses
    • Scope resolution and its failures: no Server, 503, 401, a missing route, a configured Scope that does not exist, and a non-JSON response
    • failure classification and throttling
    • the bootstrap receipt that SessionStart hands to the next prompt, and its removal after a failed start
    • Codex tool binding and the Stop statistics
  • A refactoring leaves the fixtures unchanged. After a reviewed behavior change, uv run python tests/test_hook_characterization.py rewrites the expectations, and the fixture diff shows what changed.

The fixtures record today's behavior, including a gap that #2250 lists: when Scope resolution fails, the Claude Code and Codex prompt hooks end without output, although their guides promise a diagnostic. The case descriptions state only what is observable, such as "the prompt is neither prepared nor captured", so the fix for that gap can rewrite those expectations without renaming the cases. The fix is a separate PR.

Are there any user-facing changes?

No. This PR adds a test and its fixtures.

How was this change tested?

  • tests/test_hook_characterization.py: 107 passed on macOS with Python 3.13, in 7 s with -n auto, the worker setting make unit-test uses, and in 19 s serially. Python 3.11 and 3.14 also pass serially.

  • Each mutation below changed one place in a plugin. The table lists how many cases failed:

    Mutation Failing cases
    The Claude Code prompt hook imports pydantic 42
    The Codex recall hook imports powercontext 34
    The Codex recall hook imports pydantic_settings, which the plugin's lock file provides (control) 0
    The Claude Code receipt directory is created with mode 0755 instead of 0700 4
    The Claude Code prepare request lists its keys in another order (control) 0
    The Codex workspace key uses the hook's cwd instead of the git root 1
    The Codex workspace key uses the process's working directory 40
    Claude Code ignores POWERCONTEXT_CLAUDE_SERVER_URL 1
    Plaintext HTTP is allowed to a non-loopback address (once per host) 1 each
    Codex drops POWERCONTEXT_CODEX_AUTHORIZATION 1
    Codex reads its authorization through an equivalent alias (control) 0
    Codex ignores capture_prompts 1
    Settings load outside the fail-open try (once per host) 2 each
    A failed SessionStart or invalid settings leave the receipt in place (two places per host) 1 each
    The byte bound on the query is removed 2
    The hook writes one more file: Claude Code configuration directory, Claude Code workspace, Codex workspace 37, 37, 26
    Diagnostic state is copied from its temporary file instead of renamed, so the temporary file stays: Claude Code, Codex 22, 12
    The receipt is copied from its temporary file instead of renamed, so the temporary file stays: Claude Code, Codex 4, 3
  • make check passed. make unit-test: 6339 passed, 300 skipped.

  • The cases run the real hook scripts across the real process boundary, but their payloads are built from the hosts' documentation and the existing tests, not recorded from Claude Code or Codex. I did not run either host.

AI usage statement

Claude Code (Claude Opus 5.5) was used to write the test and the fixtures, review them with an independent agent, and draft this description. I reviewed the change.

Record what each hook does at its process boundary, so that moving the
hosts onto a shared lifecycle module can show that it changed nothing.
Each case runs an installed copy of the plugin's hook scripts with a
native payload, the host environment, and a scripted Server. Claude Code
hooks run without site packages, since python3 provides none of the
plugin's dependencies, and Codex hooks run in an environment built from
the plugin's lock file, so an import the host cannot satisfy fails the
case. Each case records the requests the Server receives, the hook's
stdout, stderr, and exit status, every file the hook leaves in the case
directory, and the mode of each directory it creates. JSON objects are
recorded with sorted keys.

The cases cover recall and capture, flushing, the query bound in
characters and bytes, credential scrubbing and stored or configured
credentials, endpoint precedence and the plaintext transport policy at
a prompt and at SessionStart, invalid settings, redirects and non-JSON,
oversized, or slow responses, Scope resolution and the ways it fails,
failure classification and throttling, the bootstrap receipt that
SessionStart hands to the next prompt and clears after a failed start,
Codex tool binding, and the Stop statistics. The Claude Code fixture
names a closed port as its lower-precedence endpoint, so no case can
reach a Server on the default address.

A refactoring leaves the fixtures unchanged. A reviewed behavior change
rewrites them with `uv run python tests/test_hook_characterization.py`.
@Fengzdadi
Fengzdadi requested a review from PsiACE as a code owner October 26, 2026 19:34
Copilot AI balanced review requested due to automatic review settings October 26, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

{
"hook": "hooks/user_prompt_submit.py",
"environment": {
"POWERCONTEXT_CLAUDE_REQUEST_TIMEOUT_SECONDS": "0.3"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Give the slow-response fixtures more scheduling margin

This 0.3-second timeout also applies to the prerequisite Scope resolution and the subsequent capture request. On the unchanged head, a serial Linux/Python 3.14 run under high system load produced 105 passes and two failures: both hosts' slow-preparation-times-out cases timed out during Scope resolution, so the expected prepare/capture requests never happened. The Claude Code case also failed in isolation. In a control run, increasing the request timeout to 1 second while retaining the scripted 2-second preparation delay matched the original snapshots for both hosts.

Please increase the scheduling margin for these requests and adjust the delayed response as needed, so the intended preparation timeout remains distinguishable from a slow test runner. The same issue applies to codex.json:1965; otherwise normal parallel or loaded test runs can report a behavior regression without any hook changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in a060e97. Both slow-preparation-times-out cases now set a 1 s request timeout, and the scripted preparation answers after 5 s instead of 2 s, so the prepare request still times out first, with a 4 s margin. Re-recording leaves the expectations unchanged, and each case now takes about 1.3 s.

I couldn't reproduce the failure with 36 busy loops on an 18-core Mac, so I checked the mechanism directly: with the scripted Scope response delayed by 0.4 s, both cases fail with the old 0.3 s timeout. With the new values, both pass with the Scope response delayed by 0.4 s or 0.8 s.

…argin

The two slow-preparation cases set a 0.3 s request timeout, and that
timeout also bounds the step's Scope resolution and capture requests. On
a loaded runner, Scope resolution could miss it, so the case failed
without any hook change. The cases now use a 1 s timeout, and the
scripted preparation answers after 5 s instead of 2 s, so the prepare
request still times out first with a 4 s margin. The recorded
expectations are unchanged.
Copilot AI balanced review requested due to automatic review settings October 27, 2026 13:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hireview001 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot AI balanced review requested due to automatic review settings October 28, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous timeout finding is fixed for both hosts; replaying the delayed-Scope counterexample preserves the expected behavior. No remaining findings. This PR changes only tests and fixtures, with no public API or database schema changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.5.0 v1.5.0 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants