This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/tests/test_dispatcher.py
Adam Moussa a705859ea3 Use the approved plan title as a conventional PR title
The apply pipeline's draft PRs were titled `agent-apply: <task_id>
(diff <hash>)` with a flat body — neither within Sea Haven PR
conventions. That format was deliberate injection-hardening (only
sanitized tokens, never model free-text; §4.6).

Thread the approved plan's title through dispatch as an optional
`pr_title` input, sanitized box-side (single line, no control chars,
70-char cap, capitalized) and RE-VALIDATED in the workflow as defense-
in-depth, with the hardened `agent-apply: <task_id>` title as the
fallback when empty/unsafe. Provenance (task id, diff hash, head) moves
to the PR body. gh consumes both as argv data, never shell-interpolated.
2026-06-25 13:43:30 -04:00

641 lines
23 KiB
Python

"""Unit tests for agent_team.dispatcher — the trusted apply-path transport (§4.3).
Fully hermetic: the branch-push and workflow-dispatch side effects are injected
fakes, so no git, no ``gh``, and no network are exercised. The tests pin the
pure input-assembly + validation contract and the push-before-dispatch order.
"""
from __future__ import annotations
import base64
import pytest
from agent_team.dispatcher import (
MAX_DIFF_BYTES,
MAX_PR_TITLE_LEN,
DispatcherError,
DispatchInputs,
DispatchResult,
app_branch_pusher,
app_run_locator,
app_workflow_dispatcher,
build_dispatch_inputs,
dispatch_apply_verify,
head_branch_for,
run_name_for,
sanitize_pr_title,
select_run_id,
)
from agent_team.state_store import compute_content_hash
TASK = "0a1b2c3d4e5f6071"
DIFF = "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1,2 @@\n title\n+added line\n"
SCOPE = "README.md\ndocs/**"
# --------------------------------------------------------------------------- #
# build_dispatch_inputs (pure)
# --------------------------------------------------------------------------- #
def test_build_dispatch_inputs_binds_hash_and_b64() -> None:
di = build_dispatch_inputs(task_id=TASK, diff_text=DIFF, declared_scope=SCOPE)
# expected_diff_hash is the plain sha256 the workflow's sha256sum reproduces.
assert di.expected_diff_hash == compute_content_hash(DIFF.encode("utf-8"))
# diff_b64 round-trips back to the exact diff bytes.
assert base64.b64decode(di.diff_b64).decode("utf-8") == DIFF
assert di.head_branch == f"agent-team/apply/{TASK}"
assert di.diff_artifact_name == f"agent-team-diff-{TASK}"
assert di.declared_scope == SCOPE
def test_as_inputs_keys_match_the_workflow_inputs() -> None:
di = build_dispatch_inputs(task_id=TASK, diff_text=DIFF, declared_scope=SCOPE)
assert set(di.as_inputs()) == {
"task_id",
"diff_artifact_name",
"expected_diff_hash",
"declared_scope",
"diff_b64",
"head_branch",
"pr_title",
}
def test_sanitize_pr_title_collapses_whitespace_and_capitalizes() -> None:
assert (
sanitize_pr_title(" add\tconfluence writer\nsection ")
== "Add confluence writer section"
)
def test_sanitize_pr_title_strips_control_chars() -> None:
# A newline / control char must never survive into the single-line PR title.
out = sanitize_pr_title("Add section\r\n\x00malicious")
assert "\n" not in out and "\r" not in out and "\x00" not in out
assert out == "Add section malicious"
def test_sanitize_pr_title_caps_length_on_word_boundary() -> None:
long = "Add a very long descriptive title " + "word " * 40
out = sanitize_pr_title(long)
assert len(out) <= MAX_PR_TITLE_LEN
assert not out.endswith(" ") # trimmed cleanly
@pytest.mark.parametrize("bad", ["", " ", "\n\t ", None, 123])
def test_sanitize_pr_title_returns_empty_for_unusable(bad) -> None:
assert sanitize_pr_title(bad) == ""
def test_build_dispatch_inputs_carries_sanitized_pr_title() -> None:
di = build_dispatch_inputs(
task_id=TASK,
diff_text=DIFF,
declared_scope=SCOPE,
pr_title=" document the writer\nlane ",
)
assert di.pr_title == "Document the writer lane"
def test_build_dispatch_inputs_pr_title_defaults_empty() -> None:
di = build_dispatch_inputs(task_id=TASK, diff_text=DIFF, declared_scope=SCOPE)
assert di.pr_title == ""
@pytest.mark.parametrize("bad_diff", ["", " ", "\n\n"])
def test_empty_diff_rejected(bad_diff: str) -> None:
with pytest.raises(DispatcherError):
build_dispatch_inputs(task_id=TASK, diff_text=bad_diff, declared_scope=SCOPE)
def test_oversized_diff_rejected() -> None:
# The diff rides a base64 workflow_dispatch input (GitHub ~64 KB cap); a diff
# over MAX_DIFF_BYTES must fail closed in the dispatcher, not be dispatched.
big = "diff --git a/x b/x\n" + "+" + ("x" * (MAX_DIFF_BYTES + 1)) + "\n"
with pytest.raises(DispatcherError):
build_dispatch_inputs(task_id=TASK, diff_text=big, declared_scope=SCOPE)
@pytest.mark.parametrize("bad_scope", ["", " "])
def test_empty_scope_rejected(bad_scope: str) -> None:
# An empty declared scope would let a diff touch ANY path — fail closed.
with pytest.raises(DispatcherError):
build_dispatch_inputs(task_id=TASK, diff_text=DIFF, declared_scope=bad_scope)
@pytest.mark.parametrize(
"bad_task", ["", "has space", "semi;colon", "../escape", "a/b", "x" * 201]
)
def test_unsafe_task_id_rejected(bad_task: str) -> None:
with pytest.raises(DispatcherError):
head_branch_for(bad_task)
with pytest.raises(DispatcherError):
build_dispatch_inputs(task_id=bad_task, diff_text=DIFF, declared_scope=SCOPE)
# --------------------------------------------------------------------------- #
# dispatch_apply_verify (injected seams)
# --------------------------------------------------------------------------- #
class _Recorder:
def __init__(self) -> None:
self.calls: list[dict] = []
def test_dispatch_pushes_then_fires_with_correct_inputs() -> None:
order: list[str] = []
pushed = _Recorder()
fired = _Recorder()
def pusher(*, owner, repo, base, head_branch, diff_text):
order.append("push")
pushed.calls.append(
{
"owner": owner,
"repo": repo,
"base": base,
"head": head_branch,
"diff": diff_text,
}
)
def dispatcher(*, owner, repo, inputs, ref):
order.append("dispatch")
fired.calls.append({"owner": owner, "repo": repo, "inputs": inputs, "ref": ref})
located = _Recorder()
def locator(*, owner, repo, task_id, since_iso):
order.append("locate")
located.calls.append(
{"owner": owner, "repo": repo, "task_id": task_id, "since_iso": since_iso}
)
return "27990718108"
result = dispatch_apply_verify(
owner="Sea-Haven-Industries",
repo="orchestrator",
task_id=TASK,
diff_text=DIFF,
declared_scope=SCOPE,
pusher=pusher,
dispatcher=dispatcher,
locator=locator,
)
assert isinstance(result, DispatchResult)
assert isinstance(result.inputs, DispatchInputs)
# run_id is captured from the locator and surfaced for the verifier.
assert result.run_id == "27990718108"
assert result.correlation_tag == TASK
assert result.dispatched_at # stamped, non-empty
# Push BEFORE dispatch BEFORE locate (the run can only be located after it is
# triggered, and the branch must exist before the run reaches the PR step).
assert order == ["push", "dispatch", "locate"]
assert pushed.calls[0]["head"] == f"agent-team/apply/{TASK}"
assert pushed.calls[0]["diff"] == DIFF
# The dispatch carries all six inputs, including the b64 diff + head branch.
inputs = fired.calls[0]["inputs"]
assert inputs["head_branch"] == f"agent-team/apply/{TASK}"
assert base64.b64decode(inputs["diff_b64"]).decode("utf-8") == DIFF
assert inputs["expected_diff_hash"] == compute_content_hash(DIFF.encode("utf-8"))
assert fired.calls[0]["ref"] == "main"
# The locator is keyed by THIS task and the dispatched-at watermark.
assert located.calls[0]["task_id"] == TASK
assert located.calls[0]["since_iso"] == result.dispatched_at
def test_dispatch_returns_none_run_id_when_locator_cannot_resolve() -> None:
# A fired-but-unlocatable run fails closed (None run_id); never raises here.
result = dispatch_apply_verify(
owner="o",
repo="r",
task_id=TASK,
diff_text=DIFF,
declared_scope=SCOPE,
pusher=lambda **_k: None,
dispatcher=lambda **_k: None,
locator=lambda **_k: None,
)
assert isinstance(result, DispatchResult)
assert result.run_id is None
assert result.dispatched_at # still stamped for the CI-watch timeout
def test_run_name_for_matches_workflow_run_name_convention() -> None:
# Mirrors run-name: "agent-team-apply ${{ inputs.task_id }}" in the workflow.
assert run_name_for(TASK) == f"agent-team-apply {TASK}"
def test_dispatch_does_not_fire_if_push_fails() -> None:
fired = _Recorder()
def failing_pusher(**_kw):
raise RuntimeError("push failed")
def dispatcher(**kw):
fired.calls.append(kw)
with pytest.raises(RuntimeError):
dispatch_apply_verify(
owner="o",
repo="r",
task_id=TASK,
diff_text=DIFF,
declared_scope=SCOPE,
pusher=failing_pusher,
dispatcher=dispatcher,
)
# A failed push must NOT dispatch a run (no orphan run against a missing head).
assert fired.calls == []
@pytest.mark.parametrize(
"owner,repo", [("", "r"), ("o", ""), ("bad owner", "r"), ("o", "r/x")]
)
def test_unsafe_owner_repo_rejected(owner: str, repo: str) -> None:
with pytest.raises(DispatcherError):
dispatch_apply_verify(
owner=owner,
repo=repo,
task_id=TASK,
diff_text=DIFF,
declared_scope=SCOPE,
pusher=lambda **_k: None,
dispatcher=lambda **_k: None,
)
# --------------------------------------------------------------------------- #
# select_run_id (pure; anti-stale on rapid re-dispatch of the SAME task_id)
# --------------------------------------------------------------------------- #
def _row(db_id, *, created, status="completed", conclusion=None):
"""A minimal ``gh run list`` row for the apply/verify run-name of TASK."""
return {
"databaseId": db_id,
"name": run_name_for(TASK),
"createdAt": created,
"status": status,
"conclusion": conclusion,
}
def test_select_run_id_skips_cancelled_prior_run_on_re_dispatch() -> None:
# Rapid re-dispatch of the SAME task_id: the concurrency group cancelled the
# OLDER run, and a NEWER run is now in progress. We must bind to the newer,
# active run — never the older cancelled one (it carries the prior verdict).
older_cancelled = _row(
100, created="2026-06-23T10:00:00Z", status="completed", conclusion="cancelled"
)
newer_active = _row(
200, created="2026-06-23T10:05:00Z", status="in_progress", conclusion=None
)
runs = [newer_active, older_cancelled]
chosen = select_run_id(runs, task_id=TASK, floor_iso="2026-06-23T09:58:00Z")
assert chosen == "200"
def test_select_run_id_skips_cancelled_even_when_it_is_newest() -> None:
# Defensive: a cancelled run is NEVER selected even if its createdAt is the
# greatest — it is the superseded run, not ours.
active = _row(300, created="2026-06-23T10:00:00Z", status="queued", conclusion=None)
newest_cancelled = _row(
400, created="2026-06-23T10:10:00Z", status="completed", conclusion="cancelled"
)
runs = [active, newest_cancelled]
chosen = select_run_id(runs, task_id=TASK, floor_iso="2026-06-23T09:58:00Z")
assert chosen == "300"
def test_select_run_id_prefers_newest_active_over_older_completed() -> None:
# An older legitimately-completed run plus a newer active run -> the active,
# newest run wins (the freshly-triggered one with no conclusion yet).
older_done = _row(
500, created="2026-06-23T10:00:00Z", status="completed", conclusion="success"
)
newer_active = _row(
600, created="2026-06-23T10:05:00Z", status="in_progress", conclusion=None
)
chosen = select_run_id(
[older_done, newer_active], task_id=TASK, floor_iso="2026-06-23T09:58:00Z"
)
assert chosen == "600"
def test_select_run_id_falls_back_to_newest_non_cancelled_when_none_active() -> None:
# No active runs (e.g. a fast run already concluded by the time we poll):
# fall back to the newest NON-cancelled run overall.
older = _row(
700, created="2026-06-23T10:00:00Z", status="completed", conclusion="success"
)
newer = _row(
800, created="2026-06-23T10:05:00Z", status="completed", conclusion="failure"
)
cancelled = _row(
900, created="2026-06-23T10:09:00Z", status="completed", conclusion="cancelled"
)
chosen = select_run_id(
[older, newer, cancelled], task_id=TASK, floor_iso="2026-06-23T09:58:00Z"
)
assert chosen == "800"
def test_select_run_id_respects_created_floor_and_run_name() -> None:
# Below-floor runs and other-task runs are not matched.
below_floor = _row(
1000, created="2026-06-23T09:00:00Z", status="in_progress", conclusion=None
)
other_task = {
"databaseId": 1100,
"name": "agent-team-apply other-task",
"createdAt": "2026-06-23T10:00:00Z",
"status": "in_progress",
"conclusion": None,
}
assert (
select_run_id(
[below_floor, other_task], task_id=TASK, floor_iso="2026-06-23T09:58:00Z"
)
is None
)
def test_select_run_id_returns_none_when_only_cancelled_matches() -> None:
# If the only matching run is cancelled, there is nothing to bind to -> None
# (the caller fails closed: no run_id -> verify BLOCKs/parks).
only_cancelled = _row(
1200, created="2026-06-23T10:00:00Z", status="completed", conclusion="cancelled"
)
assert (
select_run_id([only_cancelled], task_id=TASK, floor_iso="2026-06-23T09:58:00Z")
is None
)
# --------------------------------------------------------------------------- #
# app_branch_pusher (App-token git seam; secret hygiene)
# --------------------------------------------------------------------------- #
class _StubTokenProvider:
"""Minimal token provider: returns a fixed installation token."""
def __init__(self, token: str = "ghs_TESTTOKEN") -> None:
self._token = token
def token(self) -> str:
return self._token
def test_app_branch_pusher_keeps_token_out_of_argv_and_uses_plain_remote() -> None:
# The clone must use the PLAIN HTTPS remote (no token in the URL/argv), and
# the installation token must be injected via http.extraHeader through git's
# GIT_CONFIG_* env vars on the network steps (clone + push) — never in argv.
recorded: list[dict] = []
def fake_run(cmd, *, cwd=None, env=None):
recorded.append({"cmd": list(cmd), "env": env})
push = app_branch_pusher(_StubTokenProvider(), _run=fake_run)
push(
owner="owner",
repo="repo",
base="main",
head_branch="agent-team/apply/abc",
diff_text=DIFF,
)
clone_cmd = recorded[0]["cmd"]
assert "clone" in clone_cmd
assert "https://github.com/owner/repo.git" in clone_cmd
# The token NEVER appears in any git argv element.
for call in recorded:
for arg in call["cmd"]:
assert "ghs_TESTTOKEN" not in arg
# Auth rides in GIT_CONFIG_* env on the network steps only (clone + push), as
# BASIC auth (username x-access-token) — git smart-HTTP rejects Bearer.
expected_basic = "Authorization: Basic " + base64.b64encode(
b"x-access-token:ghs_TESTTOKEN"
).decode("ascii")
clone_env = recorded[0]["env"]
assert clone_env["GIT_CONFIG_KEY_0"] == "http.https://github.com/.extraHeader"
assert clone_env["GIT_CONFIG_VALUE_0"] == expected_basic
push_env = recorded[-1]["env"]
assert push_env["GIT_CONFIG_VALUE_0"] == expected_basic
# The raw token never appears literally in the auth header (it is base64'd).
assert "ghs_TESTTOKEN" not in clone_env["GIT_CONFIG_VALUE_0"]
# The non-network steps (checkout/apply/commit) carry no auth env.
for call in recorded[1:4]:
assert call["env"] is None
# The five expected git steps fired in order. Skip the global ``git``
# options (``-C <dir>``, ``-c <key=val>``) to find each subcommand.
def _git_subcommand(cmd: list[str]) -> str:
i = 1 # cmd[0] == "git"
while i < len(cmd) and cmd[i] in ("-C", "-c"):
i += 2 # each takes one argument
return cmd[i]
steps = [_git_subcommand(c["cmd"]) for c in recorded]
assert steps == ["clone", "checkout", "apply", "commit", "push"]
def test_app_branch_pusher_scrubs_token_from_errors() -> None:
# The token rides in the http.extraHeader config value (passed via env, not
# argv). If a failing git step's exception string ever surfaces that header,
# it must NEVER leak the token — it is re-raised as a DispatcherError with the
# token scrubbed to ***.
def failing_run(cmd, *, cwd=None, env=None):
# Simulate an error whose string carries the Authorization header value.
raise RuntimeError(
"fatal: remote rejected (Authorization: Bearer ghs_TESTTOKEN)"
)
push = app_branch_pusher(_StubTokenProvider(), _run=failing_run)
with pytest.raises(DispatcherError) as excinfo:
push(
owner="owner",
repo="repo",
base="main",
head_branch="agent-team/apply/abc",
diff_text=DIFF,
)
assert "ghs_TESTTOKEN" not in str(excinfo.value)
assert "***" in str(excinfo.value)
# ``from None``: the token-bearing original is not chained onto the raised error.
assert excinfo.value.__cause__ is None
# --------------------------------------------------------------------------- #
# app_workflow_dispatcher (App-token REST seam)
# --------------------------------------------------------------------------- #
class _FakeHttp:
"""Records POST/GET calls; returns a canned response object."""
def __init__(self, *, post_status=204, get_body=None, get_status=200) -> None:
self.post_calls: list[dict] = []
self.get_calls: list[dict] = []
self._post_status = post_status
self._get_body = get_body or {}
self._get_status = get_status
def post(self, url, *, json=None, headers=None, timeout=None):
self.post_calls.append({"url": url, "json": json, "headers": headers})
return _FakeResp(status_code=self._post_status)
def get(self, url, *, params=None, headers=None, timeout=None):
self.get_calls.append({"url": url, "params": params, "headers": headers})
return _FakeResp(status_code=self._get_status, body=self._get_body)
class _FakeResp:
def __init__(self, *, status_code=200, body=None) -> None:
self.status_code = status_code
self._body = body or {}
def json(self):
return self._body
def test_app_workflow_dispatcher_posts_correct_url_and_body() -> None:
http = _FakeHttp(post_status=204)
fire = app_workflow_dispatcher(_StubTokenProvider(), _http=http)
inputs = {"task_id": TASK, "diff_b64": "x"}
fire(owner="owner", repo="repo", inputs=inputs, ref="main")
call = http.post_calls[0]
assert call["url"].endswith(
"/actions/workflows/agent-team-apply-verify.yml/dispatches"
)
assert call["json"] == {"ref": "main", "inputs": inputs}
# The installation token is carried as a Bearer header (and nowhere else).
assert call["headers"]["Authorization"] == "Bearer ghs_TESTTOKEN"
def test_app_workflow_dispatcher_fails_closed_without_token_in_message() -> None:
http = _FakeHttp(post_status=422)
fire = app_workflow_dispatcher(_StubTokenProvider(), _http=http)
with pytest.raises(DispatcherError) as excinfo:
fire(owner="owner", repo="repo", inputs={"task_id": TASK}, ref="main")
assert "422" in str(excinfo.value)
assert "ghs_TESTTOKEN" not in str(excinfo.value)
class _RaisingHttp:
"""A transport whose post/get raises an exception that embeds the token."""
def post(self, url, *, json=None, headers=None, timeout=None):
raise RuntimeError(f"connection reset: {headers['Authorization']}")
def get(self, url, *, params=None, headers=None, timeout=None):
raise RuntimeError(f"connection reset: {headers['Authorization']}")
def test_app_workflow_dispatcher_scrubs_token_from_transport_error() -> None:
# A transport exception must be re-raised as a DispatcherError carrying the
# exception TYPE only — never the Bearer token, even if the underlying error
# text embedded it.
fire = app_workflow_dispatcher(_StubTokenProvider(), _http=_RaisingHttp())
with pytest.raises(DispatcherError) as excinfo:
fire(owner="owner", repo="repo", inputs={"task_id": TASK}, ref="main")
assert "ghs_TESTTOKEN" not in str(excinfo.value)
assert excinfo.value.__cause__ is None # `from None` breaks the chain
# --------------------------------------------------------------------------- #
# app_run_locator (App-token REST seam; field mapping + run-name correlation)
# --------------------------------------------------------------------------- #
def test_app_run_locator_maps_rest_fields_and_correlates_run_name() -> None:
# The REST list endpoint uses id/created_at; the locator must map them to the
# databaseId/createdAt names select_run_id reads, and correlate on run-name.
body = {
"workflow_runs": [
{
"id": 12345,
"name": run_name_for(TASK),
"created_at": "2026-06-23T10:00:00Z",
"status": "in_progress",
"conclusion": None,
}
]
}
http = _FakeHttp(get_body=body)
locate = app_run_locator(_StubTokenProvider(), _http=http, _sleep=lambda *_a: None)
run_id = locate(
owner="owner",
repo="repo",
task_id=TASK,
since_iso="2026-06-23T09:58:00Z",
)
assert run_id == "12345"
# The list GET is authenticated + filtered to workflow_dispatch events.
call = http.get_calls[0]
assert call["url"].endswith("/actions/runs")
assert call["params"]["event"] == "workflow_dispatch"
assert call["headers"]["Authorization"] == "Bearer ghs_TESTTOKEN"
def test_app_run_locator_returns_none_for_non_matching_task() -> None:
body = {
"workflow_runs": [
{
"id": 999,
"name": "agent-team-apply someone-else",
"created_at": "2026-06-23T10:00:00Z",
"status": "in_progress",
"conclusion": None,
}
]
}
http = _FakeHttp(get_body=body)
locate = app_run_locator(_StubTokenProvider(), _http=http, _sleep=lambda *_a: None)
assert (
locate(
owner="owner",
repo="repo",
task_id=TASK,
since_iso="2026-06-23T09:58:00Z",
)
is None
)
def test_app_run_locator_raises_on_auth_error_without_token_in_message() -> None:
# A 401/403/404 list response must park immediately with the STATUS only
# (never the token), not silently poll ~60s and return None as "no runs".
http = _FakeHttp(get_status=403, get_body={"message": "Bad credentials"})
locate = app_run_locator(_StubTokenProvider(), _http=http, _sleep=lambda *_a: None)
with pytest.raises(DispatcherError) as excinfo:
locate(
owner="owner",
repo="repo",
task_id=TASK,
since_iso="2026-06-23T09:58:00Z",
)
assert "403" in str(excinfo.value)
assert "ghs_TESTTOKEN" not in str(excinfo.value)
def test_app_run_locator_scrubs_token_from_transport_error() -> None:
locate = app_run_locator(
_StubTokenProvider(), _http=_RaisingHttp(), _sleep=lambda *_a: None
)
with pytest.raises(DispatcherError) as excinfo:
locate(
owner="owner",
repo="repo",
task_id=TASK,
since_iso="2026-06-23T09:58:00Z",
)
assert "ghs_TESTTOKEN" not in str(excinfo.value)
assert excinfo.value.__cause__ is None