rara/agent-ops/skills/common/orchestrate-agent-task-loop/tests/test_dispatch.py
toki 4fda90e4c2
Some checks are pending
ci / validate (push) Waiting to run
sync: agent-ops from agentic-framework v1.1.188
2026-08-06 12:13:32 +09:00

330 lines
13 KiB
Python

import argparse
import asyncio
import importlib.util
import json
import os
import stat
import subprocess
import sys
import unittest
from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import mock
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "dispatch.py"
SPEC = importlib.util.spec_from_file_location("agent_task_dispatch_test", SCRIPT)
dispatch = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = dispatch
SPEC.loader.exec_module(dispatch)
def catalog_value(command: str = "/bin/true") -> dict:
targets = {
"primary": {
"agent": "runner-primary",
"model": "model-primary",
"execution_class": "local_model",
"selfcheck_required": True,
"runtime": {
"command": [command, "--workspace", "{workspace}", "--model", "{model}", "{prompt}"],
"resume_command": [command, "--resume", "{resume_session}", "{prompt}"],
"environment": {"TARGET_ID": "{target_id}"},
"output_format": "jsonl",
"native_session_monitor": True,
"session_path": "sessions/{session_id}.jsonl",
},
},
"alternate": {
"agent": "runner-alternate",
"model": "model-alternate",
"execution_class": "cloud_model",
"runtime": {"command": [command, "{prompt}"]},
},
}
routes = {"worker": {}, "review": {}}
for stage in routes:
for lane in ("local", "cloud"):
for grade in range(1, 11):
routes[stage][f"{lane}-G{grade:02d}"] = {
"candidates": ["primary", "alternate"],
"rule_id": f"{stage}-{lane}-{grade:02d}",
"reason_codes": ["injected-route"],
}
return {"schema_version": "1.0", "targets": targets, "routes": routes}
def write_catalog(root: Path, value: dict | None = None) -> Path:
path = root / "execution-catalog.json"
path.write_text(json.dumps(value or catalog_value()), encoding="utf-8")
return path
def write_plan(root: Path, *, task_name: str = "group/01_task") -> Path:
directory = root / "agent-task" / task_name
directory.mkdir(parents=True, exist_ok=True)
path = directory / "PLAN-cloud-G05.md"
path.write_text(
f"<!-- task={task_name} plan=0 tag=API -->\n\n"
"# Plan\n\n## Modified Files Summary\n\n"
"| File | Action |\n|---|---|\n| `src/item.txt` | modify |\n",
encoding="utf-8",
)
return path
def task_from_plan(root: Path, plan: Path) -> dispatch.Task:
directory = plan.parent
return dispatch.Task(
name="group/01_task",
directory=directory,
plan=plan,
review=None,
user_review=None,
recovery=False,
index=1,
write_set={"src/item.txt"},
write_set_known=True,
plan_hash=dispatch.sha256_file(plan),
)
class RuntimeCatalogDispatcherTests(unittest.TestCase):
def setUp(self):
self.previous_catalog = dispatch.EXECUTION_CATALOG_PATH
def tearDown(self):
dispatch.EXECUTION_CATALOG_PATH = self.previous_catalog
def test_agent_spec_is_loaded_from_persisted_catalog_evidence(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
catalog = write_catalog(root)
plan = write_plan(root)
dispatch.EXECUTION_CATALOG_PATH = catalog
selector = dispatch._selector_module()
decision = selector.select_execution_target(plan, catalog_path=catalog)
spec = dispatch.agent_spec_from_decision(decision)
self.assertEqual(spec.target_id, "primary")
self.assertEqual(spec.cli, "runner-primary")
self.assertEqual(spec.model, "model-primary")
self.assertTrue(spec.native_resume)
self.assertEqual(spec.runtime["command"][0], "/bin/true")
def test_agent_spec_rejects_catalog_change_after_selection(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
catalog = write_catalog(root)
plan = write_plan(root)
selector = dispatch._selector_module()
decision = selector.select_execution_target(plan, catalog_path=catalog)
changed = catalog_value()
changed["targets"]["primary"]["model"] = "changed"
catalog.write_text(json.dumps(changed), encoding="utf-8")
with self.assertRaisesRegex(dispatch.ExecutionDecisionError, "변경"):
dispatch.agent_spec_from_decision(decision)
def test_command_is_expanded_only_from_runtime_template(self):
spec = dispatch.AgentSpec(
"opaque-agent",
"opaque-model",
"opaque-agent/opaque-model",
target_id="opaque-id",
runtime={
"command": ["runner", "{workspace}", "{model}", "{session_id}", "{attempt_dir}", "{prompt}"],
"resume_command": ["runner", "resume", "{resume_session}", "{prompt}"],
},
)
command = dispatch.build_command(
spec,
"do work",
Path("/workspace"),
"session-1",
Path("/attempt"),
)
resumed = dispatch.build_command(
spec,
"continue",
Path("/workspace"),
"session-1",
Path("/attempt"),
native_resume_session=Path("/attempt/session.jsonl"),
)
self.assertEqual(command, ["runner", "/workspace", "opaque-model", "session-1", "/attempt", "do work"])
self.assertEqual(resumed, ["runner", "resume", "/attempt/session.jsonl", "continue"])
def test_preflight_checks_executable_and_optional_probe(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
value = catalog_value("/bin/true")
value["targets"]["primary"]["runtime"]["preflight_command"] = ["/bin/true", "--check"]
catalog = write_catalog(root, value)
dispatch.preflight_execution_catalog(catalog)
def test_preflight_rejects_missing_command(self):
with TemporaryDirectory() as tmp:
catalog = write_catalog(Path(tmp), catalog_value("definitely-missing-command"))
with self.assertRaisesRegex(dispatch.ExecutionDecisionError, "command not found"):
dispatch.preflight_execution_catalog(catalog)
def test_persisted_decision_failover_uses_next_runtime_target(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
catalog = write_catalog(root)
plan = write_plan(root)
task = task_from_plan(root, plan)
dispatch.EXECUTION_CATALOG_PATH = catalog
with mock.patch.dict(os.environ, {"XDG_STATE_HOME": str(root / "state")}):
store = dispatch.StateStore(root)
try:
initial, first_spec = dispatch.persisted_execution_decision(store, task, stage="worker")
failed, second_spec = dispatch.persisted_execution_decision(
store,
task,
stage="worker",
transition="failover",
failure_class="provider-quota",
)
finally:
store.close()
self.assertEqual(initial["selected"]["target_id"], "primary")
self.assertEqual(first_spec.model, "model-primary")
self.assertEqual(failed["selected"]["target_id"], "alternate")
self.assertEqual(second_spec.model, "model-alternate")
self.assertNotIn("quota", failed)
def test_retry_blocked_marks_failover_without_quota_state(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
catalog = write_catalog(root)
plan = write_plan(root)
task = task_from_plan(root, plan)
dispatch.EXECUTION_CATALOG_PATH = catalog
with mock.patch.dict(os.environ, {"XDG_STATE_HOME": str(root / "state")}):
store = dispatch.StateStore(root)
try:
decision, _ = dispatch.persisted_execution_decision(store, task, stage="worker")
state = store.task_state(task)
state.update(
blocked="runtime failure",
blocker_evidence={
"role": "worker",
"failure_class": "provider-quota",
"locator": "/tmp/locator.json",
"selected": decision["selected"],
"work_unit_id": decision["work_unit_id"],
},
)
store.save()
store.mark_retry_failover("group")
state = store.task_state(task)
finally:
store.close()
self.assertTrue(state["retry_failover_pending"])
self.assertNotIn("quota_snapshot", state)
self.assertNotIn("retry_quota_refresh_pending", state)
def test_runtime_error_classifier_keeps_provider_quota(self):
failure, evidence = dispatch.classify_failure_with_evidence(
"HTTP 429 resource exhausted: quota reached"
)
self.assertEqual(failure, "provider-quota")
self.assertIsNotNone(evidence)
def test_generic_json_terminal_diagnostic_has_no_agent_branch(self):
diagnostic = dispatch.terminal_diagnostic(
"opaque-agent",
"stdout",
json.dumps({"type": "turn.failed", "error": {"code": 429}}),
)
self.assertIn("429", diagnostic or "")
self.assertIsNone(
dispatch.terminal_diagnostic(
"opaque-agent",
"stdout",
json.dumps({"type": "message", "text": "quota design notes"}),
)
)
def test_catalog_source_is_in_runtime_audit_evidence(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
catalog = write_catalog(root)
plan = write_plan(root)
selector = dispatch._selector_module()
decision = selector.select_execution_target(plan, catalog_path=catalog)
evidence = dispatch.selector_runtime_evidence(decision)
self.assertEqual(evidence["catalog"]["source"], str(catalog.resolve()))
self.assertNotIn("quota", evidence)
class GenericDispatcherContractTests(unittest.TestCase):
def test_parallel_limit_contract(self):
self.assertEqual(dispatch.validated_max_parallel(0), 0)
self.assertEqual(dispatch.validated_max_parallel(3), 3)
with self.assertRaises(ValueError):
dispatch.validated_max_parallel(-1)
def test_modified_files_summary_is_canonicalized(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
plan = write_plan(root)
write_set, diagnostics = dispatch.inspect_write_set(plan, root)
self.assertEqual(diagnostics, [])
self.assertEqual(write_set, {str((root / "src/item.txt").resolve())})
def test_outside_workspace_claim_is_rejected(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
plan = write_plan(root)
plan.write_text(
plan.read_text(encoding="utf-8").replace("`src/item.txt`", "`../outside.txt`"),
encoding="utf-8",
)
_, diagnostics = dispatch.inspect_write_set(plan, root)
self.assertTrue(any("outside" in item.lower() or "workspace" in item.lower() for item in diagnostics))
def test_selector_evidence_uses_agent_model_fields(self):
decision = {
"work_unit_id": "group/01::plan-0::tag-API",
"selected": {"target_id": "a", "agent": "runner", "model": "model"},
"candidates": [
{"candidate_rank": 1, "target_id": "a", "agent": "runner", "model": "model"}
],
"decision": {"rule_id": "rule", "policy_priority": 1, "reason_codes": []},
"transition": {"trigger": "initial"},
}
lines = dispatch.selector_evidence_lines(decision)
self.assertIn("candidates=#1:runner/model", lines)
self.assertFalse(any("quota" in line for line in lines))
def test_validate_plan_mode_does_not_require_catalog(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
plan = write_plan(root)
completed = subprocess.run(
[sys.executable, str(SCRIPT), "--workspace", str(root), "--validate-plan", str(plan)],
capture_output=True,
text=True,
env={key: value for key, value in os.environ.items() if key != "AGENT_TASK_EXECUTION_CATALOG"},
check=False,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
def test_dry_run_requires_catalog(self):
with TemporaryDirectory() as tmp:
completed = subprocess.run(
[sys.executable, str(SCRIPT), "--workspace", tmp, "--dry-run"],
capture_output=True,
text=True,
env={key: value for key, value in os.environ.items() if key != "AGENT_TASK_EXECUTION_CATALOG"},
check=False,
)
self.assertEqual(completed.returncode, 2)
self.assertIn("missing_execution_catalog", completed.stderr)
if __name__ == "__main__":
unittest.main()