a2a-source-edit: write tests/test_full_stack_contract.py
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from a2a_pack import LocalRunContext, PlatformUserAuth
|
||||
from a2a_pack.cli.local import load_local_project
|
||||
|
||||
from agent import BrowserDocument, QuoteInput, QuoteJudgeStudioV1, QuoteWeights, _parse_browser_documents
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
SUCCESS_ARGS = {
|
||||
"comparison_id": "studio-quote-v1",
|
||||
"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},
|
||||
],
|
||||
"weights": {"price": 50, "delivery": 30, "warranty": 20},
|
||||
}
|
||||
|
||||
|
||||
def _ctx():
|
||||
return LocalRunContext(
|
||||
auth=PlatformUserAuth(sub="test-user", user_id=42, email="quote@example.test"),
|
||||
task_id="test-task",
|
||||
caller="pytest",
|
||||
)
|
||||
|
||||
|
||||
def test_full_stack_product_contract():
|
||||
manifest = (ROOT / "a2a.yaml").read_text(encoding="utf-8")
|
||||
@@ -12,17 +34,87 @@ 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 "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
|
||||
assert "config.json" not in frontend, "frontend should use a2a.js config loader rather than hard-code platform metadata"
|
||||
assert "A2A_LITELLM" not in frontend and "DATABASE_URL" not in frontend
|
||||
|
||||
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", "upload_quotes", "upload_quote_file"}.issubset(skill_names)
|
||||
databases = card.runtime.platform_resources.databases
|
||||
assert databases, "live Agent Card must declare its managed database"
|
||||
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_compare_reload_failure_and_receipt():
|
||||
agent = QuoteJudgeStudioV1()
|
||||
ctx = _ctx()
|
||||
|
||||
result = asyncio.run(agent.compare_quotes(
|
||||
ctx,
|
||||
SUCCESS_ARGS["comparison_id"],
|
||||
[QuoteInput.model_validate(item) for item in SUCCESS_ARGS["quotes"]],
|
||||
QuoteWeights.model_validate(SUCCESS_ARGS["weights"]),
|
||||
))
|
||||
assert result["ok"] is True
|
||||
assert result["comparison_id"] == "studio-quote-v1"
|
||||
assert result["recommendation"]["vendor"] == "Beta Industrial"
|
||||
assert result["receipt"]["persisted"] is True
|
||||
assert "receipt_id" in result["receipt"]
|
||||
|
||||
reopened = asyncio.run(agent.get_comparison(ctx, "studio-quote-v1"))
|
||||
assert reopened["ok"] is True
|
||||
assert reopened["recommendation"]["vendor"] == "Beta Industrial"
|
||||
|
||||
failure = asyncio.run(agent.compare_quotes(
|
||||
ctx,
|
||||
"studio-quote-invalid",
|
||||
[QuoteInput(vendor="Only Vendor", unit_price=10, quantity=1, delivery_days=5, warranty_months=6)],
|
||||
QuoteWeights(price=50, delivery=30, warranty=20),
|
||||
))
|
||||
assert failure["ok"] is False
|
||||
assert failure["code"] == "at_least_two_quotes_required"
|
||||
assert "action" in failure
|
||||
|
||||
|
||||
def test_browser_upload_bridge_bounds_and_parses():
|
||||
payload = json.dumps({"quotes": SUCCESS_ARGS["quotes"]}).encode("utf-8")
|
||||
parsed = _parse_browser_documents([
|
||||
BrowserDocument(filename="quotes.json", media_type="application/json", data_base64=base64.b64encode(payload).decode("ascii"))
|
||||
])
|
||||
assert parsed["ok"] is True
|
||||
assert len(parsed["quotes"]) == 2
|
||||
|
||||
too_large = base64.b64encode(b"x" * (64 * 1024 + 1)).decode("ascii")
|
||||
invalid = _parse_browser_documents([
|
||||
BrowserDocument(filename="too-big.txt", media_type="text/plain", data_base64=too_large)
|
||||
])
|
||||
assert invalid["ok"] is False
|
||||
assert invalid["code"] == "upload_too_large"
|
||||
|
||||
bad_b64 = _parse_browser_documents([
|
||||
BrowserDocument(filename="bad.csv", media_type="text/csv", data_base64="not valid base64")
|
||||
])
|
||||
assert bad_b64["ok"] is False
|
||||
assert bad_b64["code"] == "invalid_base64"
|
||||
|
||||
|
||||
def test_mcp_typed_tool_schemas_are_present():
|
||||
card = QuoteJudgeStudioV1().card()
|
||||
schemas = {skill.name: skill.input_schema for skill in card.skills}
|
||||
compare_schema = schemas["compare_quotes"]
|
||||
assert set(compare_schema["required"]) == {"comparison_id", "quotes", "weights"}
|
||||
assert "quotes" in compare_schema["properties"]
|
||||
assert schemas["upload_quote_file"]["properties"]["file"]["x-a2a-file-upload"]["max_bytes"] == 64 * 1024
|
||||
|
||||
Reference in New Issue
Block a user