test_production_composition.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. "read_database_socks_proxy": None,
  42. "write_database_url": f"sqlite+aiosqlite:///{tmp_path / 'write.db'}",
  43. "agent_data_root": tmp_path / "agent-data",
  44. "persona_root": tmp_path / "persona",
  45. "section_pattern_root": tmp_path / "sections",
  46. "decode_index_root": decode,
  47. "knowledge_root": knowledge,
  48. "prompt_fallback_root": tmp_path / "prompts",
  49. "websocket_allowed_origins": ("https://ui.example",),
  50. }
  51. values.update(overrides)
  52. return ScriptBuildSettings(**values) # type: ignore[arg-type]
  53. @pytest.mark.asyncio
  54. async def test_production_composition_wires_real_stores_and_closes_resources(
  55. tmp_path: Path,
  56. ) -> None:
  57. host = compose_production_host(_settings(tmp_path, phase_two_max_tasks=7), _runtime(tmp_path))
  58. await host.validate_startup()
  59. manifest = await host.runtime_manifest.load()
  60. assert manifest["decode_index"]["file_count"] == 1
  61. assert manifest["knowledge"]["file_count"] == 1
  62. paths = set(host.composition.app.openapi()["paths"])
  63. assert "/api/pattern/script_builds" in paths
  64. assert any(path.startswith("/api/v2/script-builds/") for path in paths)
  65. assert (
  66. host.composition.coordinator.task_store.base_path
  67. == (tmp_path / "agent-data" / "task-ledger").resolve()
  68. )
  69. assert host.composition.phase_two_planning.limits.max_tasks == 7
  70. await host.close()
  71. await host.close()
  72. assert host.http_client.is_closed
  73. def test_production_requires_decode_service_and_durable_trace_store(tmp_path: Path) -> None:
  74. production = _settings(
  75. tmp_path,
  76. environment="production",
  77. read_database_url="mysql+asyncmy://reader:read@db.example/app",
  78. write_database_url="mysql+asyncmy://writer:write@db.example/app",
  79. final_database_url="mysql+asyncmy://publisher:publish@db.example/app",
  80. )
  81. with pytest.raises(RuntimeError, match="DECODE_ENDPOINT"):
  82. compose_production_host(production, _runtime(tmp_path))
  83. runtime = _runtime(tmp_path)
  84. runtime.runner.trace_store = None
  85. with pytest.raises(RuntimeError, match="durable trace store"):
  86. compose_production_host(_settings(tmp_path), runtime)
  87. external = _runtime(tmp_path)
  88. external.runner.trace_store = object()
  89. with pytest.raises(RuntimeError, match="must provide a DurableTraceStoreVerifier"):
  90. compose_production_host(_settings(tmp_path), external)
  91. @pytest.mark.asyncio
  92. async def test_production_expands_one_internal_model_to_all_script_roles(tmp_path: Path) -> None:
  93. host = compose_production_host(
  94. _settings(tmp_path, role_model="qwen3.7-max", role_max_iterations=90),
  95. _runtime(tmp_path),
  96. )
  97. manifest = host.composition.mission_service.phase_two_model_manifest
  98. presets = manifest["presets"]
  99. assert isinstance(presets, dict)
  100. assert len(presets) == 16
  101. assert presets["script_root_worker"]["model"] == "qwen3.7-max"
  102. assert presets["script_root_validator"]["max_iterations"] == 90
  103. await host.close()