feat(conversations,api,ui): rewind takes the last turns of a thread back, the cut session stays in the store

This commit is contained in:
hh committed 2026-10-13 02:10:03 +02:00
1 parent b0e5aaf599
commit 2a47c6771d
14 files changed
+583 -9

No files matched your search

+47 -1
View File
@@ -16,7 +16,7 @@ from claude_agent_sdk import (
UserMessage,
)
from httpx import ASGITransport, AsyncClient
from test_conversations import ScriptedClient, World
from test_conversations import ScriptedClient, World, _three_turns
from beaver_gateway.frontends.markdown.history import rewrite_messages
from beaver_gateway.security.auth import TokenStore
@@ -444,6 +444,52 @@ async def test_sessions_show_their_processes_and_take_orders(world: World) -> No
assert kinds == ["session_interrupt", "session_close"]
async def test_rewind_lists_turns_and_needs_admin_scope(world: World) -> None:
api = Api(world)
api.store.grant("api-only", "weak", scope="api")
weak = {"Authorization": "Bearer weak"}
sid, entries = await _three_turns(world)
conv = await world.conversations.create(
kind="master", agent="a", origin="test", session_id=sid
)
path = f"/conversations/{conv.external_id}"
listed = (await api.http.get(f"{path}/turns", headers=weak)).json()
assert [t["text"] for t in listed["turns"]] == ["two", "three"]
assert (await api.get(f"{path}/turns", {"limit": 1}))["turns"] == [
listed["turns"][1]
]
body = {"turn": listed["turns"][0]["id"]}
denied = await api.http.post(f"{path}/rewind", json=body, headers=weak)
assert denied.status_code == 403
missing = await api.http.post(
f"{path}/rewind", json={"turn": entries[0]["uuid"]}, headers=HEADERS
)
assert missing.status_code == 404
done = await api.http.post(f"{path}/rewind", json=body, headers=HEADERS)
assert done.status_code == 200, done.text
out = done.json()
assert out["turns"] == 2 and out["previous_session"] == sid
assert (await api.get(path))["session_id"] == out["session_id"] != sid
assert (await api.get(f"{path}/turns"))["turns"] == []
messages = (await api.get(f"{path}/history"))["messages"]
assert [m["content"] for m in messages] == [
"one",
[{"type": "text", "text": "done one"}],
]
(record,) = (await api.get("/audit"))["records"]
assert record["kind"] == "api_rewind"
assert record["detail"]["previous_session"] == sid
ScriptedClient.hold = asyncio.Event()
await world.conversations.post(conv, "busy")
await asyncio.sleep(0.2)
busy = await api.http.post(f"{path}/rewind", json=body, headers=HEADERS)
assert busy.status_code == 409
ScriptedClient.hold.set()
async def test_tokens_and_audit_need_admin_scope(world: World) -> None:
api = Api(world)
api.store.grant("api-only", "weak", scope="api")