diff --git a/tests/test_full_stack_contract.py b/tests/test_full_stack_contract.py index 59cee34..79ec763 100644 --- a/tests/test_full_stack_contract.py +++ b/tests/test_full_stack_contract.py @@ -1,28 +1,134 @@ +import asyncio +import base64 +import json from pathlib import Path +from a2a_pack import LocalRunContext, LocalWorkspaceClient, PlatformUserAuth, WorkspaceMode from a2a_pack.cli.local import load_local_project +import agent as agent_module + ROOT = Path(__file__).resolve().parents[1] +def _auth(): + return PlatformUserAuth(sub="test-user", user_id=123, email="tester@example.com") + + +def _ctx(): + workspace = LocalWorkspaceClient( + {}, + access=agent_module.TypeDataCsvExcel7rh4pz057e66.workspace_access, + bucket="local", + issuer="pytest", + ) + workspace.outputs_prefix = "outputs/data-workflows" + workspace.write_prefixes = ("outputs/data-workflows/",) + workspace.current_mode = WorkspaceMode.READ_WRITE_OVERLAY + return LocalRunContext(auth=_auth(), workspace=workspace, 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") + migration = (ROOT / "db" / "migrations" / "001_app_records.sql").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" - - migrations = list((ROOT / "db" / "migrations").glob("*.sql")) - assert migrations and all(path.read_text(encoding="utf-8").strip() for path in migrations) + assert "DATABASE_URL" not in frontend + assert "execution_receipts" in migration card = load_local_project(ROOT).agent_cls().card() + skills = {skill.name: skill for skill in card.skills} + assert "create_workflow_from_browser_upload" in skills + assert "create_workflow_from_file_upload" in skills + assert "list_workflow_receipts" in skills + schema = skills["create_workflow_from_browser_upload"].input_schema + assert set(schema["required"]) >= {"source_file", "output_schema"} + assert "workflow_name" in schema["properties"] 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 and databases[0].migrations is not None assert databases[0].migrations.path == "db/migrations" + assert card.runtime.pricing.caller_pays_llm is False + + +def test_browser_upload_workflow_smoke_without_secrets(monkeypatch): + monkeypatch.delenv("DATABASE_URL", raising=False) + app = agent_module.TypeDataCsvExcel7rh4pz057e66() + csv_bytes = b"Name,Age,Email\nAda,37,ada@example.com\n" + result = asyncio.run( + app.local_invoke( + "create_workflow_from_browser_upload", + auth=_auth(), + workspace=_ctx().workspace, + source_file={ + "filename": "people.csv", + "media_type": "text/csv", + "data_base64": base64.b64encode(csv_bytes).decode("ascii"), + }, + output_schema={ + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "email": {"type": "string", "format": "email"}, + }, + "required": ["name", "email"], + "additionalProperties": False, + }, + workflow_name="people-normalizer", + ) + ) + assert result.status == "storage_warning" + assert result.source_format == "csv" + assert result.workflow_path.startswith("outputs/data-workflows/") + assert result.artifact_uri and not any("KEY" in warning for warning in result.warnings) + mapped_targets = {item["target_field"] for item in result.mapped_fields} + assert {"name", "age", "email"} <= mapped_targets + + +def test_invalid_schema_returns_structured_validation_error(): + app = agent_module.TypeDataCsvExcel7rh4pz057e66() + result = asyncio.run( + app.local_invoke( + "create_workflow_from_browser_upload", + auth=_auth(), + workspace=_ctx().workspace, + source_file={ + "filename": "data.json", + "media_type": "application/json", + "data_base64": base64.b64encode(b"[]").decode("ascii"), + }, + output_schema={"type": "array"}, + workflow_name="bad", + ) + ) + assert result.status == "validation_error" + assert result.persisted is False + assert "type='object'" in " ".join(result.warnings) + + +def test_workflow_spec_has_no_secret_fields(): + schema = {"type": "object", "properties": {"Name": {"type": "string"}}, "required": ["Name"]} + profile = agent_module._profile_source_data("x.csv", "text/csv", b"Name\nAda\n") + spec = agent_module._build_workflow_spec( + workflow_id="wf_test", + workflow_name="test", + filename="x.csv", + media_type="text/csv", + upload_mode="browser_base64", + profile=profile, + output_schema=schema, + receipt_id="rcpt_test", + ) + dumped = json.dumps(spec) + assert "DATABASE_URL" not in dumped + assert "OPENAI_API_KEY" not in dumped + assert spec["receipt"]["receipt_id"] == "rcpt_test"