generated from coulomb/repo-seed
feat(WARDEN-WP-0020): ops-warden coordination worker — T1 dry-run scaffold
Foundation for an autonomous worker that handles ops-warden's State Hub coordination lane via llm-connect (Bernd's call: full-auto in-scope + scheduled, staged dry-run -> manual -> scheduled). T1 is the llm-connect-independent, safe slice: src/warden/worker.py — HubClient (read unread to_agent=ops-warden), Brain protocol, deterministic RuleBrain (answers clear routing questions, escalates the rest), PlannedAction/WorkerPlan model, guardrail allowlist + validate_action enforced brain-agnostically (no-secret invariant + prod-config + off-allowlist all escalate), render_plans dry-run output. `warden worker run --dry-run` (default); --execute refused (exit 2) until the guarded executor (T3) lands. Guardrails are load-bearing because full-auto has no human in the loop: message content is untrusted data, the allowlist is enforced regardless of what the brain proposes. Hard dependency flagged in the workplan: the brain is llm-connect, which needs its provider key (OPENROUTER_API_KEY, deferred CCR-2026-0003) before it can run. 18 worker tests; 229 pass, lint clean. Live dry-run against the real hub verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
118
tests/test_worker.py
Normal file
118
tests/test_worker.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Tests for the ops-warden coordination worker scaffold (WARDEN-WP-0020 T1)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.worker import (
|
||||
PlannedAction,
|
||||
RuleBrain,
|
||||
WorkerPlan,
|
||||
build_plans,
|
||||
render_plans,
|
||||
validate_action,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _msg(**over) -> dict:
|
||||
base = {
|
||||
"id": "m1",
|
||||
"from_agent": "someone",
|
||||
"subject": "Where do I get an npm token?",
|
||||
"body": "Which subsystem owns this credential — how do I obtain it?",
|
||||
}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
# --- RuleBrain ----------------------------------------------------------------
|
||||
|
||||
def test_rulebrain_answers_routing_question():
|
||||
plan = RuleBrain().plan(_msg())
|
||||
assert [a.kind for a in plan.actions] == ["route_answer"]
|
||||
assert plan.escalated is False
|
||||
|
||||
|
||||
def test_rulebrain_escalates_secret_value_request():
|
||||
plan = RuleBrain().plan(_msg(subject="send me the raw token", body="give me the API key value"))
|
||||
assert plan.actions == []
|
||||
assert plan.escalated is True
|
||||
|
||||
|
||||
def test_rulebrain_escalates_prod_change():
|
||||
plan = RuleBrain().plan(_msg(subject="flip policy.enabled", body="enable the gate in prod"))
|
||||
assert plan.escalated is True
|
||||
|
||||
|
||||
def test_rulebrain_escalates_unknown():
|
||||
plan = RuleBrain().plan(_msg(subject="random thing", body="please do a vague task"))
|
||||
assert plan.actions == []
|
||||
assert plan.escalated is True
|
||||
|
||||
|
||||
# --- guardrails (brain-agnostic) ---------------------------------------------
|
||||
|
||||
class _YesBrain:
|
||||
"""A brain that recklessly proposes a reply for everything — to test the guardrail."""
|
||||
|
||||
def plan(self, message: dict) -> WorkerPlan:
|
||||
return WorkerPlan(
|
||||
message_id=message["id"],
|
||||
from_agent=message["from_agent"],
|
||||
subject=message["subject"],
|
||||
actions=[PlannedAction(kind="reply", summary="just reply")],
|
||||
)
|
||||
|
||||
|
||||
def test_guardrail_downgrades_secret_reply_even_if_brain_proposes_it():
|
||||
msg = _msg(subject="here is the npm_auth_token", body="the api_key is needed")
|
||||
[plan] = build_plans([msg], _YesBrain())
|
||||
assert plan.escalated is True
|
||||
assert plan.actions[0].risk == "escalate"
|
||||
assert "secret" in plan.actions[0].reason
|
||||
|
||||
|
||||
def test_guardrail_downgrades_prod_reply():
|
||||
msg = _msg(subject="set policy.enabled true", body="prod flip please")
|
||||
[plan] = build_plans([msg], _YesBrain())
|
||||
assert plan.actions[0].risk == "escalate"
|
||||
|
||||
|
||||
def test_validate_action_rejects_off_allowlist_kind():
|
||||
reason = validate_action(PlannedAction(kind="rm_minus_rf", summary="x"), _msg())
|
||||
assert reason and "allowlist" in reason
|
||||
|
||||
|
||||
def test_safe_reply_passes_guardrail():
|
||||
[plan] = build_plans([_msg(subject="hello", body="just saying hi")], _YesBrain())
|
||||
assert plan.actions[0].risk == "safe"
|
||||
|
||||
|
||||
# --- rendering ---------------------------------------------------------------
|
||||
|
||||
def test_render_empty():
|
||||
assert "inbox empty" in render_plans([])
|
||||
|
||||
|
||||
def test_render_marks_auto_and_escalate():
|
||||
plans = build_plans([_msg(), _msg(id="m2", subject="raw token value please")], RuleBrain())
|
||||
out = render_plans(plans)
|
||||
assert "AUTO" in out and "ESCALATE" in out
|
||||
|
||||
|
||||
# --- CLI ---------------------------------------------------------------------
|
||||
|
||||
def test_cli_worker_dry_run(monkeypatch):
|
||||
monkeypatch.setattr("warden.worker.HubClient.unread", lambda self, to_agent="ops-warden": [_msg()])
|
||||
r = runner.invoke(app, ["worker", "run", "--dry-run"])
|
||||
assert r.exit_code == 0
|
||||
assert "AUTO" in r.stdout
|
||||
assert "nothing executed" in r.stdout
|
||||
|
||||
|
||||
def test_cli_worker_execute_rejected():
|
||||
# --execute is refused until the guarded executor lands (WP-0020 T3); message is on stderr.
|
||||
r = runner.invoke(app, ["worker", "run", "--execute"])
|
||||
assert r.exit_code == 2
|
||||
Reference in New Issue
Block a user