Skip to content
Merged
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
17 changes: 17 additions & 0 deletions backend/app/models/watched_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,20 @@ def delete(watched_id: str) -> bool:
.execute()
)
return len(result.data) > 0

@staticmethod
def delete_by_agent(agent_id: str) -> None:
"""Remove every watched_repos row for an agent.

Called by Orchestrator.stop_agent so an offboarded agent stops
occupying its (user, owner, repo) slot — otherwise the cross-agent
uniqueness from #28 would block re-hiring against the same repo
(#31). Fire-and-forget: we don't care about the row count.
"""
(
get_supabase()
.table(TABLE)
.delete()
.eq("agent_id", agent_id)
.execute()
)
6 changes: 6 additions & 0 deletions backend/app/services/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from docker.errors import NotFound, APIError
from app.config import get_settings
from app.models.agent import AgentModel
from app.models.watched_repo import WatchedRepoModel
from app.services.template_loader import load_template


Expand Down Expand Up @@ -95,6 +96,11 @@ def stop_agent(self, agent_id: str) -> dict | None:
except NotFound:
pass

# Release the agent's repo subscriptions so a re-hire under the same
# user can take the same (owner, repo) — #31. agent_memory /
# agent_action_log / reviewed_prs are preserved as the audit trail.
WatchedRepoModel.delete_by_agent(agent_id)

# Clear the token: a stopped agent must not authenticate.
AgentModel.update(
agent_id, status="stopped", container_id=None, agent_token=None
Expand Down
28 changes: 28 additions & 0 deletions backend/tests/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,34 @@ def test_stop_agent_not_found(self, fake_supabase, mock_docker):
result = orch.stop_agent("nonexistent")
assert result is None

def test_stop_agent_releases_watched_repos(self, fake_supabase, mock_docker):
"""Offboarding must release the agent's repo subscriptions so a
re-hire under the same user can take the same (owner, repo) — #31.
Audit tables (agent_memory / agent_action_log / reviewed_prs) are
intentionally untouched."""
from app.services.orchestrator import Orchestrator

agent = {
"id": "agent-001",
"user_id": "user-001",
"role": "code-review-engineer",
"container_id": "ctr-123",
"status": "running",
"config_json": {},
}
agents_table = fake_supabase.get_table("agents")
agents_table.set_select_result([agent])
agents_table.set_update_result([{**agent, "status": "stopped"}])
mock_docker.containers.get.return_value = MagicMock()

with patch(
"app.services.orchestrator.WatchedRepoModel.delete_by_agent"
) as mock_delete:
orch = Orchestrator()
orch.stop_agent("agent-001")

mock_delete.assert_called_once_with("agent-001")


class TestGetAgentStatus:
def test_clears_agent_token_when_container_exited(self, fake_supabase, mock_docker):
Expand Down