Skip to content

test(indexing): pin _active_runs cancellation contract (refs #640)#648

Merged
memtomem merged 1 commit intomainfrom
fix/issue-640-stream-cancel
May 1, 2026
Merged

test(indexing): pin _active_runs cancellation contract (refs #640)#648
memtomem merged 1 commit intomainfrom
fix/issue-640-stream-cancel

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented May 1, 2026

Summary

Why

PR #609's existing tests only exercise:

  • happy-path index_path / index_path_stream counter inc/dec
  • explicit aclose() on the stream generator
  • exception inside the inner work
  • two concurrent runs

The task-cancellation path — what really happens when a Starlette
StreamingResponse consumer disconnects mid-stream — is not exercised.
That's the gap #640 points at. Adding coverage:

  • test_decrements_on_task_cancellation cancels a task iterating
    index_path_stream directly. Confirms CancelledError propagation
    through async-gen stack-unwind runs the finally block.
  • test_decrements_through_generator_wrapper_on_cancel mirrors the
    SSE route's _generate() wrapper shape (catches Exception,
    re-yields error, lets BaseException propagate). Confirms the
    stacked-generator path is also clean.

Status of #640

Hypothesis tested, not reproduced. Will follow up on #640 with a
comment proposing narrower investigation paths (watcher concurrency,
client-side state machine, hot-reload engine swap).

Test plan

  • uv run pytest packages/memtomem/tests/test_indexing_engine.py::TestActiveRunsCounter -xvs — 8/8 pass
  • uv run pytest packages/memtomem/tests/test_indexing_engine.py -m "not ollama" — 133/133 pass
  • uv run pytest packages/memtomem/tests/test_web_routes.py::TestIndexingActive — 3/3 pass
  • uv run ruff check packages/memtomem/tests/test_indexing_engine.py
  • uv run ruff format --check packages/memtomem/tests/test_indexing_engine.py

🤖 Generated with Claude Code

Issue #640 hypothesizes that ``IndexEngine._active_runs`` leaks when an
SSE ``StreamingResponse`` consumer disconnects mid-stream — the path
isn't covered by the PR #609 tests, which only exercise explicit
``aclose()`` and exhausted async-for.

Adds two regression tests in ``TestActiveRunsCounter``:

- ``test_decrements_on_task_cancellation`` — cancels a consumer task
  iterating ``index_path_stream`` directly. Confirms Python's async-gen
  stack-unwind on ``CancelledError`` runs the ``finally`` block that
  decrements the counter.
- ``test_decrements_through_generator_wrapper_on_cancel`` — cancels a
  consumer task iterating a wrapper async generator that mirrors the
  SSE route's ``_generate()`` shape (catches ``Exception`` but not
  ``BaseException``). Pins that the stacked-generator path is also
  clean — the production code shape behind #640.

Both tests pass against current ``main``: the engine-layer hypothesis
isn't reproduced. They're landed regardless to keep the cancellation
contract pinned, so a future refactor that breaks unwind cleanup
(e.g., switching ``try/finally`` to ``ExitStack`` or moving
``_active_runs`` mutation outside the engine) fails visibly. Follow-up
investigation of #640 needs to look elsewhere — likely watcher /
client-state — and the issue will be updated with that finding
separately.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 2efe627 into main May 1, 2026
7 checks passed
@memtomem memtomem deleted the fix/issue-640-stream-cancel branch May 1, 2026 06:21
@github-actions github-actions Bot locked and limited conversation to collaborators May 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants