Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
539 changes: 539 additions & 0 deletions tests/conftest.py

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions tests/test_agent_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import asyncio
import importlib
import sys


def test_agent_drain_turn_tracks_delegate_phase_and_session(
tools_lab, fake_claude_sdk, monkeypatch, capsys
):
lab = tools_lab(remote=False, local=True)
sys.modules.pop("agent", None)
agent = importlib.import_module("agent")
agent.RUN_DIR = str(lab.workspace / "runs" / "test")
(lab.workspace / "runs" / "test").mkdir(parents=True)
phases = []
monkeypatch.setattr(agent, "_set_phase", phases.append)
fake_claude_sdk.plan_client(
turns=[
fake_claude_sdk.turn(
fake_claude_sdk.assistant(
fake_claude_sdk.tool_use(
"Agent", {"subagent_type": "reader"}, "call-1"
),
),
fake_claude_sdk.assistant(
fake_claude_sdk.tool_use("mcp__cas__submit_local"),
parent_tool_use_id="call-1",
),
fake_claude_sdk.result("success", "agent-session"),
)
],
contexts=[None],
)
client = fake_claude_sdk.module.ClaudeSDKClient(options=object())

async def run():
await client.query("turn")
await agent.drain_turn(client, 2)
await agent._context_task

asyncio.run(run())

assert any("waiting on subagent reader" in phase for phase in phases)
assert any("subagent reader (submit_local)" in phase for phase in phases)
assert agent._session_id == "agent-session"
assert "[round 2 turn end] success" in capsys.readouterr().out
58 changes: 58 additions & 0 deletions tests/test_claude_harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import asyncio
import importlib


def test_engineer_answer_records_query_and_session(
fake_claude_sdk, monkeypatch, tmp_path, capsys
):
monkeypatch.setenv("LAB_DIR", str(tmp_path))
monkeypatch.setenv("SLACK_INBOX", str(tmp_path / "run" / "inbox.md"))
sys_module = importlib.import_module("sys")
sys_module.modules.pop("engineer", None)
monkeypatch.syspath_prepend(str(tmp_path.parent / "AgentLab" / "framework"))
engineer = importlib.import_module("framework.engineer")
engineer.SESSION_FILE = str(tmp_path / "run" / "session")
fake_claude_sdk.plan_client(
turns=[
fake_claude_sdk.turn(
fake_claude_sdk.assistant(fake_claude_sdk.text("answer")),
fake_claude_sdk.result("success", "session-1"),
expected_query="question\n\nAnswer it, then stop.",
)
]
)
client = fake_claude_sdk.module.ClaudeSDKClient(options=object())

asyncio.run(engineer.answer(client, "question"))

assert fake_claude_sdk.last_client.queries == ["question\n\nAnswer it, then stop."]
assert (tmp_path / "run" / "session").read_text() == f"session-1\n{tmp_path}\n"
assert "answer" in capsys.readouterr().out


def test_secretary_answer_includes_live_agent_status(
fake_claude_sdk, monkeypatch, tmp_path
):
monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
sys_module = importlib.import_module("sys")
sys_module.modules.pop("framework.secretary", None)
secretary = importlib.import_module("framework.secretary")
(tmp_path / "run").mkdir()
fake_claude_sdk.plan_client(
turns=[
fake_claude_sdk.turn(
fake_claude_sdk.result("success", "session-2"),
expected_query=lambda prompt: (
"Research agents running:\nagent1" in prompt
),
)
]
)
client = fake_claude_sdk.module.ClaudeSDKClient(options=object())

asyncio.run(secretary.answer(client, "status?", ["agent1 -- campaign demo"]))

assert "New from Slack:\n\nstatus?" in fake_claude_sdk.last_client.queries[0]
assert (
tmp_path / "run" / "secretary_session"
).read_text() == f"session-2\n{tmp_path}\n"
53 changes: 53 additions & 0 deletions tests/test_critic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import types


def test_resolve_explicit_and_auto(import_module, monkeypatch):
critic = import_module("critic", {"CRITIC_MODEL": "auto"})
monkeypatch.setattr(
critic, "_served_models", lambda: {"gpt": "openai/gpt", "claude": "claude-3"}
)
assert critic.resolve("claude-sonnet")[0] == "gpt"

critic.MODEL_SETTING = "missing"
monkeypatch.setattr(critic, "_served_models", lambda: {"gpt": "openai/gpt"})
try:
critic.resolve()
except critic.CriticUnavailable as exc:
assert "not served" in str(exc)
else:
raise AssertionError("expected unavailable critic")


def test_blocking_only_returns_structured_blocking_findings(import_module):
critic = import_module("critic")
reply = """CLAIM: x is lower
VERDICT: unsupported
SEVERITY: blocking

CLAIM: y improved
VERDICT: supported
SEVERITY: minor"""
assert critic.blocking(reply) == [("x is lower", "unsupported")]
assert critic.blocking("plain prose") == []


def test_review_builds_request_and_joins_content(import_module, monkeypatch):
critic = import_module("critic", {"CRITIC_API_KEY": "secret"})
request = types.SimpleNamespace()
request.data = None

class Response:
def __enter__(self):
return self

def __exit__(self, *_):
return None

response = Response()
monkeypatch.setattr(critic.urllib.request, "urlopen", lambda req, timeout: response)
monkeypatch.setattr(
critic.json, "load", lambda _: {"content": [{"text": "a"}, {"text": " b "}]}
)
result = critic.review("model", "write-up", "evidence")
assert result == "a b"
assert request.data is None
55 changes: 55 additions & 0 deletions tests/test_lab_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import importlib
from pathlib import Path


def test_load_strips_comments_and_resolves_path_values(tmp_path, monkeypatch):
monkeypatch.setenv("LAB_DIR", str(tmp_path))
module = importlib.import_module("framework.lab_config")
config = tmp_path / "lab.yaml"
config.write_text("""
# comment
litellm-bin: ~/bin/litellm # inline
litellm-url: http://localhost:4000
malformed
key: value:with:colons
""")

loaded = module.load(config)
assert loaded["litellm-bin"] == str(Path.home() / "bin" / "litellm")
assert loaded["key"] == "value:with:colons"


def test_start_command_and_service_selection(tmp_path, monkeypatch):
monkeypatch.setenv("LAB_DIR", str(tmp_path))
module = importlib.import_module("framework.lab_config")
config = {
"litellm-bin": "/bin/litellm",
"litellm-config": "/tmp/config.yaml",
"litellm-url": "http://localhost:4000",
"bridge": "yes",
"secretary": "off",
}
assert (
module.start_command(config)
== "/bin/litellm --config /tmp/config.yaml --port 4000"
)
assert module.on("bridge", config)
assert not module.on("secretary", config)


def test_main_get_services_and_export(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("LAB_DIR", str(tmp_path))
(tmp_path / "lab.yaml").write_text("slack-channel: C123\nbridge: on\n")
module = importlib.import_module("framework.lab_config")
module.load = lambda path=str(tmp_path / "lab.yaml"): {
"slack-channel": "C123",
"bridge": "on",
}

monkeypatch.setattr(module.sys, "argv", ["lab_config.py", "--get", "slack-channel"])
module.main()
assert capsys.readouterr().out.strip() == "C123"

monkeypatch.setattr(module.sys, "argv", ["lab_config.py", "--services"])
module.main()
assert capsys.readouterr().out.strip() == "bridge"
47 changes: 47 additions & 0 deletions tests/test_slack_to_board.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import importlib


def test_forward_orders_messages_and_routes_to_campaigns(tmp_path, monkeypatch):
monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
module = importlib.import_module("framework.slack_to_board")
(tmp_path / "alpha").mkdir()
(tmp_path / "beta").mkdir()
monkeypatch.setattr(module, "DEDICATED", False)
monkeypatch.setattr(module, "READ_ALL", False)
delivered = module.forward(
[
{"user": "u2", "text": "<@BOT> beta second"},
{"user": "u1", "text": "<@BOT> alpha first"},
],
"BOT",
)
assert delivered == 2
assert "alpha first" in (tmp_path / "alpha" / "ANNOUNCEMENTS.md").read_text()
assert "beta second" in (tmp_path / "beta" / "ANNOUNCEMENTS.md").read_text()


def test_forward_skips_bots_and_unaddressed_messages(tmp_path, monkeypatch):
monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
module = importlib.import_module("framework.slack_to_board")
(tmp_path / "alpha").mkdir()
delivered = module.forward(
[
{"bot_id": "B", "text": "<@BOT> bot"},
{"user": "u", "text": "ordinary"},
],
"BOT",
)
assert delivered == 0
assert not (tmp_path / "alpha" / "ANNOUNCEMENTS.md").exists()


def test_check_first_run_does_not_fetch_history(tmp_path, monkeypatch):
monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path))
module = importlib.import_module("framework.slack_to_board")
called = []
monkeypatch.setattr(
module, "slack_get", lambda *args, **kwargs: called.append(args)
)
module.check("token", "BOT")
assert called == []
assert module.read_state()
67 changes: 67 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio
import json


def payload(result):
return json.loads(result["content"][0]["text"])


def test_remote_submit_and_collect_success(tools_lab):
lab = tools_lab(remote=True)
lab.globus.plan_success({"metric": 7}, task_id="task-7")

submitted = asyncio.run(lab.tools.submit_job({"value": 7}))
assert payload(submitted)["key"] == "value=7"
assert lab.globus.submits[0][3]["nranks"] == 4
assert len(lab.globus.executor_inits) == 1

completed = payload(asyncio.run(lab.tools.get_completed_jobs({})))
assert completed["completed"] == [{"metric": 7, "job_id": 1, "key": "value=7"}]
assert completed["pending"] == []
assert lab.tools.jobs_in_flight() == 0
assert '"event": "submit"' in (lab.workspace / "jobs.jsonl").read_text()


def test_submit_failure_releases_claim(tools_lab):
lab = tools_lab(remote=True)
lab.globus.plan_submit_error(RuntimeError("endpoint down"))

result = asyncio.run(lab.tools.submit_job({"value": 1}))

assert result["is_error"]
assert lab.tools.submit_count() == 0
claims = (lab.workspace / "claims.jsonl").read_text()
assert '"state": "claimed"' in claims
assert '"state": "done"' in claims


def test_local_only_needs_no_user_file_and_exposes_local_tools(tools_lab):
lab = tools_lab(remote=False, local=True)

assert lab.tools.HAS_LOCAL
assert not lab.tools.HAS_REMOTE
assert lab.tools.ENDPOINT_ID == ""
assert lab.tools.tool_names() == [
"mcp__cas__submit_local",
"mcp__cas__get_local_completed",
"mcp__cas__notify",
"mcp__cas__cycle_done",
"mcp__cas__goal_met",
]


def test_backend_trouble_requires_all_pending_tasks_to_fail(tools_lab):
lab = tools_lab(remote=True)
lab.globus.plan_pending("failed-task")
asyncio.run(lab.tools.submit_job({"value": 1}))
lab.globus.task_statuses["failed-task"] = {"status": "failed"}
assert "all 1 in-flight task(s) failed/lost" in lab.tools.backend_trouble()


def test_goal_met_stops_new_work(tools_lab):
lab = tools_lab(remote=True)
asyncio.run(lab.tools.goal_met({"reason": "enough evidence"}))
refused = asyncio.run(lab.tools.submit_job({"value": 3}))
assert lab.tools.goal_is_met() == "enough evidence"
assert refused["is_error"]
assert "winding down" in refused["content"][0]["text"]
61 changes: 61 additions & 0 deletions tests/test_transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import asyncio


def test_configure_and_path_confinement(transfer_module, tmp_path):
cfg = transfer_module.configure(
{
"work_dir": "/remote/work",
"globus": {"remote_collection": "r", "local_collection": "l"},
},
str(tmp_path),
str(tmp_path / "campaign"),
{"globus_collection_root": "/projects"},
)
assert cfg["remote_write_root"] == "/remote/work"
assert cfg["collection_root"] == ["/projects"]
assert transfer_module._local_dest("file.txt", [str(tmp_path)]) == str(
tmp_path / "file.txt"
)
assert transfer_module._local_dest("../secret", [str(tmp_path)]) is None


def test_transfer_requires_configuration_and_authentication(
transfer_module, monkeypatch
):
transfer_module.CFG = None
result = asyncio.run(transfer_module.transfer({"op": "ls", "path": "/"}))
assert result["is_error"]
assert "not configured" in result["content"][0]["text"]

transfer_module.CFG = {
"remote_collection": "r",
"local_collection": "l",
"remote_write_root": "/work",
"remote_read_root": "",
"collection_root": [],
"local_roots": ["/tmp"],
}
monkeypatch.setattr(
transfer_module, "_authenticated", lambda: (False, "login required")
)
result = asyncio.run(transfer_module.transfer({"op": "ls", "path": "/"}))
assert "login required" in result["content"][0]["text"]


def test_ls_and_unknown_operation(transfer_module, monkeypatch):
transfer_module.CFG = {
"remote_collection": "r",
"local_collection": "l",
"remote_write_root": "/work",
"remote_read_root": "",
"collection_root": [],
"local_roots": ["/tmp"],
}
monkeypatch.setattr(transfer_module, "_authenticated", lambda: (True, "user"))
monkeypatch.setattr(
transfer_module, "_globus", lambda *args, **kwargs: (0, "a\nb", "")
)
result = asyncio.run(transfer_module.transfer({"op": "ls", "path": "/data"}))
assert result["content"][0]["text"] == "a\nb"
result = asyncio.run(transfer_module.transfer({"op": "wat", "path": "x"}))
assert result["is_error"]
Loading