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:
1 parent
b0e5aaf599
commit
2a47c6771d
14 files changed
+583
-9
No files matched your search
+47
-1
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user