test_production_composition.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. import pytest
  5. from agent import AgentRunner, FileSystemTraceStore
  6. from script_build_host.domain.records import Principal
  7. from script_build_host.infrastructure.config import ScriptBuildSettings
  8. from script_build_host.production import (
  9. ProductionRuntimeDependencies,
  10. compose_production_host,
  11. )
  12. class _Principals:
  13. async def current(self, request_context: object | None = None) -> Principal:
  14. if request_context is None:
  15. raise PermissionError("request context is required")
  16. return Principal("owner")
  17. class _Authorizer:
  18. async def require_source_access(self, _principal: Principal, **_source: int) -> None:
  19. return None
  20. async def require_access(self, _principal: Principal, _script_build_id: int) -> None:
  21. return None
  22. def _runtime(tmp_path: Path) -> ProductionRuntimeDependencies:
  23. async def llm_call(*_args: object, **_kwargs: object) -> dict[str, str]:
  24. return {"content": "unused", "finish_reason": "stop"}
  25. return ProductionRuntimeDependencies(
  26. runner=AgentRunner(
  27. trace_store=FileSystemTraceStore(str(tmp_path / "traces")),
  28. llm_call=llm_call,
  29. ),
  30. principal_provider=_Principals(),
  31. build_authorizer=_Authorizer(),
  32. )
  33. def _settings(tmp_path: Path, **overrides: object) -> ScriptBuildSettings:
  34. decode = tmp_path / "decode.json"
  35. knowledge = tmp_path / "knowledge.json"
  36. decode.write_text(json.dumps({"items": []}), encoding="utf-8")
  37. knowledge.write_text(json.dumps({"items": []}), encoding="utf-8")
  38. values: dict[str, object] = {
  39. "environment": "test",
  40. "read_database_url": f"sqlite+aiosqlite:///{tmp_path / 'read.db'}",
  41. "write_database_url": f"sqlite+aiosqlite:///{tmp_path / 'write.db'}",
  42. "agent_data_root": tmp_path / "agent-data",
  43. "persona_root": tmp_path / "persona",
  44. "section_pattern_root": tmp_path / "sections",
  45. "decode_index_root": decode,
  46. "knowledge_root": knowledge,
  47. "prompt_fallback_root": tmp_path / "prompts",
  48. "websocket_allowed_origins": ("https://ui.example",),
  49. }
  50. values.update(overrides)
  51. return ScriptBuildSettings(**values) # type: ignore[arg-type]
  52. @pytest.mark.asyncio
  53. async def test_production_composition_wires_real_stores_and_closes_resources(
  54. tmp_path: Path,
  55. ) -> None:
  56. host = compose_production_host(_settings(tmp_path), _runtime(tmp_path))
  57. await host.validate_startup()
  58. manifest = await host.runtime_manifest.load()
  59. assert manifest["decode_index"]["file_count"] == 1
  60. assert manifest["knowledge"]["file_count"] == 1
  61. paths = set(host.composition.app.openapi()["paths"])
  62. assert "/api/pattern/script_builds" in paths
  63. assert any(path.startswith("/api/v2/script-builds/") for path in paths)
  64. assert (
  65. host.composition.coordinator.task_store.base_path
  66. == (tmp_path / "agent-data" / "task-ledger").resolve()
  67. )
  68. await host.close()
  69. await host.close()
  70. assert host.http_client.is_closed
  71. def test_production_requires_decode_service_and_durable_trace_store(tmp_path: Path) -> None:
  72. production = _settings(
  73. tmp_path,
  74. environment="production",
  75. read_database_url="mysql+asyncmy://reader:read@db.example/app",
  76. write_database_url="mysql+asyncmy://writer:write@db.example/app",
  77. )
  78. with pytest.raises(RuntimeError, match="DECODE_ENDPOINT"):
  79. compose_production_host(production, _runtime(tmp_path))
  80. runtime = _runtime(tmp_path)
  81. runtime.runner.trace_store = None
  82. with pytest.raises(RuntimeError, match="durable trace store"):
  83. compose_production_host(_settings(tmp_path), runtime)