Skip to content

test(terminal): prove the WS ownership refusal through the real route (#14960) - #15095

Merged
mrveiss merged 3 commits into
Dev_new_guifrom
issue-14960-route-ownership-test
Aug 26, 2026
Merged

mrveiss merged 3 commits into
Dev_new_guifrom
issue-14960-route-ownership-test

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Thinking Path

Closure audit of #14960 found four of its five acceptance criteria discharged by merged code
with route-level evidence, and one — "an authenticated user connecting to another user's
session id is rejected"
— covered only at handler level.

api/terminal_websocket_auth_test.py::TestTerminalWebsocketOwnership::test_non_owner_is_rejected
awaits terminal_websocket(ws, session_id) directly. That style of test cannot observe a
routing or dependency-resolution failure — which is precisely how #14998 hid: the same suite
stayed green for the entire period every real handshake 500'd inside solve_dependencies,
because a router-level Request-typed Depends cannot resolve in a WebSocket scope.

A rejection criterion is about what a client experiences, so it needs a test that reaches
the guard the way a client does. #15085 added exactly that shape for the authenticated and the
unauthenticated cases; the non-owner case was the one left behind. This adds it, and nothing
else — no production code changes, no change to authentication or authorization semantics.

What Changed

autobot-backend/api/terminal_websocket_route_test.py only.

  • New TestTerminalWebsocketRouteHandshake::test_authenticated_non_owner_is_refused: a real
    TestClient handshake on /ws/{session_id} as mallory against a session owned by alice.
    Asserts close code 1008 observed on the wire as a WebSocketDisconnect, that accept()
    never ran (so no terminal handler is constructed and no shell starts), and — via caplog —
    that the ownership branch fired, not the unknown-session branch of Unknown terminal session_id defaults to a live STANDARD-security terminal #14961. Both branches
    close 1008 from the same line, so without the log assertion the test would pass for the
    wrong reason.
  • WebSocketDisconnect hoisted to module scope and the two existing function-local imports
    dropped in favour of it. This is what keeps the file inside the 600-line cap: 599 lines,
    up from 584 minus the 4 lines the hoist recovers.

api/terminal.py is untouched and unchanged in size at its grandfathered 1299 lines.

Verification

Targeted run, this file only:

api/terminal_websocket_route_test.py ...................  [100%]
19 passed in 4.71s

Contrast mutation — the point of the change. Replacing the ownership comparison in
_lookup_terminal_session with a never-taken branch:

    owner = config.get("owner")
-   if not owner or owner != user.get("username"):
+   if False:  # MUTATION: ownership check removed
1 failed, 18 passed
FAILED ...::TestTerminalWebsocketRouteHandshake::test_authenticated_non_owner_is_refused
E   Failed: DID NOT RAISE <class 'starlette.websockets.WebSocketDisconnect'>
------------------------------ Captured log call ----------------------------
ERROR    services.simple_pty: Error in PTY read loop: [Errno 9] Bad file descriptor

Exactly one test fails, the new one, and it fails for the right reason: with the check removed
the non-owner's handshake is accepted and a real PTY is allocated for them — the captured
simple_pty error is that shell being torn down. The other 18 route tests, including both
pre-existing handshake tests, pass throughout: they could not see this.

Mutation reverted before commit; git diff --stat on the branch shows one file, +19/-4.

Local fastapi is 0.135.2 against CI's 0.141.1 (#15091). The assertion here is behavioural —
a real handshake — so it is unaffected by the deferred-include_router differences that made
introspection unusable in #14998; the file's structural checks already run through a
clean-interpreter subprocess dump and are untouched.

Model Used

Claude Opus 5 (1M context)

Refs #14960.

…#14960)

The non-owner rejection had handler-level coverage only: terminal_websocket_auth_test.py awaits terminal_websocket() directly, which cannot observe a routing or dependency-resolution failure - the lesson of #14998, where those same tests stayed green while every handshake 500'd.

Adds a TestClient handshake driving /ws/{session_id} as mallory against alice's session, asserting close code 1008 on the wire, that accept() never ran, and via caplog that the ownership branch fired rather than #14961's unknown-session branch (both close 1008 from the same line).

WebSocketDisconnect hoisted to module scope, reused by the two existing local imports, so the file stays under the 600-line cap at 599.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

github-actions Bot and others added 2 commits August 26, 2026 09:47
Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
@mrveiss
mrveiss merged commit 3b32f7b into Dev_new_gui Aug 26, 2026
69 checks passed
@mrveiss
mrveiss deleted the issue-14960-route-ownership-test branch August 26, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant