"""The composition root. These tests exist because two components — the delegation harness and the comment poller — were fully built, fully tested, and reachable from nothing but their own tests. Unit tests that construct a component directly cannot catch that; only assembly can.""" import sqlite3 from pathlib import Path from typing import Any import httpx import pytest from test_config import COMPLETE from nisse_dev.agent import build_agent, prepare_workspace from nisse_dev.config import AgentConfig from nisse_dev.git import GitRepo def config_for(tmp_path: Path, **overrides: str) -> AgentConfig: env = { **COMPLETE, "NISSE_STATE_DB": str(tmp_path / "state.db"), "NISSE_WORKSPACE": str(tmp_path / "work"), **overrides, } return AgentConfig.from_env(env) @pytest.fixture def origin(tmp_path: Path) -> Path: """A bare repo standing in for the project on the forge.""" remote = tmp_path / "origin.git" GitRepo(tmp_path)._run("init", "-q", "--bare", "-b", "main", str(remote)) seed = GitRepo(tmp_path / "seed") seed.init() seed.configure_identity("seed", "seed@test") (seed.root / "README.md").write_text("project\n") seed.commit_all("seed") seed.push(str(remote), "main") return remote def test_workspace_is_cloned_when_absent(tmp_path: Path, origin: Path) -> None: config = config_for(tmp_path, NISSE_GITEA_URL=f"file://{origin}") # authenticated_url would mangle a file:// remote, so clone directly. GitRepo(tmp_path).clone(str(origin), config.workspace) repo = prepare_workspace(config, "nisse-dev-testing") assert (config.workspace / "README.md").exists() assert repo.current_branch() == "main" def test_workspace_is_reused_when_already_cloned(tmp_path: Path, origin: Path) -> None: config = config_for(tmp_path) GitRepo(tmp_path).clone(str(origin), config.workspace) (config.workspace / "local-work.txt").write_text("in progress") prepare_workspace(config, "nisse-dev-testing") # Re-cloning would silently destroy uncommitted work. assert (config.workspace / "local-work.txt").exists() class FakeTransport: def __init__(self) -> None: self.sent: list[str] = [] self.started = False self.closed = False def send_message(self, text: str) -> None: self.sent.append(text) def send_approval_request(self, text: str) -> None: self.sent.append(text) def ask_question(self, text: str) -> None: self.sent.append(text) def on_message(self, callback: Any) -> None: self.callback = callback def start(self) -> None: self.started = True def close(self) -> None: self.closed = True def build_offline(tmp_path: Path, origin: Path, monkeypatch: Any) -> Any: """Assemble the real graph with only the network boundaries faked.""" config = config_for(tmp_path) GitRepo(tmp_path).clone(str(origin), config.workspace) monkeypatch.setattr( "nisse_dev.agent.GiteaClient.whoami", lambda self: "nisse-dev-testing" ) monkeypatch.setattr("nisse_dev.agent.MatrixTransport", lambda cfg: FakeTransport()) monkeypatch.setattr( "nisse_dev.agent.authenticated_url", lambda base, repo, user, token: str(origin), ) return build_agent(config) def test_the_whole_graph_assembles(tmp_path: Path, origin: Path, monkeypatch) -> None: agent = build_offline(tmp_path, origin, monkeypatch) orchestrator = agent.orchestrator # Every component that was previously unreachable is now connected. assert orchestrator._executor is not None assert orchestrator._publisher is not None assert orchestrator._board is not None assert orchestrator._delegate is not None assert orchestrator._poller_factory is not None assert orchestrator._executor._delegate is not None # A12 escalation path assert orchestrator._executor._expert is not None # A14 consult path agent.stop() def test_start_announces_where_it_resumed( tmp_path: Path, origin: Path, monkeypatch ) -> None: agent = build_offline(tmp_path, origin, monkeypatch) agent.start() assert agent.transport.started readiness = agent.transport.sent[-1] assert "nisse-dev up as nisse-dev-testing" in readiness assert "greet" in readiness # the project assert "claude-opus-5" in readiness # the pinned planner agent.stop() def test_stop_closes_everything_and_releases_the_wait( tmp_path: Path, origin: Path, monkeypatch ) -> None: agent = build_offline(tmp_path, origin, monkeypatch) agent.start() agent.stop() assert agent.transport.closed agent.wait() # returns immediately rather than hanging with pytest.raises(sqlite3.ProgrammingError): agent.store.get_state() # the store really is closed def test_poller_factory_ignores_the_agents_own_comments( tmp_path: Path, origin: Path, monkeypatch ) -> None: # Without this the agent answers its own PR replies in a loop. agent = build_offline(tmp_path, origin, monkeypatch) poller = agent.orchestrator._poller_factory(lambda c: None) assert "nisse-dev-testing" in poller._ignore agent.stop() def test_delegation_uses_the_passthrough_route( tmp_path: Path, origin: Path, monkeypatch ) -> None: # A13: delegations run vendor-native; the normalized route is for the # places where fidelity is cheap. from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient agent = build_offline(tmp_path, origin, monkeypatch) delegation = agent.orchestrator._delegate assert isinstance(delegation._client, AnthropicPassthroughClient) assert isinstance(agent.orchestrator._executor._client, LiteLLMClient) agent.stop() def test_httpx_clients_are_not_shared_between_routes( tmp_path: Path, origin: Path, monkeypatch ) -> None: agent = build_offline(tmp_path, origin, monkeypatch) normalized = agent.orchestrator._executor._client._http passthrough = agent.orchestrator._delegate._client._http assert isinstance(normalized, httpx.Client) assert normalized is not passthrough # different base URLs and headers agent.stop()