CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
Answering "is the workspace where the agent runs, or what it works on?" — it is what it works on, and conflating the two is dangerous rather than merely untidy. The agent commits its workspace with git add -A and pushes it, so anything inside the clone reaches the project repository. A .env placed there would publish the LiteLLM, Matrix and Gitea tokens as a side effect of the agent working correctly: silently, once, and probably unnoticed. Startup now refuses both that and a state store inside the clone, and .env.example documents the intended layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
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
|
|
|
|
|
|
def test_state_db_inside_the_clone_is_refused(tmp_path: Path) -> None:
|
|
# git add -A would commit it, and the push would publish the agent's
|
|
# own state alongside the project.
|
|
work = tmp_path / "project"
|
|
with pytest.raises(ConfigError, match="inside the workspace"):
|
|
AgentConfig.from_env(
|
|
{
|
|
**COMPLETE,
|
|
"NISSE_WORKSPACE": str(work),
|
|
"NISSE_STATE_DB": str(work / "state.db"),
|
|
}
|
|
)
|
|
|
|
|
|
def test_env_file_inside_the_clone_is_refused(tmp_path: Path) -> None:
|
|
# The severe case: the agent would commit and push its own credentials
|
|
# as a side effect of working correctly.
|
|
work = tmp_path / "project"
|
|
with pytest.raises(ConfigError, match="NISSE_ENV"):
|
|
AgentConfig.from_env(
|
|
{
|
|
**COMPLETE,
|
|
"NISSE_WORKSPACE": str(work),
|
|
"NISSE_STATE_DB": str(tmp_path / "state.db"),
|
|
"NISSE_ENV": str(work / ".env"),
|
|
}
|
|
)
|
|
|
|
|
|
def test_sibling_paths_are_fine(tmp_path: Path) -> None:
|
|
config = AgentConfig.from_env(
|
|
{
|
|
**COMPLETE,
|
|
"NISSE_WORKSPACE": str(tmp_path / "project"),
|
|
"NISSE_STATE_DB": str(tmp_path / "state.db"),
|
|
"NISSE_ENV": str(tmp_path / ".env"),
|
|
}
|
|
)
|
|
assert config.workspace.name == "project"
|