Enable kaizen agents to run on a regular cadence against a preselected repo roster, orchestrated by activity-core and prepared by kaizen-agentic — without this repo owning cron, Temporal workers, or an LLM runtime. CLI + module: - src/kaizen_agentic/schedule.py — .kaizen/schedule.yml parse/validate/scaffold - `kaizen-agentic schedule` group: init, validate, list, prepare <agent> (prepare bundles agent prompt + memory + metrics + repo pointers, offline) - tests/test_schedule_cli.py — 15 tests Contract & design: - ADR-005 scheduled agent execution; schema doc + example manifest - discover_kaizen_scheduled_repos resolver spec, state-hub roster fields, kaizen.schedule.prepared event payload, activity-core handoff checklist - INTEGRATION_PATTERNS Pattern 2 extended with roster model ActivityDefinition drafts (enabled: false): - weekly-coach-orientation, weekly-optimization-review Docs: agency-framework, CLI cheat sheet, PACKAGE_RELEASE runner prereqs, EcosystemIntegration, CHANGELOG, TODO. Workplan closed (status: done). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
|