feat: engagement CLI for forward-deployed agency (WP-0009 T08)
Add kaizen-agentic engagement subcommands (list, show, validate, checklist, phase, prepare, staff, quote, scrub, export-handoff) backed by engagement.py for file-based pilot lifecycle. Tests cover staff/validate/phase/prepare and the railiance01 pilot smoke path.
This commit is contained in:
parent
600954500f
commit
ad5965be91
6 changed files with 1661 additions and 7 deletions
365
tests/test_engagement_cli.py
Normal file
365
tests/test_engagement_cli.py
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
"""CLI + module tests for forward-deployed engagements (KAIZEN-WP-0009 T08)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from click.testing import CliRunner
|
||||
|
||||
from kaizen_agentic.cli import cli
|
||||
from kaizen_agentic.engagement import (
|
||||
EngagementError,
|
||||
build_prepare_bundle,
|
||||
checklist_summary,
|
||||
load_checklist,
|
||||
load_engagement,
|
||||
parse_checklist_markdown,
|
||||
set_checklist_item_status,
|
||||
set_phase,
|
||||
staff_engagement,
|
||||
validate_engagement,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner() -> CliRunner:
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mini_repo(tmp_path: Path) -> Path:
|
||||
"""Minimal repo with host-operator Role package."""
|
||||
root = tmp_path / "kaizen-root"
|
||||
role = root / "roles" / "host-operator"
|
||||
role.mkdir(parents=True)
|
||||
(role / "ROLE.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"apiVersion": "kaizen.agentic/v1",
|
||||
"kind": "Role",
|
||||
"metadata": {"id": "host-operator", "version": "0.1.0"},
|
||||
"spec": {
|
||||
"protocols": [
|
||||
{
|
||||
"agent": "host-operator",
|
||||
"slug": "load-workload-review",
|
||||
"path": "roles/host-operator/protocols/load-workload-review.md",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(role / "agent-definition.md").write_text(
|
||||
"---\nname: host-operator\ncategory: infrastructure\nmemory: enabled\n---\n\n# Host Operator\n\nDo ops.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(role / "memory-template.md").write_text(
|
||||
"---\nagent: host-operator\nengagement_id: <set on init>\n---\n\n# Memory\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(role / "ramp-up.md").write_text(
|
||||
"# Ramp-up\n\n| ID | Criterion | Status | Evidence |\n"
|
||||
"|----|-----------|--------|----------|\n"
|
||||
"| RU-01 | Access path verified | todo | access-plan.md |\n"
|
||||
"| RU-02 | Host baseline documented | todo | vault/baselines |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(role / "ramp-down.md").write_text(
|
||||
"# Ramp-down\n\n| ID | Criterion | Status | Evidence |\n"
|
||||
"|----|-----------|--------|----------|\n"
|
||||
"| RD-01 | Open threads triaged | todo | memory |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
proto = role / "protocols"
|
||||
proto.mkdir()
|
||||
(proto / "load-workload-review.md").write_text(
|
||||
"---\nslug: load-workload-review\n---\n\n# Load review\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "engagements" / "pilots").mkdir(parents=True)
|
||||
return root
|
||||
|
||||
|
||||
class TestChecklistParse:
|
||||
def test_parse_checklist_rows(self):
|
||||
text = (
|
||||
"| ID | Criterion | Status | Evidence |\n"
|
||||
"|----|-----------|--------|----------|\n"
|
||||
"| RU-01 | Access | done | access-plan.md |\n"
|
||||
"| RU-02 | Baseline | todo | vault |\n"
|
||||
)
|
||||
items = parse_checklist_markdown(text)
|
||||
assert len(items) == 2
|
||||
assert items[0].done is True
|
||||
assert items[1].done is False
|
||||
summary = checklist_summary(items)
|
||||
assert summary == {
|
||||
"done": 1,
|
||||
"total": 2,
|
||||
"complete": False,
|
||||
"pending": ["RU-02"],
|
||||
}
|
||||
|
||||
def test_set_checklist_item_status(self, tmp_path: Path):
|
||||
p = tmp_path / "c.md"
|
||||
p.write_text(
|
||||
"| ID | Criterion | Status | Evidence |\n"
|
||||
"|----|-----------|--------|----------|\n"
|
||||
"| RU-01 | Access | todo | a |\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert set_checklist_item_status(p, "RU-01", "done")
|
||||
assert "RU-01" in p.read_text()
|
||||
assert "done" in p.read_text()
|
||||
items = parse_checklist_markdown(p.read_text())
|
||||
assert items[0].done
|
||||
|
||||
|
||||
class TestStaffAndLifecycle:
|
||||
def test_staff_validate_phase_prepare(self, mini_repo: Path):
|
||||
eng = staff_engagement(
|
||||
engagement_id="eng-test-host-001",
|
||||
role_id="host-operator",
|
||||
client_id="coulomb",
|
||||
target_id="railiance01",
|
||||
repo_root=mini_repo,
|
||||
)
|
||||
assert eng.phase == "staffing"
|
||||
assert eng.engagement_id == "eng-test-host-001"
|
||||
assert (eng.path / "vault" / "memory.md").exists()
|
||||
assert (eng.path / "agent-host-operator.md").exists()
|
||||
|
||||
errs = validate_engagement(eng)
|
||||
assert errs == []
|
||||
|
||||
path, items = load_checklist(eng, "ramp_up")
|
||||
assert path is not None
|
||||
assert any(i.item_id == "RU-01" for i in items)
|
||||
|
||||
eng = set_phase(eng, "ramp_up")
|
||||
assert eng.phase == "ramp_up"
|
||||
ad = (eng.path / "agent-host-operator.md").read_text(encoding="utf-8")
|
||||
assert "phase: ramp_up" in ad or "phase: ramp_up" in ad.replace('"', "")
|
||||
|
||||
bundle = build_prepare_bundle(eng, mini_repo)
|
||||
assert bundle["engagement_id"] == "eng-test-host-001"
|
||||
assert bundle["agent_prompt_found"] is True
|
||||
assert bundle["phase"] == "ramp_up"
|
||||
assert "Access" in (bundle.get("phase_instructions") or "")
|
||||
|
||||
with pytest.raises(EngagementError):
|
||||
set_phase(eng, "closed") # illegal without force
|
||||
|
||||
eng = set_phase(eng, "closed", force=True)
|
||||
assert eng.phase == "closed"
|
||||
|
||||
|
||||
class TestEngagementCli:
|
||||
def test_staff_list_checklist_prepare(self, runner: CliRunner, mini_repo: Path):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"staff",
|
||||
"--id",
|
||||
"eng-cli-001",
|
||||
"--role",
|
||||
"host-operator",
|
||||
"--client",
|
||||
"coulomb",
|
||||
"--target",
|
||||
"railiance01",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Staffed engagement" in result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli, ["engagement", "list", "--repo-root", str(mini_repo), "--json"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
rows = json.loads(result.output)
|
||||
assert any(r["id"] == "eng-cli-001" for r in rows)
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"validate",
|
||||
"eng-cli-001",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"checklist",
|
||||
"eng-cli-001",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
"--mark",
|
||||
"RU-01=done",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "RU-01" in result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"checklist",
|
||||
"eng-cli-001",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
"--json",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
data = json.loads(result.output)
|
||||
ru = data["checklists"]["ramp_up"]["items"]
|
||||
assert any(i["id"] == "RU-01" and i["done"] for i in ru)
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"phase",
|
||||
"eng-cli-001",
|
||||
"--to",
|
||||
"ramp_up",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "ramp_up" in result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"prepare",
|
||||
"eng-cli-001",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
bundle = json.loads(result.output)
|
||||
assert bundle["phase"] == "ramp_up"
|
||||
assert bundle["agent_prompt_found"] is True
|
||||
|
||||
def test_phase_rejects_illegal_transition(self, runner: CliRunner, mini_repo: Path):
|
||||
runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"staff",
|
||||
"--id",
|
||||
"eng-cli-002",
|
||||
"--role",
|
||||
"host-operator",
|
||||
"--client",
|
||||
"c",
|
||||
"--target",
|
||||
"h1",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
],
|
||||
)
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"phase",
|
||||
"eng-cli-002",
|
||||
"--to",
|
||||
"closed",
|
||||
"--repo-root",
|
||||
str(mini_repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "Illegal phase" in result.output
|
||||
|
||||
def test_real_pilot_if_present(self, runner: CliRunner):
|
||||
"""Smoke against repo pilot when checked out as kaizen-agentic."""
|
||||
repo = Path.cwd()
|
||||
pilot = (
|
||||
repo
|
||||
/ "engagements"
|
||||
/ "pilots"
|
||||
/ "eng-coulomb-railiance01-ho-001"
|
||||
/ "ENGAGEMENT.yaml"
|
||||
)
|
||||
if not pilot.exists():
|
||||
pytest.skip("pilot engagement not in cwd")
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"show",
|
||||
"eng-coulomb-railiance01-ho-001",
|
||||
"--repo-root",
|
||||
str(repo),
|
||||
"--json",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
data = json.loads(result.output)
|
||||
assert data["id"] == "eng-coulomb-railiance01-ho-001"
|
||||
assert data["role"] == "host-operator"
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"checklist",
|
||||
"eng-coulomb-railiance01-ho-001",
|
||||
"--repo-root",
|
||||
str(repo),
|
||||
"--which",
|
||||
"ramp_up",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "RU-01" in result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"validate",
|
||||
"eng-coulomb-railiance01-ho-001",
|
||||
"--repo-root",
|
||||
str(repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"engagement",
|
||||
"quote",
|
||||
"eng-coulomb-railiance01-ho-001",
|
||||
"--repo-root",
|
||||
str(repo),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "72800" in result.output or "Kai" in result.output
|
||||
Loading…
Add table
Add a link
Reference in a new issue