a2a-source-edit: write tests/test_full_stack_contract.py
This commit is contained in:
@@ -1,136 +1,9 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
|
||||||
from a2a_pack import LocalRunContext, PlatformUserAuth
|
|
||||||
from a2a_pack.cli.local import load_local_project
|
from a2a_pack.cli.local import load_local_project
|
||||||
|
|
||||||
from agent import QuoteJudgeStudioV1
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SUCCESS_QUOTES = [
|
|
||||||
{"vendor": "Acme Bearings", "unit_price": 10.75, "quantity": 500, "delivery_days": 12, "warranty_months": 12},
|
|
||||||
{"vendor": "Beta Industrial", "unit_price": 9.1, "quantity": 500, "delivery_days": 8, "warranty_months": 18},
|
|
||||||
]
|
|
||||||
SUCCESS_WEIGHTS = {"price": 50, "delivery": 30, "warranty": 20}
|
|
||||||
|
|
||||||
|
|
||||||
class FakeCursor:
|
|
||||||
def __init__(self, row=None):
|
|
||||||
self._row = row
|
|
||||||
|
|
||||||
def fetchone(self):
|
|
||||||
return self._row
|
|
||||||
|
|
||||||
|
|
||||||
class FakeConn:
|
|
||||||
store = {}
|
|
||||||
receipts = []
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc, tb):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, sql, params):
|
|
||||||
normalized = " ".join(sql.lower().split())
|
|
||||||
if normalized.startswith("insert into quote_comparisons"):
|
|
||||||
tenant, comparison_id, vendor, score, payload_json = params
|
|
||||||
self.store[(tenant, comparison_id)] = json.loads(payload_json)
|
|
||||||
return FakeCursor()
|
|
||||||
if normalized.startswith("select payload from quote_comparisons"):
|
|
||||||
tenant, comparison_id = params
|
|
||||||
payload = self.store.get((tenant, comparison_id))
|
|
||||||
return FakeCursor((payload,) if payload else None)
|
|
||||||
if normalized.startswith("insert into quote_execution_receipts"):
|
|
||||||
self.receipts.append(params)
|
|
||||||
return FakeCursor()
|
|
||||||
raise AssertionError(f"unexpected SQL: {sql}")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def fake_db(monkeypatch):
|
|
||||||
FakeConn.store = {}
|
|
||||||
FakeConn.receipts = []
|
|
||||||
monkeypatch.setenv("DATABASE_URL", "postgresql://example.invalid/quotejudge")
|
|
||||||
monkeypatch.setattr("agent._connect", lambda: FakeConn())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ctx():
|
|
||||||
return LocalRunContext(
|
|
||||||
auth=PlatformUserAuth(sub="user-123", user_id=123, email="buyer@example.com", scopes=["agent:invoke"]),
|
|
||||||
task_id="test-task",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_success_failure_reload_and_receipt(ctx):
|
|
||||||
agent = QuoteJudgeStudioV1()
|
|
||||||
success = await agent.invoke_json(
|
|
||||||
"compare_quotes",
|
|
||||||
ctx,
|
|
||||||
{"comparison_id": "studio-quote-v1", "quotes": SUCCESS_QUOTES, "weights": SUCCESS_WEIGHTS},
|
|
||||||
)
|
|
||||||
assert success["ok"] is True
|
|
||||||
assert success["comparison_id"] == "studio-quote-v1"
|
|
||||||
assert success["recommendation"]["vendor"] == "Beta Industrial"
|
|
||||||
assert success["quotes"][0]["weighted_score"] == 100.0
|
|
||||||
assert FakeConn.receipts, "production execution receipt should be persisted"
|
|
||||||
|
|
||||||
failure = await agent.invoke_json(
|
|
||||||
"compare_quotes",
|
|
||||||
ctx,
|
|
||||||
{
|
|
||||||
"comparison_id": "studio-quote-invalid",
|
|
||||||
"quotes": [{"vendor": "Only Vendor", "unit_price": 10, "quantity": 1, "delivery_days": 5, "warranty_months": 6}],
|
|
||||||
"weights": SUCCESS_WEIGHTS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert failure["ok"] is False
|
|
||||||
assert failure["code"] == "at_least_two_quotes_required"
|
|
||||||
|
|
||||||
reload = await agent.invoke_json("get_comparison", ctx, {"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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_browser_upload_bridge(ctx):
|
|
||||||
agent = QuoteJudgeStudioV1()
|
|
||||||
csv_text = "vendor,unit_price,quantity,delivery_days,warranty_months\nAcme Bearings,10.75,500,12,12\nBeta Industrial,9.10,500,8,18\n"
|
|
||||||
result = await agent.invoke_json(
|
|
||||||
"compare_quotes_upload",
|
|
||||||
ctx,
|
|
||||||
{
|
|
||||||
"comparison_id": "upload-quote-v1",
|
|
||||||
"upload": {
|
|
||||||
"filename": "quotes.csv",
|
|
||||||
"media_type": "text/csv",
|
|
||||||
"data_base64": base64.b64encode(csv_text.encode()).decode(),
|
|
||||||
},
|
|
||||||
"weights": SUCCESS_WEIGHTS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert result["ok"] is True
|
|
||||||
assert result["recommendation"]["vendor"] == "Beta Industrial"
|
|
||||||
|
|
||||||
invalid = await agent.invoke_json(
|
|
||||||
"compare_quotes_upload",
|
|
||||||
ctx,
|
|
||||||
{
|
|
||||||
"comparison_id": "bad-upload",
|
|
||||||
"upload": {"filename": "quotes.csv", "media_type": "text/csv", "data_base64": "%%%not-base64%%%"},
|
|
||||||
"weights": SUCCESS_WEIGHTS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert invalid["ok"] is False
|
|
||||||
assert invalid["code"] == "invalid_base64"
|
|
||||||
|
|
||||||
|
|
||||||
def test_full_stack_product_contract():
|
def test_full_stack_product_contract():
|
||||||
@@ -139,30 +12,17 @@ def test_full_stack_product_contract():
|
|||||||
frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8")
|
frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "frontend:" in manifest and "mount: /app" in manifest
|
assert "frontend:" in manifest and "mount: /app" in manifest
|
||||||
assert "public: false" in manifest
|
|
||||||
assert "resources:" in manifest and "databases:" in manifest
|
assert "resources:" in manifest and "databases:" in manifest
|
||||||
assert "migrations:" in manifest and "db/migrations" in manifest
|
assert "migrations:" in manifest and "db/migrations" in manifest
|
||||||
assert "PlatformUserAuth" in source
|
assert "PlatformUserAuth" in source
|
||||||
assert "AgentPlatformResources" in source and "AgentDatabase(" in source
|
assert "AgentPlatformResources" in source and "AgentDatabase(" in source
|
||||||
assert "Skill runner" not in frontend
|
assert "Skill runner" not in frontend, "replace the generic scaffold with the product workflow"
|
||||||
assert "QuoteJudge" in frontend
|
|
||||||
assert "data_base64" in frontend
|
|
||||||
assert "DATABASE_URL" not in frontend
|
|
||||||
|
|
||||||
migrations = list((ROOT / "db" / "migrations").glob("*.sql"))
|
migrations = list((ROOT / "db" / "migrations").glob("*.sql"))
|
||||||
assert migrations and all(path.read_text(encoding="utf-8").strip() for path in migrations)
|
assert migrations and all(path.read_text(encoding="utf-8").strip() for path in migrations)
|
||||||
|
|
||||||
card = load_local_project(ROOT).agent_cls().card()
|
card = load_local_project(ROOT).agent_cls().card()
|
||||||
skill_names = {skill.name for skill in card.skills}
|
|
||||||
assert {"compare_quotes", "get_comparison", "compare_quotes_upload", "compare_quotes_file"} <= skill_names
|
|
||||||
compare_schema = next(skill.input_schema for skill in card.skills if skill.name == "compare_quotes")
|
|
||||||
assert set(compare_schema["required"]) == {"comparison_id", "quotes", "weights"}
|
|
||||||
upload_schema = next(skill.input_schema for skill in card.skills if skill.name == "compare_quotes_file")
|
|
||||||
assert upload_schema["properties"]["document"]["x-a2a-file-upload"]["max_bytes"] == 65536
|
|
||||||
databases = card.runtime.platform_resources.databases
|
databases = card.runtime.platform_resources.databases
|
||||||
assert databases, "live Agent Card must declare its managed database"
|
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 is not None
|
||||||
assert databases[0].migrations.path == "db/migrations"
|
assert databases[0].migrations.path == "db/migrations"
|
||||||
assert card.mcp_endpoint == "/mcp"
|
|
||||||
assert card.runtime.pricing.caller_pays_llm is False
|
|
||||||
|
|||||||
Reference in New Issue
Block a user