main
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b1451cdda |
refactor(runtime): centralize async bridges under an owned runtime (#376)
* feat(runtime): add application-scoped async runtime * refactor(cli): use owned runtime for session stats * refactor(onboard): use the owned async runtime * docs(runtime): record async bridge ownership * refactor(middleware): keep sync fallback synchronous * refactor(mcp): load tools on an owned runtime * refactor(cli): share owned runtime across entry points * refactor(channels): make inbound sync bridge explicit * refactor(stream): run Rich streaming on owned runtime * chore(runtime): remove nest-asyncio dependency * refactor(asyncio): require active loops in async code * docs(runtime): document final event loop ownership * fix(stream): cancel stalled owned streams * fix(cli): recover cleanly from stream cancellation * fix(runtime): drain executor work before shutdown * fix(runtime): terminate cancelled shell process trees * fix(models): let fallback bypass selector failures * fix(cli): reset interrupt handling between turns * docs: rm implementation spec * fix(serve): cancel active turns during shutdown * fix(runtime): protect settlement from waiter cancellation * fix(backends): reject empty shell commands * fix(runtime): terminate descendants after shell exit * fix(mcp): keep standalone discovery off channel loop * fix(cli): own and settle interactive prompt cancellation * fix(serve): keep channel sends off runtime loop * fix(stream): scope cancel context to iterator steps * refactor(serve): require the owned async runtime * fix(channels): keep interactive sends off runtime loop * fix(selector): surface fallback without log spam * test(runtime): normalize Windows shell marker * fix(cli): serialize interactive session turns * fix(shell): bound output drain after termination * fix(ui): do not retry owned runtime failures * fix(shell): allow signal-safe registry reentry * fix(shell): avoid terminating reused process ids * fix(channels): preserve streaming send order * fix(cli): report runtime shutdown timeouts cleanly * fix(mcp): guide async callers to async loader * docs(runtime): clarify reserved async bridge APIs * fix(runtime): bound code interpreter cleanup * test(shell): use active Python for drain regression --------- Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com> |
||
|
|
690b903f85 |
test: standardize async tests on pytest-asyncio auto mode (#338)
* chore: add pytest-asyncio in auto mode * test: migrate channel and stream tests to native async Convert run_async() wrapper tests to plain 'async def test_*' under pytest-asyncio auto mode. collect_events() in stream_v3_fakes becomes a coroutine awaited at every call site. * test: migrate command and model/middleware tests to native async Convert run_async() wrappers (import, alias, and fixture forms) to plain 'async def test_*'. Multi-call tests merge onto one loop as sequential awaits; none asserted on loop identity. * test: migrate TUI, notifier, gateway, and session tests to native async TUI/notifier/gateway files convert run_async wrappers to plain async tests. test_sessions.py's unittest.TestCase classes move to unittest.IsolatedAsyncioTestCase (pytest-asyncio does not await async methods on plain TestCase; converting blindly would have made ~70 tests silently vacuous). Its setUpClass keeps a one-shot asyncio.run() since IsolatedAsyncioTestCase has no async class-level hook. TestLoadingWidget in test_tui_widgets.py drops its TestCase base for the same reason. * test: replace direct asyncio.run() calls with native async tests Convert tests that called asyncio.run() (directly or via a local _run helper) to plain 'async def test_*'; delete the local helpers. * test: drop undeclared anyio markers and delete run_async helper The @pytest.mark.anyio tests relied on anyio being a transitive dep of httpx; auto-mode pytest-asyncio collects them natively. run_async() and its fixture are unreferenced after the migration, so remove them — pytest-asyncio's per-test loop teardown covers the pending-task cancellation the helper existed for (verified: full suite runs with no 'Event loop is closed' errors or destroyed-task warnings). |
||
|
|
7568a6bc1f |
fix(tui): keep welcome banner at top after /new (#311)
* fix(tui): keep welcome banner at top after /new PR #262 replaced scroll_end() with anchor() for free-scrolling. When /new clears a long anchored conversation, the anchor kept the viewport pinned to the (now empty) bottom, producing a negative scroll_y and pushing the welcome banner out of view. Reset the anchor and scroll to the top in clear_chat(), and restore the follow/new-content flags so the fresh session starts correctly. Closes #301 * fix(tui): suppress anchor when chat content fits viewport The previous fix for #301 only handled the /new path. din0s reported that the banner still dropped to the bottom after a normal short turn (user types 'hi', agent replies) — i.e. whenever the conversation fit in the viewport. Root cause is in Textual's compositor (textual._compositor): when a widget is anchored, scroll_y is recomputed via set_reactive, which bypasses the validator. If the anchored widget's content is shorter than the viewport, scroll_y goes negative on the next layout pass and the welcome banner is pushed below the visible region. PR #262 made _stream_with_widgets re-engage the anchor at the end of every turn via _anchor_chat, so the bug surfaced on any short reply that fit in the viewport. Markdown re-renders, status-bar updates, or any subsequent mount would then trip the compositor. Fix in three places: * _anchor_chat: only engage the anchor when max_scroll_y > 0; otherwise release and scroll_home so the banner stays at the top. * streaming anchor loop: if content shrinks below the viewport mid-stream (e.g. loading widget removed), release the anchor instead of leaving _anchored=True for the compositor to trip on. * clear_chat: keep the unconditional reset (children are removed asynchronously so a max_scroll_y check would be stale) but document why. Adds two regressions: * test_short_turn_keeps_banner_at_top_after_layout_refresh — the exact scenario din0s tested; fails with scroll_y=-10 on the previous code, passes with the fix. * test_long_turn_keeps_viewport_pinned_to_bottom — guards against regressing free-scrolling for overflowing conversations. Manually verified: 'hi' -> reply (banner stays at top) -> /new (banner at top) -> another turn (banner stays at top). * test(tui): address review feedback on banner-position regressions - extract `_release_anchor_and_pin_top` helper for the 3-line `anchor(False) + scroll_home(...)` pattern repeated in `clear_chat`, `_anchor_chat`, and the streaming loop - replace `pytest.skip` in `_capture_app` with a hard `RuntimeError` so a broken capture never silently passes - drop the redundant `load_agent` and `create_session_workspace` monkeypatches (the factory is given those as parameters, so the module-level symbols never run; added a comment explaining why) - add a defensive `_FakeChannelRuntime` patch for symmetry with the other module-level fakes - drop the local `_run` helper and use the `run_async` fixture from `conftest.py` (its teardown is better) * test(tui): replace _FakeChannelRuntime with _auto_start_channel no-op The _FakeChannelRuntime patch was ineffective because ChannelRuntime is just a dataclass — the real channel manager still started via _auto_start_channel, leaving pending tasks and non-hermetic test state. Per review feedback, stub _auto_start_channel directly instead. |