From f8ee428614b8d1049eafb405550c23d3b4d47475 Mon Sep 17 00:00:00 2001 From: a2a-cloud Date: Sat, 18 Jul 2026 06:10:06 +0000 Subject: [PATCH] a2a-source-edit: write tests/test_full_stack_contract.py --- tests/test_full_stack_contract.py | 110 +++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/tests/test_full_stack_contract.py b/tests/test_full_stack_contract.py index 59cee34..20b441f 100644 --- a/tests/test_full_stack_contract.py +++ b/tests/test_full_stack_contract.py @@ -1,28 +1,136 @@ +import asyncio +import base64 +import json from pathlib import Path +from a2a_pack.auth import PlatformUserAuth from a2a_pack.cli.local import load_local_project +from a2a_pack.context import LocalRunContext + +from agent import QuoteJudgeStudioV1, _LOCAL_COMPARISONS, _LOCAL_RECEIPTS ROOT = Path(__file__).resolve().parents[1] +AUTH = PlatformUserAuth(sub="user-123", user_id=123, email="buyer@example.com") + + +def run(coro): + return asyncio.run(coro) + + +def ctx(): + return LocalRunContext(auth=AUTH, task_id="test-task", caller="pytest") def test_full_stack_product_contract(): manifest = (ROOT / "a2a.yaml").read_text(encoding="utf-8") source = (ROOT / "agent.py").read_text(encoding="utf-8") frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8") + browser_client = (ROOT / "frontend" / "src" / "a2a.js").read_text(encoding="utf-8") assert "frontend:" in manifest and "mount: /app" in manifest + assert "public: false" in manifest assert "resources:" in manifest and "databases:" in manifest assert "migrations:" in manifest and "db/migrations" in manifest + assert "runtime:" in manifest and "max_runtime_seconds: 600" in manifest assert "PlatformUserAuth" in source assert "AgentPlatformResources" in source and "AgentDatabase(" in source - assert "Skill runner" not in frontend, "replace the generic scaffold with the product workflow" + assert "LLMProvisioning" not in source and "ctx.llm" not in source + assert "Skill runner" not in frontend + assert "QuoteJudge Studio" in frontend and "compare_quotes" in frontend + assert "authorizeUrl" in browser_client and "loginUrl" in browser_client + forbidden = ["DATABASE_URL", "A2A_LITELLM", "OPENAI_API_KEY", ".svc.cluster.local"] + assert not any(secret in frontend for secret in forbidden) migrations = list((ROOT / "db" / "migrations").glob("*.sql")) assert migrations and all(path.read_text(encoding="utf-8").strip() for path in migrations) card = load_local_project(ROOT).agent_cls().card() + skill_names = {skill.name for skill in card.skills} + assert {"compare_quotes", "get_comparison", "compare_quotes_from_browser_upload", "compare_quotes_from_file"} <= skill_names databases = card.runtime.platform_resources.databases assert databases, "live Agent Card must declare its managed database" + assert databases[0].scope == "user" assert databases[0].migrations is not None assert databases[0].migrations.path == "db/migrations" + assert card.runtime.pricing.caller_pays_llm is False + + +def test_success_failure_reload_and_receipt_contract(): + _LOCAL_COMPARISONS.clear() + _LOCAL_RECEIPTS.clear() + agent = QuoteJudgeStudioV1() + quotes = [ + {"vendor": "Acme Bearings", "quantity": 500, "unit_price": 10.75, "delivery_days": 12, "warranty_months": 12}, + {"vendor": "Beta Industrial", "quantity": 500, "unit_price": 9.1, "delivery_days": 8, "warranty_months": 18}, + ] + weights = {"price": 50, "delivery": 30, "warranty": 20} + + success = run(agent.local_invoke("compare_quotes", auth=AUTH, comparison_id="studio-quote-v1", quotes=quotes, weights=weights)) + assert success["ok"] is True + assert success["comparison_id"] == "studio-quote-v1" + assert success["recommendation"]["vendor"] == "Beta Industrial" + assert success["receipt"]["receipt_id"].startswith("qjr_") + assert success["receipt"]["persisted"] is True + assert _LOCAL_RECEIPTS and _LOCAL_RECEIPTS[-1]["schema"] == "quotejudge.production_receipt.v1" + assert "tenant_key" not in json.dumps(success) + + reload = run(agent.local_invoke("get_comparison", auth=AUTH, comparison_id="studio-quote-v1")) + assert reload["ok"] is True + assert reload["comparison_id"] == "studio-quote-v1" + assert reload["recommendation"]["vendor"] == "Beta Industrial" + assert reload["reloaded"] is True + + failure = run(agent.local_invoke( + "compare_quotes", + auth=AUTH, + comparison_id="studio-quote-invalid", + quotes=[{"vendor": "Only Vendor", "quantity": 1, "unit_price": 10, "delivery_days": 5, "warranty_months": 6}], + weights=weights, + )) + assert failure == {"ok": False, "code": "at_least_two_quotes_required", "comparison_id": "studio-quote-invalid"} + + +def test_browser_upload_bridge_success_and_invalid_base64(): + _LOCAL_COMPARISONS.clear() + agent = QuoteJudgeStudioV1() + payload = base64.b64encode(json.dumps({ + "quotes": [ + {"vendor": "Acme Bearings", "quantity": 500, "unit_price": 10.75, "delivery_days": 12, "warranty_months": 12}, + {"vendor": "Beta Industrial", "quantity": 500, "unit_price": 9.1, "delivery_days": 8, "warranty_months": 18}, + ] + }).encode()).decode() + + result = run(agent.local_invoke( + "compare_quotes_from_browser_upload", + auth=AUTH, + comparison_id="upload-quote-v1", + documents=[{"filename": "quotes.json", "media_type": "application/json", "data_base64": payload}], + weights={"price": 50, "delivery": 30, "warranty": 20}, + )) + assert result["ok"] is True + assert result["recommendation"]["vendor"] == "Beta Industrial" + assert result["source"] == "browser_upload" + assert result["parsed_documents"] == [{"filename": "quotes.json", "quotes": 2}] + + invalid = run(agent.local_invoke( + "compare_quotes_from_browser_upload", + auth=AUTH, + comparison_id="upload-bad", + documents=[{"filename": "quotes.json", "media_type": "application/json", "data_base64": "not base64 ***"}], + weights={"price": 50, "delivery": 30, "warranty": 20}, + )) + assert invalid["ok"] is False + assert invalid["code"] == "invalid_base64" + + +def test_mcp_tool_schemas_are_bounded_and_typed(): + card = load_local_project(ROOT).agent_cls().card() + skills = {skill.name: skill for skill in card.skills} + schema = skills["compare_quotes"].input_schema + assert schema["type"] == "object" + assert schema["additionalProperties"] is False + assert set(schema["required"]) == {"comparison_id", "quotes", "weights"} + assert schema["properties"]["comparison_id"]["type"] == "string" + assert "quotes" in schema["properties"] + assert skills["compare_quotes_from_file"].input_schema["properties"]["document"]["x-a2a-file-upload"]["max_bytes"] == 64 * 1024