feat(ai): services partages A2 + SSE streaming B4 + confirmations UI B5 (#79)
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
This commit is contained in:
@@ -186,6 +186,65 @@ class TestConfirmationAndLimits:
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestConfirmationResume:
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_applies_pending_and_continues(self, monkeypatch):
|
||||
seen = {}
|
||||
|
||||
def handler(ctx, params):
|
||||
seen["ctx_confirmed"] = ctx.confirmed
|
||||
return {"done": True}
|
||||
|
||||
_register(monkeypatch, "_write", handler, risk=ToolRisk.WRITE)
|
||||
|
||||
# First run pauses on the mutating tool.
|
||||
llm1 = ScriptedLLM([LLMResponse(tool_calls=[ToolCall(id="1", name="_write", arguments={"x": 1})])])
|
||||
paused = await run_agent([{"role": "user", "content": "write"}], ctx=_ctx(), llm=llm1)
|
||||
assert paused.stopped == STOP_CONFIRMATION_REQUIRED
|
||||
assert paused.pending["error"]["id"] == "1"
|
||||
|
||||
# Resume: the pending call is applied (one-shot confirm), then the loop
|
||||
# continues and produces the final answer.
|
||||
llm2 = ScriptedLLM([LLMResponse(content="applied")])
|
||||
resumed = await run_agent(
|
||||
[{"role": "user", "content": "write"}],
|
||||
ctx=_ctx(),
|
||||
llm=llm2,
|
||||
resume_messages=paused.messages,
|
||||
confirm_pending=paused.pending,
|
||||
)
|
||||
assert resumed.stopped == STOP_DONE
|
||||
assert resumed.content == "applied"
|
||||
assert len(resumed.tool_calls) == 1
|
||||
assert resumed.tool_calls[0].ok is True
|
||||
# The one-shot confirmation must not leak into the context.
|
||||
assert seen["ctx_confirmed"] is False
|
||||
# The tool result is fed back to the model on the resumed turn.
|
||||
tool_msgs = [m for m in llm2.calls[0]["messages"] if m.get("role") == "tool"]
|
||||
assert len(tool_msgs) == 1
|
||||
assert tool_msgs[0]["tool_call_id"] == "1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_without_assistant_message_reconstructs_it(self, monkeypatch):
|
||||
_register(monkeypatch, "_write", lambda ctx, params: {"done": True}, risk=ToolRisk.WRITE)
|
||||
llm = ScriptedLLM([LLMResponse(content="ok")])
|
||||
pending = {"error": {"tool": "_write", "arguments": {}, "id": "call_9"}}
|
||||
result = await run_agent(
|
||||
[{"role": "user", "content": "write"}],
|
||||
ctx=_ctx(),
|
||||
llm=llm,
|
||||
resume_messages=[{"role": "user", "content": "write"}],
|
||||
confirm_pending=pending,
|
||||
)
|
||||
assert result.stopped == STOP_DONE
|
||||
assistant_tool_msgs = [
|
||||
m for m in llm.calls[0]["messages"]
|
||||
if m.get("role") == "assistant" and m.get("tool_calls")
|
||||
]
|
||||
assert len(assistant_tool_msgs) == 1
|
||||
assert assistant_tool_msgs[0]["tool_calls"][0]["id"] == "call_9"
|
||||
|
||||
|
||||
class TestAgentPermissions:
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_denied_recorded(self, client):
|
||||
|
||||
Reference in New Issue
Block a user