feat: assistant IA — approbation groupee des actions, bloc d'etapes, refresh UI et bouton Stop (BUG-074, BUG-075, BUG-076, BUG-077)
CI / lint (push) Successful in 2m1s
CI / security (push) Successful in 1m25s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 1m26s
CI / e2e (push) Successful in 13m38s

This commit is contained in:
2026-09-24 10:05:32 -04:00
parent 9fb094f505
commit e1842043d8
23 changed files with 701 additions and 147 deletions
+75
View File
@@ -1065,6 +1065,81 @@ class TestBooksLMAgentEndpoint:
assert "C'est fait." in resp2.text
assert "event: message" in resp2.text
def test_agent_confirmation_batches_actions_and_confirm_all(self, bookslm_client, monkeypatch):
"""BUG-075: one confirmation lists every mutating call of the turn and
``confirm_all`` authorizes the rest of the run without pausing again."""
import re
import backend.bookslm_routes as routes
from backend.ai_chat import LLMResponse, ToolCall
from backend.tools import registry
from backend.tools.api import ToolRisk
from backend.tools.registry import ToolSpec
from backend.tools.schemas import ListVaultsInput
calls = []
def handler(ctx, params):
calls.append(params)
return {"ok": True}
spec = ToolSpec(
name="_agent_write_batch",
description="write for tests",
input_model=ListVaultsInput,
handler=handler,
risk=ToolRisk.WRITE,
)
monkeypatch.setitem(registry._REGISTRY, "_agent_write_batch", spec)
responses = [LLMResponse(tool_calls=[
ToolCall(id="c1", name="_agent_write_batch", arguments={}),
ToolCall(id="c2", name="_agent_write_batch", arguments={}),
])]
async def fake_chat_completion(messages, **kwargs):
return responses.pop(0)
monkeypatch.setattr(routes, "chat_completion", fake_chat_completion)
monkeypatch.setattr(routes, "_resolve_provider_name", lambda requested: "deepseek")
token, _ = _login_bookslm(bookslm_client)
payload = {"vault": "TestVault", "directory": "", "message": "crée tout", "mode": "directory"}
resp = bookslm_client.post(
"/api/ai/bookslm/agent",
json=payload,
headers={"Authorization": f"Bearer {token}"},
)
assert "event: confirmation" in resp.text
match = re.search(r"event: confirmation\ndata: (.*)", resp.text)
assert match, resp.text
confirmation = json.loads(match.group(1))
# Both mutating calls are pending, not one.
assert [a["tool"] for a in confirmation["pending"]["actions"]] == [
"_agent_write_batch", "_agent_write_batch",
]
assert calls == [], "nothing runs before the approval"
# Resume with a global approval: the two pending calls run, and a third
# mutating call of the same run runs without a second confirmation.
responses.append(LLMResponse(tool_calls=[ToolCall(id="c3", name="_agent_write_batch", arguments={})]))
responses.append(LLMResponse(content="Terminé."))
resume_payload = dict(
payload,
confirm=confirmation["pending"],
confirm_messages=confirmation["messages"],
confirm_all=True,
)
resp2 = bookslm_client.post(
"/api/ai/bookslm/agent",
json=resume_payload,
headers={"Authorization": f"Bearer {token}"},
)
assert resp2.status_code == 200
assert "event: confirmation" not in resp2.text, "confirm_all must not pause again"
assert len(calls) == 3, "the whole plan ran in one approval"
assert "Terminé." in resp2.text
def test_agent_message_reports_effective_model(self, bookslm_client, monkeypatch):
"""The SSE payload carries the model really used, not the raw request.