feat(activity): repo-scoped automation review CLI (WP-0028)
Ship the activity console script for consumer-repo morning review: list, status, runs, deliverables, inbox, checkpoint, and ack. Offline-first with git + local defs; enriches from ops API and State Hub. Multi-source trust matrix never reports did-not-run when git has the artefact. Adds target_repo filter on GET /ops/automations.
This commit is contained in:
parent
0a7fcfa4a0
commit
01ba1865ce
17 changed files with 1836 additions and 1 deletions
193
tests/test_review_cli.py
Normal file
193
tests/test_review_cli.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""Tests for activity review CLI (ACTIVITY-WP-0028)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from activity_core.review_cli.checkpoint import ack, load_checkpoint, save_checkpoint
|
||||
from activity_core.review_cli.defs import load_local_automations
|
||||
from activity_core.review_cli.main import main
|
||||
from activity_core.review_cli.merge import classify_row
|
||||
from activity_core.review_cli.repo import resolve_repo, _slug_from_remote_url
|
||||
from activity_core.review_cli.timewin import parse_since
|
||||
|
||||
|
||||
def test_slug_from_remote() -> None:
|
||||
assert (
|
||||
_slug_from_remote_url("forgejo-remote:coulomb/freedom-intelligence.git")
|
||||
== "freedom-intelligence"
|
||||
)
|
||||
assert (
|
||||
_slug_from_remote_url("https://forgejo.example/coulomb/binky-control.git")
|
||||
== "binky-control"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_repo_explicit(tmp_path: Path) -> None:
|
||||
r = resolve_repo(cwd=tmp_path, explicit="freedom-intelligence")
|
||||
assert r["slug"] == "freedom-intelligence"
|
||||
assert r["source"] == "flag"
|
||||
|
||||
|
||||
def test_parse_since_today() -> None:
|
||||
now = datetime(2026, 8, 6, 15, 0, tzinfo=timezone.utc)
|
||||
s = parse_since("today", now=now, tz_name="UTC")
|
||||
assert s is not None
|
||||
assert s.day == 6
|
||||
assert s.hour == 0
|
||||
|
||||
|
||||
def test_parse_since_checkpoint() -> None:
|
||||
cp = {"reviewed_at": "2026-08-05T14:00:00+00:00"}
|
||||
s = parse_since("checkpoint", checkpoint=cp)
|
||||
assert s is not None
|
||||
assert s.day == 5
|
||||
|
||||
|
||||
def test_classify_trust_matrix() -> None:
|
||||
assert classify_row(git_present=True, hub_present=True) == "ok"
|
||||
assert classify_row(git_present=True, hub_present=False) == "partial"
|
||||
assert classify_row(git_present=False, hub_present=True) == "lag"
|
||||
assert classify_row(git_present=False, hub_present=False) == "missing"
|
||||
assert classify_row(git_present=None, hub_present=None, ops_failed=True) == "failed"
|
||||
|
||||
|
||||
def test_checkpoint_ack_roundtrip(tmp_path: Path) -> None:
|
||||
save_checkpoint("freedom-intelligence", {"reviewed_paths": []}, tmp_path)
|
||||
ack(
|
||||
"freedom-intelligence",
|
||||
paths=["briefs/2026/08/2026-08-06.md"],
|
||||
state_dir=tmp_path,
|
||||
)
|
||||
cp = load_checkpoint("freedom-intelligence", tmp_path)
|
||||
assert "briefs/2026/08/2026-08-06.md" in cp["reviewed_paths"]
|
||||
assert cp["reviewed_at"]
|
||||
|
||||
|
||||
def test_load_local_fi_definition(tmp_path: Path) -> None:
|
||||
defs = tmp_path / "activity-definitions"
|
||||
defs.mkdir()
|
||||
(defs / "fi-daily-research-brief.md").write_text(
|
||||
"""---
|
||||
id: fi-daily-research-brief
|
||||
name: Freedom Intelligence Daily Research Brief
|
||||
enabled: true
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "30 7 * * 1-5"
|
||||
timezone: Europe/Berlin
|
||||
---
|
||||
|
||||
```rule
|
||||
id: emit
|
||||
action:
|
||||
target_repo: freedom-intelligence
|
||||
labels: ["freedom-intelligence"]
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
items, warnings = load_local_automations(tmp_path, "freedom-intelligence")
|
||||
assert not warnings
|
||||
assert len(items) == 1
|
||||
assert items[0]["id"] == "fi-daily-research-brief"
|
||||
assert "briefs/**/*.md" in items[0]["review"]["deliverable_globs"]
|
||||
|
||||
|
||||
def test_cli_list_json(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
defs = tmp_path / "activity-definitions"
|
||||
defs.mkdir()
|
||||
(defs / "fi-daily-research-brief.md").write_text(
|
||||
"""---
|
||||
id: fi-daily-research-brief
|
||||
name: Freedom Intelligence Daily Research Brief
|
||||
enabled: true
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "30 7 * * 1-5"
|
||||
timezone: Europe/Berlin
|
||||
---
|
||||
|
||||
```rule
|
||||
id: emit
|
||||
action:
|
||||
target_repo: freedom-intelligence
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
code = main(
|
||||
[
|
||||
"--cwd",
|
||||
str(tmp_path),
|
||||
"--repo",
|
||||
"freedom-intelligence",
|
||||
"--format",
|
||||
"json",
|
||||
"list",
|
||||
]
|
||||
)
|
||||
assert code in (0, 2)
|
||||
out = capsys.readouterr().out
|
||||
data = json.loads(out)
|
||||
assert data["repo"] == "freedom-intelligence"
|
||||
assert data["count"] == 1
|
||||
|
||||
|
||||
def test_cli_status_offline(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
defs = tmp_path / "activity-definitions"
|
||||
defs.mkdir()
|
||||
briefs = tmp_path / "briefs" / "2026" / "08"
|
||||
briefs.mkdir(parents=True)
|
||||
(briefs / "2026-08-06.md").write_text("# brief\n", encoding="utf-8")
|
||||
(defs / "fi-daily-research-brief.md").write_text(
|
||||
"""---
|
||||
id: fi-daily-research-brief
|
||||
name: Freedom Intelligence Daily Research Brief
|
||||
enabled: true
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "30 7 * * 1-5"
|
||||
timezone: Europe/Berlin
|
||||
---
|
||||
|
||||
```rule
|
||||
id: emit
|
||||
action:
|
||||
target_repo: freedom-intelligence
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# init git for deliverables
|
||||
import subprocess
|
||||
|
||||
subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)
|
||||
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init"],
|
||||
cwd=tmp_path,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
code = main(
|
||||
[
|
||||
"--cwd",
|
||||
str(tmp_path),
|
||||
"--repo",
|
||||
"freedom-intelligence",
|
||||
"--state-dir",
|
||||
str(tmp_path / "state"),
|
||||
"status",
|
||||
"--since",
|
||||
"week",
|
||||
]
|
||||
)
|
||||
assert code in (0, 2)
|
||||
out = capsys.readouterr().out
|
||||
assert "freedom-intelligence" in out
|
||||
assert "Automations:" in out
|
||||
Loading…
Add table
Add a link
Reference in a new issue