Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

392 lines
16 KiB
Python

"""Tests for branch ownership and cross-agent coordination."""
from __future__ import annotations
import io
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from rich.console import Console
import surfaces.interactive_shell.command_registry.agents.core as agents_shell
from tools.system.fleet_monitoring.coordination import BranchClaim, BranchClaims
from tools.system.fleet_monitoring.registry import AgentRecord, AgentRegistry
@pytest.fixture
def claims(tmp_path: Path) -> BranchClaims:
return BranchClaims(path=tmp_path / "branch_claims.jsonl")
@pytest.fixture
def session() -> object:
return SimpleNamespace(mark_latest=lambda ok, kind: None) # noqa: ARG005
@pytest.fixture
def console() -> Console:
# ``color_system=None`` keeps output free of ANSI segments that split
# visible text (e.g. "/fleet") when CI sets ``FORCE_COLOR`` / TTY-like env.
return Console(file=io.StringIO(), color_system=None)
@pytest.fixture
def sample_claim() -> BranchClaim:
return BranchClaim(
branch="auth-refactor",
agent_name="aider",
pid=7702,
claimed_at="2026-05-07T12:00:00+00:00",
)
class TestBranchClaim:
def test_frozen(self, sample_claim: BranchClaim) -> None:
with pytest.raises(AttributeError):
sample_claim.branch = "main" # type: ignore[misc]
def test_round_trip_dict(self, sample_claim: BranchClaim) -> None:
restored = BranchClaim.from_dict(sample_claim.to_dict())
assert restored == sample_claim
def test_from_dict_missing_claimed_at(self) -> None:
claim = BranchClaim.from_dict({"branch": "main", "agent_name": "aider", "pid": 1234})
assert claim.branch == "main"
assert claim.agent_name == "aider"
assert claim.pid == 1234
assert claim.claimed_at # auto-populated
def test_from_dict_coerces_types(self) -> None:
claim = BranchClaim.from_dict({"branch": "main", "agent_name": "codex", "pid": "9999"})
assert claim.pid == 9999
assert isinstance(claim.pid, int)
class TestBranchClaims:
def test_claim_and_get(self, claims: BranchClaims, sample_claim: BranchClaim) -> None:
result = claims.claim(sample_claim.branch, sample_claim.agent_name, sample_claim.pid)
assert result is not None
assert result.branch == sample_claim.branch
assert result.agent_name == sample_claim.agent_name
assert result.pid == sample_claim.pid
retrieved = claims.get(sample_claim.branch)
assert retrieved == result
def test_claim_returns_none_on_conflict(self, claims: BranchClaims) -> None:
claims.claim("main", "aider", 7702)
result = claims.claim("main", "claude-code", 8421)
assert result is None
def test_claim_same_agent_same_branch_updates(self, claims: BranchClaims) -> None:
first = claims.claim("main", "aider", 7702)
assert first is not None
second = claims.claim("main", "aider", 7702)
assert second is not None
assert claims.get("main") == second
def test_release_removes_claim(self, claims: BranchClaims, sample_claim: BranchClaim) -> None:
claims.claim(sample_claim.branch, sample_claim.agent_name, sample_claim.pid)
removed = claims.release(sample_claim.branch)
assert removed is not None
assert removed.branch == sample_claim.branch
assert claims.get(sample_claim.branch) is None
def test_release_nonexistent_returns_none(self, claims: BranchClaims) -> None:
assert claims.release("nonexistent") is None
def test_is_held(self, claims: BranchClaims) -> None:
assert not claims.is_held("main")
claims.claim("main", "aider", 7702)
assert claims.is_held("main")
def test_holder_returns_agent_name(self, claims: BranchClaims) -> None:
claims.claim("main", "aider", 7702)
assert claims.holder("main") == "aider"
assert claims.holder("nonexistent") is None
def test_holder_pid_returns_pid(self, claims: BranchClaims) -> None:
claims.claim("main", "aider", 7702)
assert claims.holder_pid("main") == 7702
assert claims.holder_pid("nonexistent") is None
def test_list_returns_all_claims(self, claims: BranchClaims) -> None:
claims.claim("main", "aider", 7702)
claims.claim("dev", "claude-code", 8421)
listed = claims.list()
assert len(listed) == 2
branches = {c.branch for c in listed}
assert branches == {"main", "dev"}
def test_persistence_round_trip(self, tmp_path: Path) -> None:
path = tmp_path / "branch_claims.jsonl"
reg1 = BranchClaims(path=path)
reg1.claim("main", "aider", 7702)
reg1.claim("dev", "claude-code", 8421)
reg2 = BranchClaims(path=path)
assert len(reg2.list()) == 2
assert reg2.holder("main") == "aider"
assert reg2.holder("dev") == "claude-code"
def test_release_updates_disk(self, tmp_path: Path) -> None:
path = tmp_path / "branch_claims.jsonl"
reg1 = BranchClaims(path=path)
reg1.claim("main", "aider", 7702)
reg1.claim("dev", "claude-code", 8421)
reg1.release("main")
reg2 = BranchClaims(path=path)
assert len(reg2.list()) == 1
assert reg2.holder("main") is None
assert reg2.holder("dev") == "claude-code"
def test_jsonl_file_format(self, tmp_path: Path) -> None:
path = tmp_path / "branch_claims.jsonl"
reg = BranchClaims(path=path)
reg.claim("main", "aider", 7702)
lines = path.read_text(encoding="utf-8").strip().splitlines()
assert len(lines) == 1
parsed = json.loads(lines[0])
assert parsed["branch"] == "main"
assert parsed["agent_name"] == "aider"
assert parsed["pid"] == 7702
def test_corrupt_line_skipped(self, tmp_path: Path) -> None:
path = tmp_path / "branch_claims.jsonl"
path.write_text(
'{"branch":"main","agent_name":"aider","pid":7702,"claimed_at":"2026-05-07T12:00:00+00:00"}\n'
"this is not json\n"
'{"branch":"dev","agent_name":"claude-code","pid":8421,"claimed_at":"2026-05-07T12:00:00+00:00"}\n',
encoding="utf-8",
)
reg = BranchClaims(path=path)
assert len(reg.list()) == 2
def test_empty_file(self, tmp_path: Path) -> None:
path = tmp_path / "branch_claims.jsonl"
path.write_text("", encoding="utf-8")
reg = BranchClaims(path=path)
assert reg.list() == []
def test_nonexistent_file(self, tmp_path: Path) -> None:
path = tmp_path / "does_not_exist.jsonl"
reg = BranchClaims(path=path)
assert reg.list() == []
def test_claim_same_branch_different_pid_is_conflict(self, claims: BranchClaims) -> None:
"""Same agent name but different PID should be treated as a conflict."""
claims.claim("main", "aider", 7702)
result = claims.claim("main", "aider", 9999) # Same name, different PID
assert result is None
def test_claim_different_agent_same_pid_is_conflict(self, claims: BranchClaims) -> None:
"""Different agent name but same PID - this is unlikely but still a conflict."""
claims.claim("main", "aider", 7702)
result = claims.claim("main", "claude-code", 7702) # Different name, same PID
assert result is None
def test_reclaim_does_not_duplicate_jsonl_entry(self, tmp_path: Path) -> None:
"""Re-claiming the same branch should not add duplicate lines to JSONL file."""
path = tmp_path / "branch_claims.jsonl"
reg = BranchClaims(path=path)
# First claim
reg.claim("main", "aider", 7702)
lines1 = path.read_text(encoding="utf-8").strip().splitlines()
assert len(lines1) == 1
# Re-claim same branch
reg.claim("main", "aider", 7702)
lines2 = path.read_text(encoding="utf-8").strip().splitlines()
# Should still be 1 line (rewrite replaced the file, not appended)
assert len(lines2) == 1, f"Expected 1 line, got {len(lines2)}: {lines2}"
# Verify content is still valid
reg2 = BranchClaims(path=path)
assert reg2.holder("main") == "aider"
def test_reclaim_same_agent_succeeds(self, claims: BranchClaims) -> None:
"""Same agent re-claiming the same branch should succeed."""
first = claims.claim("main", "aider", 7702)
assert first is not None
# Re-claim should succeed (refreshes timestamp via _rewrite)
second = claims.claim("main", "aider", 7702)
assert second is not None
assert second.branch == "main"
assert second.agent_name == "aider"
assert second.pid == 7702
class TestCliCommands:
"""Tests for /fleet claim and /fleet release CLI commands."""
def test_claim_usage_error_missing_args(self, session: object, console: Console) -> None:
"""Missing arguments should print usage error."""
result = agents_shell._cmd_agents_claim(session, console, [])
assert result is False
output = console.file.getvalue()
assert "Usage:" in output
assert "/fleet claim" in output
def test_claim_usage_error_one_arg(self, session: object, console: Console) -> None:
"""Only one argument should print usage error."""
result = agents_shell._cmd_agents_claim(session, console, ["main"])
assert result is False
output = console.file.getvalue()
assert "Usage:" in output
def test_claim_agent_not_found(self, session: object, console: Console, tmp_path: Path) -> None:
"""Claiming with non-existent agent should print error."""
reg_path = tmp_path / "agents.jsonl"
AgentRegistry(path=reg_path) # Empty registry
original_registry = agents_shell.AgentRegistry
agents_shell.AgentRegistry = lambda *args, **kwargs: AgentRegistry( # noqa: ARG005
path=reg_path
)
try:
result = agents_shell._cmd_agents_claim(session, console, ["main", "unknown-agent"])
assert result is False
output = console.file.getvalue()
assert "not found in registry" in output
finally:
agents_shell.AgentRegistry = original_registry
def test_claim_success(self, session: object, console: Console, tmp_path: Path) -> None:
"""Successful claim should print success message."""
reg_path = tmp_path / "agents.jsonl"
reg = AgentRegistry(path=reg_path)
reg.register(AgentRecord(name="aider", pid=7702, command="aider"))
claims_path = tmp_path / "branch_claims.jsonl"
original_registry = agents_shell.AgentRegistry
original_claims = agents_shell.BranchClaims
agents_shell.AgentRegistry = lambda *args, **kwargs: AgentRegistry( # noqa: ARG005
path=reg_path
)
agents_shell.BranchClaims = lambda *args, **kwargs: BranchClaims( # noqa: ARG005
path=claims_path
)
try:
result = agents_shell._cmd_agents_claim(session, console, ["main", "aider"])
assert result is True
output = console.file.getvalue()
assert "Branch main now held by aider" in output
assert "pid 7702" in output
finally:
agents_shell.AgentRegistry = original_registry
agents_shell.BranchClaims = original_claims
def test_claim_conflict(self, session: object, console: Console, tmp_path: Path) -> None:
"""Claiming a branch held by another agent should print conflict error."""
reg_path = tmp_path / "agents.jsonl"
reg = AgentRegistry(path=reg_path)
reg.register(AgentRecord(name="aider", pid=7702, command="aider"))
reg.register(AgentRecord(name="claude-code", pid=8421, command="claude"))
claims_path = tmp_path / "branch_claims.jsonl"
claims = BranchClaims(path=claims_path)
claims.claim("main", "aider", 7702)
original_registry = agents_shell.AgentRegistry
original_claims = agents_shell.BranchClaims
agents_shell.AgentRegistry = lambda *args, **kwargs: AgentRegistry( # noqa: ARG005
path=reg_path
)
agents_shell.BranchClaims = lambda *args, **kwargs: BranchClaims( # noqa: ARG005
path=claims_path
)
try:
result = agents_shell._cmd_agents_claim(session, console, ["main", "claude-code"])
assert result is False
output = console.file.getvalue()
assert "Cannot claim" in output
assert "already held by aider" in output
finally:
agents_shell.AgentRegistry = original_registry
agents_shell.BranchClaims = original_claims
def test_release_usage_error(self, session: object, console: Console) -> None:
"""Missing branch argument should print usage error."""
result = agents_shell._cmd_agents_release(session, console, [])
assert result is False
output = console.file.getvalue()
assert "Usage:" in output
assert "/fleet release" in output
def test_release_not_held(self, session: object, console: Console, tmp_path: Path) -> None:
"""Releasing unclaimed branch should print error."""
claims_path = tmp_path / "branch_claims.jsonl"
original_claims = agents_shell.BranchClaims
agents_shell.BranchClaims = lambda *args, **kwargs: BranchClaims( # noqa: ARG005
path=claims_path
)
try:
result = agents_shell._cmd_agents_release(session, console, ["nonexistent"])
assert result is False
output = console.file.getvalue()
assert "is not currently held" in output
finally:
agents_shell.BranchClaims = original_claims
def test_release_success(self, session: object, console: Console, tmp_path: Path) -> None:
"""Successful release should print success message."""
claims_path = tmp_path / "branch_claims.jsonl"
claims = BranchClaims(path=claims_path)
claims.claim("main", "aider", 7702)
original_claims = agents_shell.BranchClaims
agents_shell.BranchClaims = lambda *args, **kwargs: BranchClaims( # noqa: ARG005
path=claims_path
)
try:
result = agents_shell._cmd_agents_release(session, console, ["main"])
assert result is True
output = console.file.getvalue()
assert "Released main" in output
assert "aider" in output
finally:
agents_shell.BranchClaims = original_claims
def test_claim_reclaim_same_agent(
self, session: object, console: Console, tmp_path: Path
) -> None:
"""Re-claiming by the same agent should succeed (refreshes timestamp)."""
reg_path = tmp_path / "agents.jsonl"
reg = AgentRegistry(path=reg_path)
reg.register(AgentRecord(name="aider", pid=7702, command="aider"))
claims_path = tmp_path / "branch_claims.jsonl"
claims = BranchClaims(path=claims_path)
claims.claim("main", "aider", 7702)
original_registry = agents_shell.AgentRegistry
original_claims = agents_shell.BranchClaims
agents_shell.AgentRegistry = lambda *args, **kwargs: AgentRegistry( # noqa: ARG005
path=reg_path
)
agents_shell.BranchClaims = lambda *args, **kwargs: BranchClaims( # noqa: ARG005
path=claims_path
)
try:
result = agents_shell._cmd_agents_claim(session, console, ["main", "aider"])
assert result is True
output = console.file.getvalue()
assert "Branch main now held by aider" in output
finally:
agents_shell.AgentRegistry = original_registry
agents_shell.BranchClaims = original_claims