from pathlib import Path import pytest from nisse_dev.config import AgentConfig, ConfigError COMPLETE = { "NISSE_STATE_DB": "~/demo/state.db", "NISSE_WORKSPACE": "~/demo/work", "NISSE_PROJECT": "greet", "NISSE_LLM_BASE_URL": "https://litellm.test", "NISSE_LLM_TOKEN": "sk-x", "NISSE_ROLE_EXECUTOR": "claude-haiku-4-5", "NISSE_ROLE_PLANNER": "claude-opus-5", "NISSE_ROLE_DELEGATE": "claude-opus-5", "NISSE_ROLE_CONSULTANT": "claude-haiku-4-5", "NISSE_MATRIX_HOMESERVER": "https://matrix.test", "NISSE_MATRIX_USER_ID": "@nisse-dev-testing:test", "NISSE_MATRIX_TOKEN": "syt_x", "NISSE_MATRIX_ROOM_ID": "!room:test", "NISSE_MATRIX_OPERATORS": "@don:test", "NISSE_GITEA_URL": "https://gitea.test", "NISSE_GITEA_TOKEN": "gt", "NISSE_GITEA_REPO": "don/demo", "NISSE_GITEA_OPERATORS": "don", } def test_complete_environment_builds() -> None: config = AgentConfig.from_env(COMPLETE) assert config.project == "greet" assert config.roles.planner == "claude-opus-5" assert config.matrix.room_id == "!room:test" assert config.gitea_repo == "don/demo" assert config.base_branch == "main" def test_paths_expand_the_home_directory() -> None: config = AgentConfig.from_env(COMPLETE) assert config.workspace.is_absolute() assert "~" not in str(config.state_db) def test_every_missing_variable_is_reported_at_once() -> None: # Failing at first use would surface a missing token twenty minutes into # a milestone, on the call that mattered. partial = {k: v for k, v in COMPLETE.items() if k != "NISSE_MATRIX_TOKEN"} partial.pop("NISSE_GITEA_REPO") with pytest.raises(ConfigError) as excinfo: AgentConfig.from_env(partial) message = str(excinfo.value) assert "NISSE_MATRIX_TOKEN" in message assert "NISSE_GITEA_REPO" in message def test_empty_is_treated_as_missing() -> None: with pytest.raises(ConfigError, match="NISSE_GITEA_TOKEN"): AgentConfig.from_env({**COMPLETE, "NISSE_GITEA_TOKEN": " "}) def test_operator_lists_are_split_and_trimmed() -> None: config = AgentConfig.from_env( { **COMPLETE, "NISSE_MATRIX_OPERATORS": " @don:test , @other:test ,", "NISSE_GITEA_OPERATORS": "don,", } ) assert config.operators == {"@don:test", "@other:test"} # A trailing comma must not admit an empty identity into an allowlist. assert "" not in config.operators assert config.gitea_operators == {"don"} def test_base_branch_is_overridable() -> None: config = AgentConfig.from_env({**COMPLETE, "NISSE_BASE_BRANCH": "trunk"}) assert config.base_branch == "trunk" def test_redacted_summary_carries_no_secrets() -> None: summary = AgentConfig.from_env(COMPLETE).redacted() flat = " ".join(summary.values()) for secret in ("sk-x", "syt_x", "gt"): assert secret not in flat assert summary["project"] == "greet" def test_state_db_and_workspace_are_separate(tmp_path: Path) -> None: # A2: build artefacts must not share a volume with agent state. config = AgentConfig.from_env(COMPLETE) assert config.state_db.parent != config.workspace