Files
openworker/tests/test_worker_connectors.py
Rohit C Prasad eef0122b24 Clarify team approvals and surface every pending request
Surface every pending request and review delegated actions using the original worker permission context.
2026-09-20 15:03:01 -07:00

583 lines
31 KiB
Python

"""Workers' connectors, approval mode, and the two connector gates (spec §11.6,
owner-ruled 2026-09-05): workers start with no connectors; the staffing card decides
connectors + approval mode per worker within the persona's ceiling; a lead may ask
for a connector later (grant_connector) and any connector-capable coworker may ask
the human to connect a service (request_connector). No network, no LLM."""
from __future__ import annotations
import pytest
from coworker.agents.base import Agent
from coworker.engine import EventType, TurnEngine
from coworker.permissions import Mode, PermissionEngine
from coworker.providers import AssistantTurn, ModelCapabilities, ProviderClient, ToolCall
from coworker.server.manager import SessionManager
from coworker.sessions import SessionRecord
from coworker.teams.store import WORKER_WAITING
from coworker.tools import ToolRegistry
@pytest.fixture
def manager(tmp_path, monkeypatch):
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
ws = tmp_path / "repo"
ws.mkdir()
m = SessionManager(data_dir=tmp_path / "data", workspace=str(ws))
# GitHub + Slack connected on this box (manual profiles are enough for the gate).
m.secrets.put("github:default", {"token": "ghp_test", "enabled": True})
m.secrets.put("slack:default", {"bot_token": "xoxb", "app_token": "xapp", "enabled": True})
yield m
def _lead(manager, sid="lead-sid", agent="swe-lead"):
manager.session_store.save(
SessionRecord(session_id=sid, workspace=manager.default_workspace, model="m", mode="interactive", messages=[], agent=agent)
)
# -- the ceiling and the default ---------------------------------------------------------
def test_workers_start_with_no_connectors_and_the_offer_is_the_default_set(manager):
# swe-worker declares [github]: that is its DEFAULT set (worker-connector-grants spec,
# 2026-09-17), not a ceiling. GitHub is what the lead may suggest unprompted; Slack is
# connected here but outside the default set, so only a human can add it.
assert manager.connector_offer("swe-worker") == ["github"]
# (`browser` is the built-in, always-connected connector.)
assert manager.connector_lists("swe-worker") == {"ready": ["github"], "connectable": [], "other_connected": ["browser", "slack"]}
assert manager.connector_offer("design-worker") == [] # declares nothing
assert manager.connector_lists("design-worker")["other_connected"] == ["browser", "github", "slack"]
assert manager.other_connected() == ["browser", "github", "slack"]
_lead(manager)
result = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])
assert result["approved"] is True
sid = result["workers"][0]["session_id"]
# Nothing on until the human ticks it — even though GitHub is connected and declared.
assert manager.effective_connectors(sid, "swe-worker") == set()
assert result["workers"][0]["connectors"] == []
assert result["workers"][0]["approvals"] == "follow the lead"
def test_staffing_card_ticks_are_the_grant_even_beyond_the_default_set(manager):
_lead(manager)
result = manager.create_team(
"lead-sid",
[
# The human ticked GitHub (default set) AND Slack (outside it): both apply — the
# declaration is the default, not the ceiling. Jira is not connected here, so
# that tick is dropped. A stray `approval_mode` (older client) is ignored.
{
"persona": "swe-worker", "name": "nia", "connectors": ["github", "slack", "jira"],
"connector_reasons": {"slack": 'you asked: "post the summary in Slack"'},
"approval_mode": "bypass-approvals",
},
{"persona": "test-worker", "name": "checks", "connectors": []},
],
approved_by="rohit@example.com",
)
assert result["approved"] is True
nia, checks = result["workers"]
assert nia["connectors"] == ["github", "slack"] and checks["connectors"] == []
# Workers are stored as "always ask"; the lead's regime is mirrored live (below).
assert manager.session_store.load(nia["session_id"]).mode == "interactive"
assert manager.effective_connectors(nia["session_id"], "swe-worker") == {"github", "slack"}
# Going beyond the default set is recorded per session and audited with who approved it.
assert manager.session_connector_extensions.get(nia["session_id"]) == {"slack": True}
rows = [r for r in manager.audit_store.list(session_id=nia["session_id"]) if r.get("stage") == "connector_extended"]
assert len(rows) == 1 and rows[0]["connector"] == "slack" and rows[0]["approved_by"] == "rohit@example.com"
assert "post the summary in Slack" in rows[0]["reason"]
def test_org_policy_is_the_only_ceiling_on_worker_connectors(manager):
manager.org_policy = {"worker_connectors": {"deny": ["slack"]}}
assert manager.connector_lists("swe-worker")["other_connected"] == ["browser"]
assert manager.other_connected() == ["browser", "github"]
_lead(manager)
nia = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia", "connectors": ["github", "slack"]}])["workers"][0]
assert nia["connectors"] == ["github"] # a forced tick does not pass policy
assert "blocked" in manager.grant_worker_connector("lead-sid", "nia", "slack")["error"]
@pytest.mark.asyncio
async def test_lead_suggestions_outside_the_default_set_need_the_users_words(manager):
import asyncio
_lead(manager)
approve = manager.inbox_team_approver("lead-sid", "swe-lead")
# No reason for Slack (outside swe-worker's default set) and Jira is not connected:
# the proposal bounces back to the lead with what to do, and nothing is parked.
from proposal_fixtures import team_proposal
bounced = await approve(team_proposal([{"persona": "swe-worker", "name": "nia", "connectors": ["slack", "jira"]}]), "t0")
assert bounced["approved"] is False
assert any("slack" in p and "quote" in p for p in bounced["problems"])
assert any("jira" in p and "request_connector" in p for p in bounced["problems"])
assert not manager.inbox.list(session_id="lead-sid", state="pending")
# With the user's words as the reason it reaches the card, carrying what the card needs.
members = [{"persona": "swe-worker", "name": "nia", "connectors": ["GitHub", "slack"],
"connector_reasons": {"github": "pushes the branch", "slack": 'you asked: "post the summary in Slack"'}}]
task = asyncio.create_task(approve(team_proposal(members), "t1"))
for _ in range(50):
await asyncio.sleep(0.02)
pending = manager.inbox.list(session_id="lead-sid", state="pending")
if pending:
break
data = pending[0].data
assert data["members"][0]["connectors"] == ["github", "slack"] # normalised
assert data["offer"] == {"swe-worker": ["github"]} and data["other_connected"] == ["browser", "github", "slack"]
assert "lead_mode" in data
# The human keeps both ticks.
manager.inbox.resolve(pending[0].id, '{"approved": true, "members": [{"persona": "swe-worker", "name": "nia", "connectors": ["github", "slack"]}]}')
result = await task
assert result["approved"] is True and result["workers"][0]["connectors"] == ["github", "slack"]
# -- approvals follow the lead ---------------------------------------------------------------
def test_worker_engine_mirrors_the_lead_mode_live(manager):
from coworker.permissions import Mode
_lead(manager)
sid = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])["workers"][0]["session_id"]
engine = manager.get_engine(sid)
assert engine.permissions.mode is Mode.INTERACTIVE # the lead is Manual
assert engine.is_attended is not None and engine.is_attended() is False
# The lead flips to auto-approve → the worker's next engine use runs the reviewer path
lead = manager.session_store.load("lead-sid")
lead.mode = "auto-approve"
manager.session_store.save(lead)
engine = manager.get_engine(sid) # cached engine, re-synced
assert engine.permissions.mode is Mode.AUTO_APPROVE and engine.is_attended() is True
# Legacy "auto" on the lead means bypass; a read-only lead still lets workers ask
lead.mode = "auto"
manager.session_store.save(lead)
assert manager.get_engine(sid).permissions.mode is Mode.BYPASS_APPROVALS
lead.mode = "plan"
manager.session_store.save(lead)
assert manager.get_engine(sid).permissions.mode is Mode.INTERACTIVE
# A non-worker session is never touched
assert manager.lead_mode_for("lead-sid") is None
def test_decide_worker_call_resolves_the_parked_prompt(manager):
_lead(manager)
result = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])
sid = result["workers"][0]["session_id"]
item = manager.inbox.add_approval(sid, "Run `run_shell`?", body="git push", visibility="inline")
manager.note_worker_waiting(sid, "run_shell", prompt_id=item.id, preview="git push")
# The lead's wake digest names the worker, the call, and the call_id to answer with.
team = manager.teams.for_lead_session("lead-sid")
subs = manager.team_store.subscribed_events(team.space, team.lead_actor)
message, rows = manager._team_digest(team, [], subs, is_lead=True)
assert "nia is waiting on your decision" in message and "run_shell — git push" in message
assert f'decide_worker_call(worker="nia", call_id="{item.id}"' in message
assert any(r["kind"] == "waiting" and r["prompt_id"] == item.id for r in rows)
# Guards: not a lead, unknown worker, unknown prompt, wrong decision value
assert "does not lead" in manager.decide_worker_call("nobody", "nia", item.id, "allow")["error"]
assert "no worker" in manager.decide_worker_call("lead-sid", "webb", item.id, "allow")["error"]
assert "no such parked call" in manager.decide_worker_call("lead-sid", "nia", "bogus", "allow")["error"]
# Deny resolves the prompt; a second answer reports it as already answered
out = manager.decide_worker_call("lead-sid", "nia", item.id, "deny", "outside the item")
assert out["ok"] is True and out["decision"] == "deny"
assert manager.inbox.get(item.id).state == "resolved" and manager.inbox.get(item.id).resolution == "deny"
assert manager.decide_worker_call("lead-sid", "nia", item.id, "allow")["note"] == "already answered"
# Allow on a fresh prompt
item2 = manager.inbox.add_approval(sid, "Run `run_shell`?", body="npm test", visibility="inline")
assert manager.decide_worker_call("lead-sid", "nia", item2.id, "allow")["decision"] == "allow"
assert manager.inbox.get(item2.id).resolution == "allow"
def test_decide_tool_is_consequential_and_lead_only(tmp_path):
from coworker.agent import build_engine
from coworker.secrets import SecretStore
secrets = SecretStore(tmp_path / "s.json")
calls = []
lead = Agent(name="lead", title="L", system_prompt="x", team="lead")
eng = build_engine(agent=lead, workspace=tmp_path, provider=_Stub(), secrets=secrets, worker_decider=lambda *a: calls.append(a) or {"ok": True})
assert "decide_worker_call" in eng.registry.names()
spec = eng.registry.get("decide_worker_call")
meta = getattr(spec, "metadata", None) or getattr(getattr(spec, "func", None), "__aisuite_tool_metadata__", None)
assert meta is not None and meta.requires_approval is True # a Manual lead's decision asks the human
worker = Agent(name="w", title="W", system_prompt="x", team="worker")
assert "decide_worker_call" not in build_engine(agent=worker, workspace=tmp_path, provider=_Stub(), secrets=secrets, worker_decider=lambda *a: {}).registry.names()
# -- grant_connector, the lead's later ask ------------------------------------------------
def test_grant_worker_connector_flips_the_session_and_can_extend_with_a_human_yes(manager):
_lead(manager)
result = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])
sid = result["workers"][0]["session_id"]
assert manager.effective_connectors(sid, "swe-worker") == set()
out = manager.grant_worker_connector("lead-sid", "nia", "github")
assert out == {"approved": True, "worker": "nia", "connector": "github"}
assert manager.effective_connectors(sid, "swe-worker") == {"github"}
# Slack is outside swe-worker's default set but connected: the human already said yes
# on the grant card, so it applies — as a recorded extension.
out = manager.grant_worker_connector("lead-sid", "nia", "slack", reason='you asked: "ping #billing"', approved_by="rohit@example.com")
assert out["approved"] is True and manager.effective_connectors(sid, "swe-worker") == {"github", "slack"}
assert manager.session_connector_extensions.get(sid) == {"slack": True}
# Not connected, unknown worker, not a lead: all refuse with a reason.
assert "not connected" in manager.grant_worker_connector("lead-sid", "nia", "jira")["error"]
assert "no worker" in manager.grant_worker_connector("lead-sid", "webb", "github")["error"]
assert "does not lead" in manager.grant_worker_connector("nobody", "nia", "github")["error"]
def test_grant_rebuilds_the_worker_tools_after_its_turn(manager):
_lead(manager)
sid = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])["workers"][0]["session_id"]
engine = manager.get_engine(sid)
assert engine is not None and "github_reply" not in engine.registry.names()
manager.grant_worker_connector("lead-sid", "nia", "github")
# Idle: the engine was dropped and rebuilds with the connector's tools.
rebuilt = manager.get_engine(sid)
assert rebuilt is not engine and "github_reply" in rebuilt.registry.names()
# Running: kept until the turn ends, then dropped at mark_idle.
manager._running_sessions.add(sid)
manager.session_connections.set(sid, "github", False)
manager._refresh_session_tools(sid)
assert manager._engines.get(sid) is rebuilt
manager.mark_idle(sid)
assert sid not in manager._engines
# -- the lead hears that a worker is waiting ----------------------------------------------
def test_worker_waiting_reaches_the_lead_through_the_board(manager):
_lead(manager)
result = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])
team = manager.teams.for_lead_session("lead-sid")
sid = result["workers"][0]["session_id"]
manager.note_worker_waiting(sid, "run_shell", prompt_id="p1", preview="git push")
events = manager.team_store.subscribed_events(team.space, team.lead_actor)
waiting = [e for e in events if e["kind"] == WORKER_WAITING]
assert waiting and waiting[0]["actor"] == "nia" and waiting[0]["payload"]["tool"] == "run_shell"
assert waiting[0]["payload"]["prompt_id"] == "p1"
# A session that is not a worker is silently ignored.
manager.note_worker_waiting("lead-sid", "run_shell")
# -- the engine gate -----------------------------------------------------------------------
class _Scripted(ProviderClient):
def __init__(self, name, arguments):
self.calls, self.name, self.arguments = 0, name, arguments
def complete(self, *, model, messages, tools=None, **settings):
self.calls += 1
if self.calls == 1:
return AssistantTurn(text="", tool_calls=[ToolCall(id="t1", name=self.name, arguments=self.arguments)])
return AssistantTurn(text="done", tool_calls=[])
def capabilities(self, model):
return ModelCapabilities(tools=True)
def _engine(tmp_path, requester, name, arguments):
return TurnEngine(
provider=_Scripted(name, arguments),
registry=ToolRegistry(),
permissions=PermissionEngine(workspace_root=tmp_path, mode=Mode.INTERACTIVE),
model="m",
connector_requester=requester,
)
async def _run(engine):
return [e async for e in engine.run("go")]
@pytest.mark.asyncio
async def test_grant_connector_emits_the_gate_and_returns_the_verdict(tmp_path):
seen = {}
async def requester(args, tool_call_id=None):
seen.update(args)
return {"approved": True, "worker": "nia", "connector": "github"}
events = await _run(_engine(tmp_path, requester, "grant_connector", {"worker": "nia", "connector": "GitHub", "reason": "push the branch"}))
gate = [e for e in events if e.type is EventType.CONNECTOR_REQUESTED]
assert gate and gate[0].data == {"request": "grant", "connector": "github", "worker": "nia", "reason": "push the branch"}
assert seen["worker"] == "nia"
assert [e for e in events if e.type is EventType.TOOL_FINISHED][0].data["status"] == "ok"
@pytest.mark.asyncio
async def test_request_connector_decline_is_a_plain_outcome(tmp_path):
async def requester(args, tool_call_id=None):
return {"approved": False, "reason": "the user declined"}
engine = _engine(tmp_path, requester, "request_connector", {"connector": "linear", "reason": "keep the tracker in step"})
events = await _run(engine)
gate = [e for e in events if e.type is EventType.CONNECTOR_REQUESTED]
assert gate[0].data["request"] == "connect" and gate[0].data["worker"] == ""
assert [e for e in events if e.type is EventType.TOOL_FINISHED][0].data["status"] == "denied"
result = next(m for m in engine.messages if m.get("role") == "tool")
assert "declined" in str(result.get("content")) and "say plainly" in str(result.get("content"))
@pytest.mark.asyncio
async def test_connector_gate_without_a_requester_or_a_name(tmp_path):
events = await _run(_engine(tmp_path, None, "request_connector", {"connector": "github", "reason": "x"}))
assert not [e for e in events if e.type is EventType.CONNECTOR_REQUESTED]
events = await _run(_engine(tmp_path, lambda a, t=None: None, "grant_connector", {"connector": "github"}))
assert not [e for e in events if e.type is EventType.CONNECTOR_REQUESTED] # no worker named
# -- who gets which tool ------------------------------------------------------------------
class _Stub(ProviderClient):
def complete(self, **_kw): # pragma: no cover
return AssistantTurn()
def capabilities(self, _model): # pragma: no cover
return ModelCapabilities()
def test_tools_follow_the_role(tmp_path):
from coworker.agent import build_engine
from coworker.secrets import SecretStore
secrets = SecretStore(tmp_path / "s.json")
lead = Agent(name="lead", title="L", system_prompt="x", team="lead", connectors=("github",))
names = build_engine(agent=lead, workspace=tmp_path, provider=_Stub(), secrets=secrets).registry.names()
assert {"grant_connector", "request_connector"} <= set(names)
worker = Agent(name="w", title="W", system_prompt="x", team="worker", connectors=("github",))
names = build_engine(agent=worker, workspace=tmp_path, provider=_Stub(), secrets=secrets).registry.names()
assert "request_connector" in names and "grant_connector" not in names
solo = Agent(name="s", title="S", system_prompt="x") # no connectors declared → nothing to ask for
names = build_engine(agent=solo, workspace=tmp_path, provider=_Stub(), secrets=secrets).registry.names()
assert "request_connector" not in names and "grant_connector" not in names
# -- boxes review by default -----------------------------------------------------------------
def test_joined_boxes_have_the_reviewer_on_by_default(manager):
# A desktop keeps the flag's default (off); a joined box (remote.json present) is on…
assert manager.auto_approve() is False
(manager._data_base / "remote.json").write_text('{"controller": "x"}', encoding="utf-8")
assert manager.is_joined_box() and manager.auto_approve() is True
# …and an explicit preference wins either way.
manager.set_auto_approve(False)
assert manager.auto_approve() is False
# -- the gates work on background turns (headless boxes) ---------------------------------------
@pytest.mark.asyncio
async def test_staffing_gate_works_without_a_socket(manager):
"""Drill finding 2026-09-05: an event-driven lead (background delivery, no socket)
got "team staffing isn't available in this surface". The queue-backed handlers are
the default for every turn: the gate parks, the human resolves it from anywhere,
approval pre-spawns the team with the human's connector decisions."""
import asyncio
_lead(manager)
approve = manager.inbox_team_approver("lead-sid", "swe-lead")
from proposal_fixtures import team_proposal
task = asyncio.create_task(approve(team_proposal([{"persona": "swe-worker", "name": "nia", "connectors": ["github"]}, {"persona": "test-worker", "name": "checks"}]), "t1"))
for _ in range(50):
await asyncio.sleep(0.02)
pending = [i for i in manager.inbox.list(session_id="lead-sid", state="pending") if i.title == "Create this team?"]
if pending:
break
assert pending and "swe-worker" in pending[0].body
# The parked item carries the SAME payload the inline card gets, so the Inbox can render
# the real staffing card (roster, per-worker connector offer, chat flag) instead of a
# bare Approve that staffs everyone with nothing (owner-hit 2026-09-16 on a box).
data = pending[0].data or {}
assert data["gate"] == "team" and [m["name"] for m in data["members"]] == ["nia", "checks"]
assert data["enable_chat"] is False and set(data["offer"]) == {"swe-worker", "test-worker"}
# The human ticks nothing for nia (the lead suggested github) and approves.
manager.inbox.resolve(pending[0].id, '{"approved": true, "members": [{"persona": "swe-worker", "name": "nia", "connectors": []}, {"persona": "test-worker", "name": "checks", "connectors": []}]}')
result = await task
assert result["approved"] is True and [w["actor"] for w in result["workers"]] == ["nia", "checks"]
assert result["workers"][0]["connectors"] == []
# An engine built for a background delivery carries all three handlers.
engine = manager.get_engine("lead-sid")
assert engine.team_approver is not None and engine.items_approver is not None and engine.connector_requester is not None
@pytest.mark.asyncio
async def test_items_gate_and_connector_ask_work_without_a_socket(manager):
import asyncio
_lead(manager)
approve = manager.inbox_items_approver("lead-sid", "swe-lead")
from proposal_fixtures import work_proposal
proposal = work_proposal([{"title": "Cap the backoff", "criteria": "no wait exceeds maxDelayMs"}])
task = asyncio.create_task(approve(proposal, "t2"))
for _ in range(50):
await asyncio.sleep(0.02)
pending = [i for i in manager.inbox.list(session_id="lead-sid", state="pending") if i.title.startswith("Approve the proposed")]
if pending:
break
assert pending and "Acceptance criteria: no wait exceeds" in pending[0].body
assert (pending[0].data or {}).get("gate") == "items" and (pending[0].data or {})["items"][0]["title"] == "Cap the backoff"
# The lead's note reaches the Inbox card too (it used to be inline only).
assert pending[0].data["summary"] == proposal["summary"]
manager.inbox.resolve(pending[0].id, '{"approved": true}')
result = await task
assert result.get("approved") is True or result.get("ok") is True
# connect: an already-connected service short-circuits; an unknown one parks and a decline is plain
ask = manager.inbox_connector_requester("lead-sid", "swe-lead")
assert (await ask({"connector": "github", "reason": "x"}, "t3"))["connected"] is True
task = asyncio.create_task(ask({"connector": "linear", "reason": "keep the tracker in step"}, "t4"))
for _ in range(50):
await asyncio.sleep(0.02)
pending = [i for i in manager.inbox.list(session_id="lead-sid", state="pending") if i.title == "Connect linear?"]
if pending:
break
assert pending and pending[0].kind == "connector"
manager.inbox.resolve(pending[0].id, '{"approved": false}')
assert (await task)["approved"] is False
@pytest.mark.asyncio
async def test_background_approver_tells_the_lead_a_worker_is_waiting(manager):
"""Workers run on background turns: the manager's own approver must raise the
worker_waiting board event too (drill finding 2026-09-05 — only the socket
approver did, so a headless lead never heard its worker had parked)."""
import asyncio
from types import SimpleNamespace
_lead(manager)
sid = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia"}])["workers"][0]["session_id"]
approve = manager.inbox_approver(sid, "swe-worker")
request = SimpleNamespace(tool_name="run_shell", arguments={"command": "git status"}, tool_call_id="c1", metadata=None, reason="")
task = asyncio.create_task(approve(request))
for _ in range(50):
await asyncio.sleep(0.02)
pending = manager.inbox.list(session_id=sid, state="pending")
if pending:
break
assert pending and pending[0].title == "Run `run_shell`?"
team = manager.teams.for_lead_session("lead-sid")
waiting = [e for e in manager.team_store.subscribed_events(team.space, team.lead_actor) if e["kind"] == WORKER_WAITING]
assert waiting and waiting[0]["payload"]["prompt_id"] == pending[0].id and "git status" in waiting[0]["payload"]["preview"]
# The lead answers it with decide_worker_call → the worker's prompt resolves.
out = manager.decide_worker_call("lead-sid", "nia", pending[0].id, "allow", "plain repo inspection")
assert out["ok"] is True
outcome = await task
assert getattr(outcome, "value", outcome) in ("once", "allow") or outcome is not None
# -- worker models: the persona's list is the recommendation, not a binding ----------------
def test_worker_model_prefers_the_human_then_the_recommendation_then_the_lead(manager, monkeypatch):
opus, sol, sonnet = "anthropic:claude-opus-4-8", "openai:gpt-5.6-sol", "anthropic:claude-sonnet-4-6"
assert manager.persona_models("swe-worker") == [opus, sol]
runnable = {opus, sol, sonnet}
monkeypatch.setattr(manager, "model_selectable", lambda m: m in runnable)
# The human's choice on the card wins, even outside the recommended list.
assert manager.resolve_worker_model("swe-worker", human_pick=sonnet, lead_pick=sol, lead_model=sonnet) == (sonnet, "")
# The lead's pick counts only when it is recommended; otherwise the first recommended runnable.
assert manager.resolve_worker_model("swe-worker", lead_pick=sol, lead_model=sonnet) == (sol, "")
assert manager.resolve_worker_model("swe-worker", lead_pick=sonnet, lead_model=sonnet) == (opus, "")
# A human pick this machine cannot run is ignored, not obeyed into a dead worker.
assert manager.resolve_worker_model("swe-worker", human_pick="google:gemini-x", lead_model=sonnet) == (opus, "")
# Nothing recommended can run here: the lead's model, with a warning that says why.
runnable.clear(); runnable.add(sonnet)
model, warning = manager.resolve_worker_model("swe-worker", lead_model=sonnet)
assert model == sonnet
assert "lead's model" in warning and "Neither is set up on this machine" in warning
def test_team_card_payload_carries_models_for_the_machine_the_workers_run_on(manager, monkeypatch):
opus, sonnet = "anthropic:claude-opus-4-8", "anthropic:claude-sonnet-4-6"
monkeypatch.setattr(manager, "model_selectable", lambda m: m == sonnet)
monkeypatch.setattr(manager, "_curated_models", lambda: [sonnet, opus])
_lead(manager)
lead = manager.session_store.load("lead-sid"); lead.model = sonnet; manager.session_store.save(lead)
extras = manager.team_card_extras("lead-sid", [{"persona": "swe-worker", "name": "nia"}, {"persona": "test-worker", "name": "checks"}])
assert extras["lead_model"] == sonnet
assert extras["model_options"] == {"swe-worker": [], "test-worker": []}
assert [m["id"] for m in extras["runnable_models"]] == [sonnet] and extras["runnable_models"][0]["label"]
nia = extras["members"][0]
assert nia["resolved_model"] == sonnet and "lead's model" in nia["model_warning"]
# The human keeps nia on the fallback and the team is created on a model that runs.
result = manager.create_team("lead-sid", [{"persona": "swe-worker", "name": "nia", "model_by_human": sonnet}])
assert manager.session_store.load(result["workers"][0]["session_id"]).model == sonnet
# -- a lead's decision on a worker's waiting call (owner catch 2026-09-17) ----------------
def _lead_engine(tmp_path, mode, arguments, decided):
from coworker.teams.tools import decide_worker_call_tool
registry = ToolRegistry()
registry.register(decide_worker_call_tool(lambda *a: decided.append(a) or {"ok": True}))
return TurnEngine(
provider=_Scripted("decide_worker_call", arguments),
registry=registry,
permissions=PermissionEngine(workspace_root=tmp_path, mode=mode),
model="m",
)
@pytest.mark.asyncio
async def test_an_auto_approve_leads_denial_asks_nobody(tmp_path):
"""A denial runs nothing, so under Auto-Approve it needs neither the reviewer nor a
card: the human was being asked to click "Allow" to deny a command."""
decided = []
args = {"worker": "nia", "call_id": "c1", "decision": "deny", "note": "wrong worktree"}
events = await _run(_lead_engine(tmp_path, Mode.AUTO_APPROVE, args, decided))
assert EventType.PERMISSION_REQUIRED not in [e.type for e in events]
assert decided == [("nia", "c1", "deny", "wrong worktree")]
@pytest.mark.asyncio
async def test_an_allow_and_a_manual_leads_denial_still_ask(tmp_path):
for mode, decision in ((Mode.AUTO_APPROVE, "allow"), (Mode.INTERACTIVE, "deny")):
decided = []
args = {"worker": "nia", "call_id": "c1", "decision": decision, "note": "n"}
from coworker.engine import ApprovalOutcome
async def deny(_request):
return ApprovalOutcome.DENY
engine = _lead_engine(tmp_path, mode, args, decided)
# The harness must resolve a real original action before allowing a proxy.
engine.delegated_approval = lambda _args: {"tool": "run_shell", "arguments": {"command": "git status"}, "context": {}, "reason": "requires approval"}
engine.approver = deny # the human says no: the lead's decision must not run
events = await _run(engine)
assert EventType.PERMISSION_REQUIRED in [e.type for e in events], (mode, decision)
assert decided == [], (mode, decision)
def test_the_leads_approval_card_carries_the_workers_call(manager):
"""The parked approval for `decide_worker_call` shows WHAT the worker wants to run,
looked up by call_id — not the id."""
_lead(manager)
from coworker.teams.registry import TeamWorker
manager.teams.create(space="acme", lead_session="lead-sid", lead_actor="lead",
workers=[TeamWorker(actor="nia", persona="swe-worker", session_id="worker-sid")])
parked = manager.inbox.add_approval(
"worker-sid", "Run `run_shell`?", body="requires approval\ncommand: npm run build",
data={"tool": "run_shell", "arguments": {"command": "npm run build"}},
)
class Request:
tool_name = "decide_worker_call"
arguments = {"worker": "nia", "call_id": parked.id, "decision": "deny", "note": "wrong worktree"}
metadata = None
data = manager.approval_prompt_data("lead-sid", Request())
assert data["worker_call"] == {
"worker": "nia", "tool": "run_shell", "arguments": {"command": "npm run build"},
"reason": "requires approval", "state": "pending", "resolution": None,
}
# An unknown id (already cleaned up, or invented by the model) attaches nothing.
Request.arguments = {**Request.arguments, "call_id": "nope"}
assert "worker_call" not in manager.approval_prompt_data("lead-sid", Request())