167 lines
5.8 KiB
Python
167 lines
5.8 KiB
Python
|
|
"""CLI + module tests for scheduled agent execution (ADR-005, WP-0006)."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from click.testing import CliRunner
|
||
|
|
|
||
|
|
from kaizen_agentic.cli import cli
|
||
|
|
from kaizen_agentic.schedule import (
|
||
|
|
ScheduleError,
|
||
|
|
parse_schedule,
|
||
|
|
schedule_path,
|
||
|
|
validate_schedule,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def runner() -> CliRunner:
|
||
|
|
return CliRunner()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def project_dir(tmp_path: Path) -> Path:
|
||
|
|
root = tmp_path / "demo-project"
|
||
|
|
root.mkdir()
|
||
|
|
return root
|
||
|
|
|
||
|
|
|
||
|
|
class TestScheduleModule:
|
||
|
|
def test_parse_requires_version(self):
|
||
|
|
with pytest.raises(ScheduleError):
|
||
|
|
parse_schedule({"agents": {}})
|
||
|
|
|
||
|
|
def test_parse_rejects_non_mapping(self):
|
||
|
|
with pytest.raises(ScheduleError):
|
||
|
|
parse_schedule(["not", "a", "mapping"])
|
||
|
|
|
||
|
|
def test_validate_flags_unknown_agent_and_bad_cadence(self):
|
||
|
|
schedule = parse_schedule(
|
||
|
|
{
|
||
|
|
"version": "1",
|
||
|
|
"agents": {
|
||
|
|
"coach": {"cadence": "weekly", "enabled": True},
|
||
|
|
"made-up": {"cadence": "hourly"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
errors = validate_schedule(schedule, known_agents=["coach", "optimization"])
|
||
|
|
assert any("hourly" in e for e in errors)
|
||
|
|
assert any("made-up" in e for e in errors)
|
||
|
|
|
||
|
|
def test_validate_clean_schedule(self):
|
||
|
|
schedule = parse_schedule(
|
||
|
|
{"version": "1", "agents": {"coach": {"cadence": "weekly"}}}
|
||
|
|
)
|
||
|
|
assert validate_schedule(schedule, known_agents=["coach"]) == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestScheduleCli:
|
||
|
|
def test_init_creates_default_schedule(self, runner: CliRunner, project_dir: Path):
|
||
|
|
result = runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
path = schedule_path(project_dir)
|
||
|
|
assert path.exists()
|
||
|
|
assert "coach" in path.read_text()
|
||
|
|
|
||
|
|
def test_init_no_overwrite_without_force(
|
||
|
|
self, runner: CliRunner, project_dir: Path
|
||
|
|
):
|
||
|
|
runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
result = runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "already exists" in result.output
|
||
|
|
|
||
|
|
def test_validate_passes_on_default(self, runner: CliRunner, project_dir: Path):
|
||
|
|
runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "validate", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "valid" in result.output
|
||
|
|
|
||
|
|
def test_validate_missing_file_errors(self, runner: CliRunner, project_dir: Path):
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "validate", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
def test_validate_rejects_bad_schema(self, runner: CliRunner, project_dir: Path):
|
||
|
|
path = schedule_path(project_dir)
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
path.write_text("version: '1'\nagents:\n not-an-agent:\n cadence: weekly\n")
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "validate", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert result.exit_code == 1
|
||
|
|
assert "not-an-agent" in result.output
|
||
|
|
|
||
|
|
def test_list_shows_enabled(self, runner: CliRunner, project_dir: Path):
|
||
|
|
runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
result = runner.invoke(cli, ["schedule", "list", "--target", str(project_dir)])
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "coach" in result.output
|
||
|
|
# tdd-workflow is disabled by default; hidden without --all
|
||
|
|
assert "tdd-workflow" not in result.output
|
||
|
|
|
||
|
|
def test_list_all_shows_disabled(self, runner: CliRunner, project_dir: Path):
|
||
|
|
runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)])
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "list", "--all", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert "tdd-workflow" in result.output
|
||
|
|
|
||
|
|
def test_prepare_markdown_includes_agent_prompt(
|
||
|
|
self, runner: CliRunner, project_dir: Path
|
||
|
|
):
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "prepare", "coach", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "Scheduled Run Orientation: coach" in result.output
|
||
|
|
assert "## Agent Prompt" in result.output
|
||
|
|
assert "Coach Agent" in result.output
|
||
|
|
assert "## Session Close" in result.output
|
||
|
|
|
||
|
|
def test_prepare_json_format(self, runner: CliRunner, project_dir: Path):
|
||
|
|
result = runner.invoke(
|
||
|
|
cli,
|
||
|
|
[
|
||
|
|
"schedule",
|
||
|
|
"prepare",
|
||
|
|
"coach",
|
||
|
|
"--target",
|
||
|
|
str(project_dir),
|
||
|
|
"--format",
|
||
|
|
"json",
|
||
|
|
],
|
||
|
|
)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
payload = json.loads(result.output)
|
||
|
|
assert payload["agent"] == "coach"
|
||
|
|
assert payload["agent_prompt_found"] is True
|
||
|
|
assert payload["session_close"]
|
||
|
|
|
||
|
|
def test_prepare_unknown_agent_notes_missing(
|
||
|
|
self, runner: CliRunner, project_dir: Path
|
||
|
|
):
|
||
|
|
result = runner.invoke(
|
||
|
|
cli,
|
||
|
|
["schedule", "prepare", "no-such-agent", "--target", str(project_dir)],
|
||
|
|
)
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "not found in registry" in result.output
|
||
|
|
|
||
|
|
def test_prepare_includes_memory_when_present(
|
||
|
|
self, runner: CliRunner, project_dir: Path
|
||
|
|
):
|
||
|
|
runner.invoke(cli, ["memory", "init", "coach", "--target", str(project_dir)])
|
||
|
|
result = runner.invoke(
|
||
|
|
cli, ["schedule", "prepare", "coach", "--target", str(project_dir)]
|
||
|
|
)
|
||
|
|
assert "## Project Memory" in result.output
|
||
|
|
assert "Project Context" in result.output
|