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

This commit is contained in:
2026-09-11 17:06:40 -04:00
parent c55e3e0cbc
commit 4c4e415975
27 changed files with 1465 additions and 386 deletions
+59
View File
@@ -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):