* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
"""Wiring tests for the per-run cancel_id field.
|
|
|
|
A thread-scoped session_id is unsafe as a cancel key (a late stop POST can match
|
|
a later run on the same thread); cancel_id is a fresh per-generation UUID sent in
|
|
both the completion payload and the /api/inference/cancel body. Verifies the
|
|
field on backend/frontend types and the chat-adapter.ts generation + wiring."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
WORKSPACE = Path(__file__).resolve().parents[2]
|
|
MODELS_SRC = (WORKSPACE / "studio/backend/models/inference.py").read_text()
|
|
ROUTES_SRC = (WORKSPACE / "studio/backend/routes/inference.py").read_text()
|
|
ADAPTER_SRC = (WORKSPACE / "studio/frontend/src/features/chat/api/chat-adapter.ts").read_text()
|
|
API_TYPES_SRC = (WORKSPACE / "studio/frontend/src/features/chat/types/api.ts").read_text()
|
|
|
|
|
|
def _find_class(tree: ast.AST, name: str) -> ast.ClassDef | None:
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ClassDef) and node.name == name:
|
|
return node
|
|
return None
|
|
|
|
|
|
def test_chat_completion_request_has_cancel_id_field():
|
|
tree = ast.parse(MODELS_SRC)
|
|
cls = _find_class(tree, "ChatCompletionRequest")
|
|
assert cls is not None
|
|
fields = {
|
|
n.target.id
|
|
for n in cls.body
|
|
if isinstance(n, ast.AnnAssign) and isinstance(n.target, ast.Name)
|
|
}
|
|
assert (
|
|
"cancel_id" in fields
|
|
), "ChatCompletionRequest must expose a cancel_id field for per-run cancellation routing"
|
|
|
|
|
|
def test_cancel_route_matches_cancel_id_exclusively_when_present():
|
|
# A stale POST with cancel_id AND session_id must not cancel a later run via
|
|
# session_id; the handler must early-return through an exclusive-cancel_id path
|
|
# (atomic helper, or a keys list with ONLY cancel_id).
|
|
for node in ast.walk(ast.parse(ROUTES_SRC)):
|
|
if isinstance(node, ast.AsyncFunctionDef) and node.name == "cancel_inference":
|
|
break
|
|
else:
|
|
raise AssertionError("cancel_inference handler missing")
|
|
|
|
cancel_id_exclusive_branch = False
|
|
for sub in ast.walk(node):
|
|
if not isinstance(sub, ast.If):
|
|
continue
|
|
test_src = ast.unparse(sub.test)
|
|
if "cancel_id" not in test_src or "isinstance" not in test_src:
|
|
continue
|
|
branch_src = "\n".join(ast.unparse(s) for s in sub.body)
|
|
before_return = branch_src.split("return", 1)[0]
|
|
matches_cancel_id_only = (
|
|
"_cancel_by_cancel_id_or_stash(cancel_id)" in branch_src
|
|
or "_cancel_by_keys([cancel_id])" in branch_src
|
|
)
|
|
if matches_cancel_id_only and "session_id" not in before_return:
|
|
cancel_id_exclusive_branch = True
|
|
break
|
|
assert cancel_id_exclusive_branch, (
|
|
"cancel_inference must early-return with an exclusive cancel_id "
|
|
"match when a cancel_id is supplied, so a stale stop POST "
|
|
"cannot cancel a later run on the same thread via session_id"
|
|
)
|
|
|
|
|
|
def test_cancel_route_falls_back_to_session_or_completion_when_no_cancel_id():
|
|
for node in ast.walk(ast.parse(ROUTES_SRC)):
|
|
if isinstance(node, ast.AsyncFunctionDef) and node.name == "cancel_inference":
|
|
break
|
|
else:
|
|
raise AssertionError("cancel_inference handler missing")
|
|
|
|
src = ast.unparse(node)
|
|
assert "session_id" in src and "completion_id" in src, (
|
|
"cancel_inference must still accept session_id / completion_id as "
|
|
"fallback keys when cancel_id is absent"
|
|
)
|
|
|
|
|
|
def test_frontend_request_type_has_cancel_id():
|
|
assert re.search(
|
|
r"cancel_id\?\s*:\s*string\s*;", API_TYPES_SRC
|
|
), "OpenAIChatCompletionsRequest must expose an optional cancel_id"
|
|
|
|
|
|
def test_chat_adapter_generates_cancel_id_per_run():
|
|
m = re.search(
|
|
r"const\s+cancelId\s*=\s*([^;]+);",
|
|
ADAPTER_SRC,
|
|
)
|
|
assert m, "chat-adapter.ts must declare a per-run `cancelId` constant"
|
|
rhs = m.group(1)
|
|
assert "randomUUID" in rhs, "cancelId should prefer crypto.randomUUID() for uniqueness"
|
|
|
|
|
|
def test_chat_adapter_sends_cancel_id_in_completion_payload():
|
|
assert "cancel_id: cancelId" in ADAPTER_SRC, (
|
|
"chat-adapter.ts must include cancel_id in the streamChatCompletions "
|
|
"request payload so the backend registers under that key"
|
|
)
|
|
|
|
|
|
def test_chat_adapter_sends_cancel_id_in_abort_cancel_post():
|
|
m = re.search(
|
|
r"const\s+onAbortCancel\s*=\s*\(\)\s*=>\s*\{(.*?)\};",
|
|
ADAPTER_SRC,
|
|
flags = re.DOTALL,
|
|
)
|
|
assert m, "onAbortCancel arrow function missing"
|
|
body = m.group(1)
|
|
assert re.search(r"cancel_id\s*:\s*cancelId", body), (
|
|
"onAbortCancel must include cancel_id in the /inference/cancel body "
|
|
"so a stop POST matches the specific run, not the whole thread"
|
|
)
|
|
|
|
|
|
def test_abort_cancel_post_uses_plain_fetch_with_manual_auth_header():
|
|
# authFetch redirects to login on 401, kicking the user out mid-stop if the
|
|
# token expired. Use plain fetch + manual Authorization for a best-effort cancel.
|
|
start = ADAPTER_SRC.find("const onAbortCancel")
|
|
assert start >= 0, "onAbortCancel handler missing"
|
|
rest = ADAPTER_SRC[start:]
|
|
end = rest.find("\n try {")
|
|
body = rest if end < 0 else rest[:end]
|
|
assert "/api/inference/cancel" in body
|
|
assert "authFetch(" not in body, (
|
|
"onAbortCancel must NOT call authFetch; a 401 from it would "
|
|
"redirect the user to the login page during a stop click"
|
|
)
|
|
assert "fetch(" in body, "onAbortCancel must use plain fetch(...)"
|
|
assert "getAuthToken" in body, (
|
|
"onAbortCancel must read the bearer token via getAuthToken() "
|
|
"rather than relying on authFetch's 401 flow"
|
|
)
|
|
assert "Authorization" in body
|
|
assert (
|
|
"keepalive: true" in body
|
|
), "keepalive is required so the fetch survives page unload during stop"
|