From 95b7c55c43738f6a5e9fb209e575648af36c36ab Mon Sep 17 00:00:00 2001 From: a2a-cloud Date: Mon, 13 Jul 2026 00:23:24 +0000 Subject: [PATCH] a2a-source-edit: write tests/test_agent.py --- tests/test_agent.py | 196 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 tests/test_agent.py diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000..f6f1eeb --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +import httpx + +from a2a_pack import LocalRunContext, LocalWorkspaceClient, NoAuth, WorkspaceAccess, WorkspaceMode + +from agent import ( + LEDGER_PATH, + ReceivesSupportEmailChecks1919313, + _action_digest, + _approval_phrase, + _normalize_refund_action, +) + + +class FakeResponse: + def __init__(self, status_code: int, payload: dict[str, Any]): + self.status_code = status_code + self._payload = payload + + def json(self) -> dict[str, Any]: + return self._payload + + +class MockAsyncClient: + queue: list[Any] = [] + calls: list[dict[str, Any]] = [] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + async def __aenter__(self) -> "MockAsyncClient": + return self + + async def __aexit__(self, *args: Any) -> None: + return None + + async def get(self, url: str, **kwargs: Any) -> FakeResponse: + self.calls.append({"method": "GET", "url": url, **kwargs}) + item = self.queue.pop(0) + if isinstance(item, BaseException): + raise item + return item + + async def post(self, url: str, **kwargs: Any) -> FakeResponse: + self.calls.append({"method": "POST", "url": url, **kwargs}) + item = self.queue.pop(0) + if isinstance(item, BaseException): + raise item + return item + + +def make_ctx() -> LocalRunContext[NoAuth]: + workspace = LocalWorkspaceClient( + {LEDGER_PATH: b'{"emails": {}, "refunds": {}}'}, + access=WorkspaceAccess.dynamic( + max_files=8, + allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), + require_reason=False, + ), + ) + workspace.outputs_prefix = "outputs" + workspace.write_prefixes = ("outputs/",) + workspace.current_mode = WorkspaceMode.READ_WRITE_OVERLAY + return LocalRunContext( + auth=NoAuth(), + workspace=workspace, + consumer_secrets={"STRIPE_SECRET_KEY": "sk_test_x", "SLACK_BOT_TOKEN": "xoxb-test"}, + consumer_config={"SLACK_CHANNEL_ID": "C123", "STRIPE_BASE_URL": "https://stripe.test", "SLACK_BASE_URL": "https://slack.test/api"}, + ) + + +def test_missing_setup_returns_clear_result() -> None: + async def run() -> None: + agent = ReceivesSupportEmailChecks1919313() + ctx = LocalRunContext(auth=NoAuth(), consumer_secrets={}, consumer_config={}) + result = await agent.process_support_email( + ctx, + sender_email="customer@example.com", + subject="Refund", + body="refund pi_abc123", + message_id="m1", + stripe_payment_intent_id="pi_abc123", + requested_refund_amount_cents=1000, + ) + assert result["status"] == "setup_required" + assert "STRIPE_SECRET_KEY" in result["missing"] + assert "SLACK_BOT_TOKEN" in result["missing"] + assert "SLACK_CHANNEL_ID" in result["missing"] + assert "sk_test" not in str(result) + + asyncio.run(run()) + + +def test_process_email_proposes_refund_and_dedupes(monkeypatch: Any) -> None: + async def run() -> None: + MockAsyncClient.queue = [ + FakeResponse(200, {"id": "pi_abc123", "status": "succeeded", "amount_received": 3000, "currency": "usd", "livemode": False, "latest_charge": {"amount_refunded": 0}}), + FakeResponse(200, {"ok": True, "ts": "1.2", "channel": "C123"}), + ] + MockAsyncClient.calls = [] + monkeypatch.setattr("agent.httpx.AsyncClient", MockAsyncClient) + agent = ReceivesSupportEmailChecks1919313() + ctx = make_ctx() + kwargs = dict( + sender_email="Customer ", + subject="Refund request", + body="Please refund payment pi_abc123", + message_id="msg-1", + stripe_payment_intent_id="pi_abc123", + requested_refund_amount_cents=2500, + currency="usd", + customer_name="Customer", + ) + first = await agent.process_support_email(ctx, **kwargs) + second = await agent.process_support_email(ctx, **kwargs) + assert first["status"] == "proposed" + assert first["approval_required"] is True + assert first["executed"] is False + assert first["slack"]["status"] == "posted" + assert "APPROVE-REFUND-" in first["approval_instructions"] + assert second["status"] == "duplicate_email" + assert second["previous_action_digest"] == first["action_digest"] + assert all("sk_test_x" not in str(call) for call in MockAsyncClient.calls) + + asyncio.run(run()) + + +def test_execute_requires_digest_bound_approval(monkeypatch: Any) -> None: + async def run() -> None: + monkeypatch.setattr("agent.httpx.AsyncClient", MockAsyncClient) + agent = ReceivesSupportEmailChecks1919313() + ctx = make_ctx() + action = _normalize_refund_action( + payment_intent_id="pi_abc123", + amount_cents=1200, + currency="usd", + customer_email="customer@example.com", + reason="requested_by_customer", + ) + digest = _action_digest(action) + wrong = await agent.execute_approved_refund( + ctx, + action_digest=digest, + approval_token="looks good", + stripe_payment_intent_id="pi_abc123", + amount_cents=1200, + customer_email="customer@example.com", + ) + assert wrong["status"] == "approval_required" + + MockAsyncClient.queue = [ + FakeResponse(200, {"id": "re_123", "status": "succeeded"}), + FakeResponse(200, {"ok": True, "ts": "2.3", "channel": "C123"}), + ] + approved = await agent.execute_approved_refund( + ctx, + action_digest=digest, + approval_token=_approval_phrase(digest), + stripe_payment_intent_id="pi_abc123", + amount_cents=1200, + customer_email="customer@example.com", + ) + assert approved["status"] == "executed" + assert approved["provider_refund_id"] == "re_123" + assert approved["idempotency_key"].startswith("a2a-refund-") + refund_call = next(call for call in MockAsyncClient.calls if call["url"].endswith("/v1/refunds")) + assert refund_call["headers"]["Idempotency-Key"] == approved["idempotency_key"] + + asyncio.run(run()) + + +def test_provider_timeout_returns_reconciliation(monkeypatch: Any) -> None: + async def run() -> None: + MockAsyncClient.queue = [httpx.TimeoutException("slow stripe")] + MockAsyncClient.calls = [] + monkeypatch.setattr("agent.httpx.AsyncClient", MockAsyncClient) + agent = ReceivesSupportEmailChecks1919313() + ctx = make_ctx() + result = await agent.process_support_email( + ctx, + sender_email="customer@example.com", + subject="Refund request", + body="Please refund payment pi_timeout", + message_id="msg-timeout", + stripe_payment_intent_id="pi_timeout", + requested_refund_amount_cents=1000, + ) + assert result["status"] == "needs_reconciliation" + assert result["policy"]["approval_required"] is True + assert result["executed"] is False + + asyncio.run(run())