headroom/tests/test_cli
Ayush Kumar Jha 202c1895e1
fix(wrap): make the Serena pre-index stall budget configurable (#3183)
## Description

`headroom wrap` blocks the agent launch on a synchronous Serena
pre-index whose
300-second ceiling is a hardcoded module constant. When indexing exceeds
it the user
waits the full five minutes, the work is discarded (`Serena: pre-index
timed out (will
index on demand)`), and nothing — env var, flag, or config — can shrink
that budget.

Closes #3093

### Why this is still open after #2938

`_serena_project_skip_reason` keeps the pre-index off non-project roots,
which covers
the reporter's two repro directories. But it **defers the stall by one
wrap rather than
removing it**: as that function's own docstring notes, Serena's MCP
server generates
`project.yml` itself on first start, "so the pre-index simply resumes
from the next wrap
onwards." A parent-of-many-repos directory therefore gets claimed during
the first
session and pays the full 300s budget on every wrap after that. The
reporter's remaining
ask — "I'd also like the pre-index timeout to be configurable" — is the
unfixed half.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Added `HEADROOM_SERENA_INDEX_TIMEOUT` and
`_resolve_serena_index_timeout_seconds()`, modelled on the existing
`_resolve_wrap_proxy_timeout_seconds()` in the same module.
- `_index_serena_project` resolves the budget after the `uvx` guard and
passes it to `communicate()` instead of the bare constant.
- `_SERENA_INDEX_TIMEOUT = 300` stays as the default, so unset behavior
is unchanged.
- Added 19 tests covering the resolver and the pre-index call path.

### Deliberate divergence from the proxy-timeout precedent

`_resolve_wrap_proxy_timeout_seconds` raises `RuntimeError` on a bad
value, which is
right for a subsystem the wrap cannot proceed without. The pre-index is
documented as
best-effort and non-fatal, so raising there would let a typo'd env var
abort a launch
that would otherwise succeed. An unusable value instead warns and falls
back to 300s.
The warning is unconditional (not gated on `--verbose`) because a knob
that looks
applied but is not is the failure this issue reports.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_cli/test_wrap_serena_boost.py -q
43 passed, 1 skipped, 1 warning in 1.13s       # 24 pre-existing + 19 new

# the same 19 tests against the unpatched tree:
18 failed, 1 passed, 24 deselected             # the 1 passer is a pre-existing test caught by -k

$ python -m pytest tests/test_cli/ -q
3 failed, 696 passed, 2 skipped in 57.80s
# the 3 are pre-existing Windows failures (symlink handling in test_recover_codex.py
# and test_unwrap_claude.py); they fail identically on an unpatched tree.

$ python -m ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_serena_boost.py
All checks passed!
$ python -m ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_serena_boost.py
2 files already formatted
$ python -m mypy headroom/cli/wrap.py
Success: no issues found in 1 source file
```

Regression check across all 42 test modules that import
`headroom.cli.wrap`, run in both
states with the working tree md5-verified before each run: identical
81-line
failure/error set, +19 passing with the fix.

## Real Behavior Proof

- Environment: Windows 11 Home 26200, Python 3.11.9, headroom at 0.36.2
(`5e0ce24`). Serena/`uvx` are not installed on this machine and the Rust
`_core` extension is not built (no Rust toolchain), so a full `headroom
wrap claude` could not be launched — see `Not tested`.
- Exact command / steps: drove the real `_index_serena_project()` with a
real child process, a real process group, real
`communicate(timeout=...)`, real `TimeoutExpired`, and the real
`_kill_serena_index_tree`, timing each phase with a monotonic clock at
`HEADROOM_SERENA_INDEX_TIMEOUT=2` and `=4`. Only *which* binary runs was
substituted (a 120s sleeper in place of `serena project index`), since
the timeout logic is indifferent to the callee.
- Observed result: the configured budget controls the wait exactly — a
2s budget waits 2.02s and a 4s budget waits 4.02s, where before the
change the same harness reports 300s regardless of any env var set. Full
output below.
- Not tested: an end-to-end `headroom wrap claude/opencode` against a
real `serena project index` (uvx/serena unavailable here); non-Windows
platforms; the interaction with a genuinely large monorepo index.

```text
budget=2s | waited  2.02s for timeout | teardown 10.02s | total 12.03s
budget=4s | waited  4.02s for timeout | teardown 10.02s | total 14.03s

misconfigured value:
  Serena: ignoring HEADROOM_SERENA_INDEX_TIMEOUT='30s' (want a positive integer
  number of seconds) - using 300s
  -> resolved to 300s, no exception raised
```

### Incidental finding (not addressed here)

On Windows, `_kill_serena_index_tree` adds a constant ~10s after any
timed-out
pre-index — one of its two 10s bounds (`taskkill` / `proc.wait`) is hit
every time. So a
2s budget still costs ~12s wall clock. That is pre-existing #2938 code
untouched by this
PR, but it caps how small the stall can usefully get and may deserve its
own issue.

## Runtime Rollout Safety

- Rollout-managed feature(s): none — this is a plain env var, not a
rollout-channel feature.
- Minimum rollout channel: n/a — available on every channel, inert
unless set.
- Stable/default behavior changed: no — unset resolves to the existing
300s constant.
- Kill switch / disable path: unset `HEADROOM_SERENA_INDEX_TIMEOUT`;
skipping the pre-index entirely remains `--no-serena`.
- Unsafe override required: no.
- Qualification impact: none — no change to compression, proxy, or
provider behavior.
- Rollback path: revert the commit; no persisted state, no migration, no
config to clean up.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

**Alternatives considered.** A CLI flag (`--serena-index-timeout`) is
more discoverable
but has to be threaded through four `wrap` subcommands, adds CLI surface
that
CONTRIBUTING gates behind maintainer sign-off, and would not reach `wrap
... -- agents`
sessions. Making the pre-index asynchronous removes the stall outright
and is arguably
the better end state, but it is an architectural change and would reopen
the
orphaned-grandchild failure mode #2938 just closed. Auto-scaling the
budget by project
size reintroduces the kind of hand-maintained heuristic #2938
deliberately removed.

**What this does not solve.** The default is still 300s, so a user who
never sets the
variable still stalls; the reporter's third point (using Serena in
background agent
sessions launched from a parent directory) is a Serena-semantics
question rather than a
headroom defect; and an in-flight pre-index is still not interruptible.

**Open questions for maintainers.**

1. Should `0` mean "skip the pre-index" instead of being rejected? I
kept the
proxy-timeout precedent (reject `<= 0`) since `--no-serena` already
covers disabling,
   but the other reading is defensible.
2. `HEADROOM_WRAP_PROXY_TIMEOUT` — the closest precedent — is not in
`docs/content/docs/configuration.mdx`, so I matched it and left docs
alone. Happy to
   add a row if you would rather document it.
3. If you consider a new env knob a feature rather than part of this
bug, say so and I
   will hold for a maintainer sign-off before you spend review time.

Documentation: no `CHANGELOG.md` edit (release-please generates it from
the PR title).
2026-08-21 14:59:53 -07:00
..
conftest.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_copilot_auth.py fix(auth): support GitHub Enterprise Copilot OAuth domain (#2192) 2026-07-14 16:07:17 -04:00
test_init_cli.py fix(cli/init): fail clearly on a target settings file with invalid JSON (#2227) 2026-07-15 18:15:45 +00:00
test_init_enable_tool_search.py fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986) 2026-08-13 15:06:41 -05:00
test_install_cli.py fix(install): honor HEADROOM_PORT in install apply and deploy (#3085) 2026-08-17 20:20:38 -07:00
test_main_help_version.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_mcp.py feat(mcp): add streamable HTTP MCP transport (#1773) 2026-07-14 13:25:45 -04:00
test_mcp_status.py fix(mcp): mcp status checks ~/.claude.json, not only ~/.claude/mcp.json (#990) 2026-07-15 09:25:52 -05:00
test_recover_codex.py test(recover-codex): bind AF_UNIX socket via short relative path (#2396) 2026-08-02 13:14:31 -07:00
test_serena_disable.py fix(serena): stop the Serena dashboard popup and make --no-serena actually disable Serena (#1003) 2026-06-14 23:32:46 -07:00
test_serena_migrate.py fix(wrap/serena): stop creating serena_config.yml, unbricking Serena on fresh installs (#2676) 2026-07-30 20:55:47 -07:00
test_subprocess_utf8_encoding.py fix(windows): pin UTF-8 encoding on text-mode subprocess calls (#1311) 2026-06-23 12:52:49 -05:00
test_unwrap_claude.py fix(install): consolidate Windows fallback and cleanup safety (#2980) 2026-08-13 15:05:45 -05:00
test_wrap_aider.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_bridge.py fix(wrap): set xAI upstream for grok-build proxy (#2772) 2026-08-16 15:04:59 -07:00
test_wrap_claude.py fix(claude): stop forcing tool search on Foundry (#2477) 2026-08-12 00:15:07 -05:00
test_wrap_claude_base_url.py fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986) 2026-08-13 15:06:41 -05:00
test_wrap_claude_finally_unbound.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_claude_vertex_proxy_env.py fix(claude): reject conflicting auth before proxy startup (#2993) 2026-08-13 23:01:59 -05:00
test_wrap_codex.py fix(wrap): verify proxy deps before mutating Codex config (#1628) 2026-08-13 11:52:22 -05:00
test_wrap_copilot.py fix(wrap): honor Copilot OAuth wire-api override and model default (#2387) 2026-08-12 00:04:42 -05:00
test_wrap_dead_marker_selfheal.py fix(wrap): self-heal a stale ANTHROPIC_BASE_URL left by a dead proxy (#2223) 2026-07-15 19:58:10 +00:00
test_wrap_encoding.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_goose.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_grok.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_helpers.py feat(wrap/claude): make the --1m fallback model configurable via HEADROOM_1M_MODEL (#2983) 2026-08-16 15:09:46 -07:00
test_wrap_kimi.py feat(wrap): add headroom wrap kimi for Kimi CLI (#1426) 2026-07-15 21:40:58 +00:00
test_wrap_omp.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_openclaude.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_openclaw.py fix(wrap): use canonical headroom-openclaw npm package for wrap openclaw (#1969) (#2120) 2026-07-13 16:33:44 -04:00
test_wrap_opencode.py fix(wrap): verify proxy deps before mutating Codex config (#1628) 2026-08-13 11:52:22 -05:00
test_wrap_openhands.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_persistent.py feat(opencode): support Copilot subscription backend for headroom models (#2441) (#2445) 2026-07-20 11:02:14 -07:00
test_wrap_proxy_detach.py fix(wrap): detach the shared proxy on Windows so it survives an ungraceful agent close (#1464) 2026-06-30 13:49:28 -05:00
test_wrap_serena_boost.py fix(wrap): make the Serena pre-index stall budget configurable (#3183) 2026-08-21 14:59:53 -07:00
test_wrap_stale_marker.py fix: detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions (#1768) (#1837) 2026-07-06 08:35:40 -07:00
test_wrap_vibe.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
test_wrap_vscode.py fix(copilot): send VS Code inline completions to the host that serves them (#3112) 2026-08-18 15:21:22 -07:00
test_wrap_vscode_claude.py fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986) 2026-08-13 15:06:41 -05:00
test_wrap_zcode.py fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00