diff --git a/tests/test_full_stack_contract.py b/tests/test_full_stack_contract.py index 59cee34..3973991 100644 --- a/tests/test_full_stack_contract.py +++ b/tests/test_full_stack_contract.py @@ -1,10 +1,24 @@ -from pathlib import Path +from __future__ import annotations +import asyncio +import os +from pathlib import Path +from unittest.mock import patch + +from a2a_pack import LocalRunContext, PlatformUserAuth from a2a_pack.cli.local import load_local_project +from agent import QuoteInput, QuoteJudgeStudioV1, QuoteWeights, _build_comparison_result + ROOT = Path(__file__).resolve().parents[1] +SUCCESS_QUOTES = [ + QuoteInput(vendor="Acme Bearings", unit_price=10.75, quantity=500, delivery_days=12, warranty_months=12), + QuoteInput(vendor="Beta Industrial", unit_price=9.1, quantity=500, delivery_days=8, warranty_months=18), +] +SUCCESS_WEIGHTS = QuoteWeights(price=50, delivery=30, warranty=20) + def test_full_stack_product_contract(): manifest = (ROOT / "a2a.yaml").read_text(encoding="utf-8") @@ -12,17 +26,79 @@ def test_full_stack_product_contract(): frontend = (ROOT / "frontend" / "src" / "App.jsx").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 "PlatformUserAuth" in source assert "AgentPlatformResources" in source and "AgentDatabase(" in source + assert "LLMProvisioning" not in source + assert "ctx.llm" not in source assert "Skill runner" not in frontend, "replace the generic scaffold with the product workflow" + forbidden = ["A2A_LITELLM_KEY", "OPENAI_API_KEY", "svc.cluster.local", "DATABASE_URL"] + 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_upload"}.issubset(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_scoring_success_fixture_is_deterministic(): + result = _build_comparison_result("studio-quote-v1", SUCCESS_QUOTES, SUCCESS_WEIGHTS) + assert result["ok"] is True + assert result["comparison_id"] == "studio-quote-v1" + assert result["recommendation"]["vendor"] == "Beta Industrial" + beta = result["comparison_table"][0] + assert beta["vendor"] == "Beta Industrial" + assert beta["total_price"] == 4550.0 + assert beta["scores"]["weighted_total"] == 100.0 + + +def test_failure_fixture_returns_actionable_validation(): + result = _build_comparison_result( + "studio-quote-invalid", + [QuoteInput(vendor="Only Vendor", unit_price=10, quantity=1, delivery_days=5, warranty_months=6)], + SUCCESS_WEIGHTS, + ) + assert result["ok"] is False + assert result["code"] == "at_least_two_quotes_required" + assert result["validation_errors"] + + +def test_success_and_reload_persist_with_receipt(): + saved: dict[tuple[str, str], dict] = {} + receipts: list[dict] = [] + + async def fake_save(tenant_key, comparison_id, result): + saved[(tenant_key, comparison_id)] = result + + async def fake_load(tenant_key, comparison_id): + return saved.get((tenant_key, comparison_id)) + + async def fake_receipt(ctx, tenant_key, comparison_id, tool_name, status, inputs, result): + receipt = {"persisted": True, "receipt_id": f"receipt-{tool_name}-{comparison_id}", "input_hash": "abc"} + receipts.append({"tenant_key": tenant_key, "comparison_id": comparison_id, "tool_name": tool_name, "status": status}) + return receipt + + async def run(): + agent = QuoteJudgeStudioV1() + ctx = LocalRunContext(auth=PlatformUserAuth(sub="user-1", user_id=1, email="u@example.com")) + with patch("agent._save_comparison", fake_save), patch("agent._load_comparison", fake_load), patch("agent._persist_receipt", fake_receipt): + success = await agent.invoke("compare_quotes", ctx, comparison_id="studio-quote-v1", quotes=SUCCESS_QUOTES, weights=SUCCESS_WEIGHTS) + reload = await agent.invoke("get_comparison", ctx, comparison_id="studio-quote-v1") + assert success["ok"] is True + assert success["recommendation"]["vendor"] == "Beta Industrial" + assert success["receipt"]["persisted"] is True + assert reload["ok"] is True + assert reload["recommendation"]["vendor"] == "Beta Industrial" + assert len(receipts) >= 2 + + asyncio.run(run())