fix(ai): creation dossier+fichier en mode agent (BUG-050)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# tests/test_agent_loop.py — Unit tests for the in-app agent loop (Phase B)
|
||||
"""Tests for backend.agent.loop.run_agent using a scripted (mocked) LLM."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.agent.loop import (
|
||||
@@ -244,6 +246,59 @@ class TestConfirmationResume:
|
||||
assert len(assistant_tool_msgs) == 1
|
||||
assert assistant_tool_msgs[0]["tool_calls"][0]["id"] == "call_9"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_with_parallel_calls_keeps_conversation_valid(self, monkeypatch):
|
||||
"""BUG-050: pausing on one tool call of a batch must answer the others.
|
||||
|
||||
The assistant message lists every tool call of the response, so the
|
||||
provider rejects the resumed turn when a ``tool_call_id`` has no tool
|
||||
result (the "create a folder and a file inside" scenario).
|
||||
"""
|
||||
_register(monkeypatch, "_write", lambda ctx, params: {"done": params}, risk=ToolRisk.WRITE)
|
||||
|
||||
llm1 = ScriptedLLM([LLMResponse(tool_calls=[
|
||||
ToolCall(id="1", name="_write", arguments={"x": 1}),
|
||||
ToolCall(id="2", name="_write", arguments={"x": 2}),
|
||||
])])
|
||||
paused = await run_agent([{"role": "user", "content": "write both"}], ctx=_ctx(), llm=llm1)
|
||||
assert paused.stopped == STOP_CONFIRMATION_REQUIRED
|
||||
assert paused.pending["error"]["id"] == "1"
|
||||
|
||||
# The call that was not reached is answered right away; the pending one
|
||||
# gets its result on resume, when the user applies it.
|
||||
answered = {m["tool_call_id"] for m in paused.messages if m.get("role") == "tool"}
|
||||
assert "2" in answered
|
||||
assert "1" not in answered
|
||||
|
||||
# Resume: the pending call is applied, the next turn stays valid.
|
||||
llm2 = ScriptedLLM([LLMResponse(content="ok")])
|
||||
resumed = await run_agent(
|
||||
[{"role": "user", "content": "write both"}],
|
||||
ctx=_ctx(),
|
||||
llm=llm2,
|
||||
resume_messages=paused.messages,
|
||||
confirm_pending=paused.pending,
|
||||
)
|
||||
assert resumed.stopped == STOP_DONE
|
||||
assert resumed.content == "ok"
|
||||
assert len(resumed.tool_calls) == 1
|
||||
assert resumed.tool_calls[0].ok is True
|
||||
# Before the resumed LLM call, every announced tool_call_id is answered.
|
||||
resumed_messages = llm2.calls[0]["messages"]
|
||||
assistant = next(
|
||||
m for m in resumed_messages
|
||||
if m.get("role") == "assistant" and m.get("tool_calls")
|
||||
)
|
||||
announced = {tc["id"] for tc in assistant["tool_calls"]}
|
||||
answered = {m["tool_call_id"] for m in resumed_messages if m.get("role") == "tool"}
|
||||
assert announced <= answered
|
||||
# The skipped call is flagged "deferred" so the model can re-issue it.
|
||||
deferred = [
|
||||
m for m in resumed_messages
|
||||
if m.get("role") == "tool" and json.loads(m["content"]).get("status") == "deferred"
|
||||
]
|
||||
assert [m["tool_call_id"] for m in deferred] == ["2"]
|
||||
|
||||
|
||||
class TestAgentPermissions:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -271,6 +271,12 @@ class TestDirectoryCRUD:
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
|
||||
def test_create_directory_existing_conflicts(self, client):
|
||||
"""The REST endpoint stays strict: an existing folder is a 409."""
|
||||
client.post("/api/directory/TestVault", json={"path": "Dup"})
|
||||
resp = client.post("/api/directory/TestVault", json={"path": "Dup"})
|
||||
assert resp.status_code == 409
|
||||
|
||||
def test_rename_directory(self, client):
|
||||
# Create first
|
||||
client.post("/api/directory/TestVault", json={"path": "OldName"})
|
||||
|
||||
@@ -143,6 +143,13 @@ class TestCreate:
|
||||
assert result.ok
|
||||
assert (_vault_path() / "A" / "B").is_dir()
|
||||
|
||||
def test_create_directory_idempotent(self, client):
|
||||
"""BUG-050: re-creating an existing folder is a success for the AI layer."""
|
||||
call_tool("create_directory", _ctx(), {"vault": "TestVault", "path": "Idem/Dir"}, confirm=True)
|
||||
second = call_tool("create_directory", _ctx(), {"vault": "TestVault", "path": "Idem/Dir"}, confirm=True)
|
||||
assert second.ok
|
||||
assert second.data.get("existed") is True
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# D2. Edit / append / rename / move
|
||||
|
||||
Reference in New Issue
Block a user