Refactor entry-side deletion refusal to execute in-transaction via
`_write_guards_reject(conn, sid)` (#123583), per maintainer review:
- Underlying `delete_session` and `delete_sessions` now accept an opt-in
kwarg `exclude_active_write_guards=True` running inside `_do` write
transaction, eliminating the race condition where a turn acquires the lease
between check and delete.
- Raises `SessionActiveWriteGuardError` when refusing single delete, leaving
the row untouched; `delete_sessions` atomically skips active rows.
- Checks both active turn leases and compression locks via the existing
reclaim-aware `_write_guards_reject` helper.
- Covers all user-facing delete sinks:
* Web `DELETE /api/sessions/{id}` -> 409 Conflict
* Web `POST /api/sessions/bulk-delete` -> skips active rows
* Web / CLI `prune` -> passes `exclude_active_write_guards=True` so lineage
parents of active conversations are not pruned
* API Server `DELETE /api/sessions/{id}` -> 409 session_active_turn
* CLI `hermes sessions delete` & `export --delete-after-verified` -> exits 1
* CLI browse picker -> refuses active delete
* TUI Gateway `session.delete` -> 4023 error
- Conforms to rubric with 2 targeted invariant tests in
`tests/hermes_state/test_delete_session_write_guards.py`.
- Updates user guide and web dashboard docs for 409 / exit 1.
(cherry picked from commit 2c037a7a79dc211b49bacc72e3140951ccf900cf)
Restored (each proven red against a sabotaged production path, green on main):
- tests/agent/test_overflow_overhead_aware_tokens.py::
test_overflow_recovery_compresses_with_tool_overhead_counted[413,
context_overflow, prompt_too_long, long_context_tier_429]
Rewritten from the four estimator-to-compressor tests: the real estimator
runs with a ~10k-token tool schema and the compressor must receive an
approx_tokens that counts it (LCM issue 441). No helper-name pinning.
- tests/hermes_cli/test_web_server_sessiondb_eventloop.py::
test_bulk_delete_sessiondb_work_runs_off_event_loop,
test_session_read_handlers_run_sessiondb_work_off_event_loop[search,stats,detail]
Real route handlers; every SessionDB open/call lands off the event-loop
thread (#60747). AST/source tests stay deleted.
- tests/hermes_state/test_hermes_state.py::TestCompressionChainProjection::
test_list_batches_tip_row_fetch_into_one_query
list_sessions_rich resolves a page of compression tips in one batched
query, not one per root (N+1 guard).
- tests/hermes_state/test_tail_rewind_semantics.py::TestCompressTagsCarriedTail::
test_compress_marks_carried_forward_tail_dicts
Repaired from a skip-on-exception body that asserted nothing: compress()
tags exactly the verbatim carried tail so archive_and_compact gets an
accurate tail_count (#86366).
- tests/hermes_state/test_wal_checkpoint_strategy.py::
test_failed_checkpoint_never_raises_and_is_not_silent,
test_failed_checkpoint_at_close_still_releases_the_handle
Periodic PASSIVE checkpoint failure neither raises into the write path nor
passes silently; a failing checkpoint at close() still releases the
handle (#45383 strategy). Log-wording asserts dropped.
- tests/hermes_state/test_hermes_state_ux_messages.py::
test_db_unavailable_details_line_carries_raw_cause_only_below_the_lead,
test_db_unavailable_network_drive_cause_does_not_offer_doctor_fix
CLI details=True keeps the raw SQLite cause on its own Details line only;
a locking-protocol (network FS) cause never offers `doctor --fix`.
- tests/tools/test_code_execution.py::TestStubSchemaDrift::
test_generated_module_accepts_all_params
Replaces the syntax-only test_generated_module_compiles: executes the
generated hermes_tools module (uds and file transports) and asserts every
stub accepts all its params as kwargs and forwards each by name and value.
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
`GET /api/sessions/search`, `/api/sessions/stats` and `/api/sessions/{id}` were
the last three session reads still doing their SQLite (including FTS5 search)
inline in an `async def` handler. On a large state.db the FTS query blocked the
dashboard loop, so every other request — status polls, the WS transport the
Desktop rides on — stalled until it returned. #65893 moved messages/timeline/
delete/export to `asyncio.to_thread` but never touched these three.
Route all three through the existing `_with_db` helper under `asyncio.to_thread`,
matching the sibling handlers in the same module. No behaviour change in the
responses.
Before/after (real app + real SessionDB via TestClient, temp HERMES_HOME):
SQLite opens on the loop thread across the 3 routes: 3/4 → 0/4.
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.
Parallel directories for one source package, folded into the mirror:
tests/acp -> tests/acp_adapter (its __init__/conftest move with it)
tests/cli -> tests/hermes_cli (prompt_toolkit fixture merged into
hermes_cli/conftest.py)
tests/run_agent -> tests/agent (backoff fixture becomes
agent/conftest.py)
tests/relay -> tests/gateway/relay
tests/state -> tests/hermes_state
246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.
Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.
Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).
Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.