WP-0001: feedback channels, CI, pre-commit, telemetry docs
Some checks failed
ci / test (3.12) (push) Has been cancelled
ci / test (3.10) (push) Has been cancelled

Add kaizen-agentic feedback CLI, Gitea issue templates, CI workflow,
pre-commit hooks, FEEDBACK/TELEMETRY docs, and cross-platform path tests.
Improve CLI registry error messages; remove agents_backup scaffolding.
Apply black formatting across src/tests for CI consistency.

State Hub message sent to agentic-resources for Helix correlation doc link.
This commit is contained in:
2026-06-16 01:58:07 +02:00
parent 79883aa25b
commit 80c60ebd7a
30 changed files with 556 additions and 110 deletions

View File

@@ -20,9 +20,12 @@ class TestClickWorkaround:
def test_install_command_error_suppression(self):
"""Test that spurious 'unexpected extra argument' errors are suppressed for install commands."""
# Test the install command that previously showed spurious errors
with patch('sys.argv', ['kaizen-agentic', 'install', 'tdd-workflow', '--target', '/tmp/test']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch(
"sys.argv",
["kaizen-agentic", "install", "tdd-workflow", "--target", "/tmp/test"],
):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
safe_cli_wrapper()
except SystemExit:
@@ -40,9 +43,9 @@ class TestClickWorkaround:
def test_update_command_error_suppression(self):
"""Test that spurious 'unexpected extra argument' errors are suppressed for update commands."""
# Test the update command that also shows spurious errors
with patch('sys.argv', ['kaizen-agentic', 'update']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch("sys.argv", ["kaizen-agentic", "update"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
safe_cli_wrapper()
except SystemExit:
@@ -59,9 +62,9 @@ class TestClickWorkaround:
def test_non_install_command_normal_operation(self):
"""Test that non-install commands work normally without interference."""
with patch('sys.argv', ['kaizen-agentic', 'list']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch("sys.argv", ["kaizen-agentic", "list"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
safe_cli_wrapper()
except SystemExit:
@@ -76,9 +79,9 @@ class TestClickWorkaround:
def test_legitimate_error_preservation(self):
"""Test that legitimate errors are still displayed for non-install commands."""
with patch('sys.argv', ['kaizen-agentic', 'invalid-command']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch("sys.argv", ["kaizen-agentic", "invalid-command"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
safe_cli_wrapper()
except SystemExit as e:
@@ -95,8 +98,8 @@ class TestClickWorkaround:
def test_help_commands_work_normally(self):
"""Test that help commands work without interference."""
with patch('sys.argv', ['kaizen-agentic', '--help']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch("sys.argv", ["kaizen-agentic", "--help"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
try:
safe_cli_wrapper()
except SystemExit as e:
@@ -104,7 +107,9 @@ class TestClickWorkaround:
assert e.code == 0
stdout_content = mock_stdout.getvalue()
assert "Kaizen Agentic - AI agent development framework" in stdout_content
assert (
"Kaizen Agentic - AI agent development framework" in stdout_content
)
assert "Commands:" in stdout_content
@@ -113,9 +118,9 @@ class TestInstallCommandSpecifics:
def test_install_with_valid_agent(self):
"""Test install command with a valid agent name."""
with patch('sys.argv', ['kaizen-agentic', 'install', 'tdd-workflow']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch("sys.argv", ["kaizen-agentic", "install", "tdd-workflow"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
safe_cli_wrapper()
except SystemExit:
@@ -127,12 +132,17 @@ class TestInstallCommandSpecifics:
# Should show clean installation output
assert "Installing agents to:" in stdout_content
# Should not show Click error
assert "Got unexpected extra argument" not in (stdout_content + stderr_content)
assert "Got unexpected extra argument" not in (
stdout_content + stderr_content
)
def test_install_with_target_option(self):
"""Test install command with target directory option."""
with patch('sys.argv', ['kaizen-agentic', 'install', 'tdd-workflow', '--target', '/tmp/test']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch(
"sys.argv",
["kaizen-agentic", "install", "tdd-workflow", "--target", "/tmp/test"],
):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
try:
safe_cli_wrapper()
except SystemExit:
@@ -144,8 +154,8 @@ class TestInstallCommandSpecifics:
def test_install_help_works(self):
"""Test that install command help works correctly."""
with patch('sys.argv', ['kaizen-agentic', 'install', '--help']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch("sys.argv", ["kaizen-agentic", "install", "--help"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
try:
safe_cli_wrapper()
except SystemExit as e:
@@ -170,12 +180,14 @@ class TestWorkaroundRemovalReadiness:
may be ready for removal.
"""
# Skip this test in normal runs since it's expected to show the spurious error
pytest.skip("This test demonstrates the underlying Click issue. "
"Enable when testing Click library updates.")
pytest.skip(
"This test demonstrates the underlying Click issue. "
"Enable when testing Click library updates."
)
with patch('sys.argv', ['kaizen-agentic', 'install', 'tdd-workflow']):
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
with patch('sys.stderr', new_callable=StringIO) as mock_stderr:
with patch("sys.argv", ["kaizen-agentic", "install", "tdd-workflow"]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try:
cli(standalone_mode=False)
except SystemExit:
@@ -201,9 +213,13 @@ class TestWorkaroundRemovalReadiness:
"""
# Test that the CLI works when invoked as a subprocess
result = subprocess.run(
['python', '-c', 'from kaizen_agentic.cli import safe_cli_wrapper; import sys; sys.argv = ["kaizen-agentic", "list"]; safe_cli_wrapper()'],
[
"python",
"-c",
'from kaizen_agentic.cli import safe_cli_wrapper; import sys; sys.argv = ["kaizen-agentic", "list"]; safe_cli_wrapper()',
],
capture_output=True,
text=True
text=True,
)
assert "Available Agents" in result.stdout
@@ -220,7 +236,7 @@ class TestErrorMessagePatterns:
spurious_patterns = [
"Got unexpected extra argument (tdd-workflow)",
"Got unexpected extra argument (some-agent)",
"Error: Got unexpected extra argument"
"Error: Got unexpected extra argument",
]
for pattern in spurious_patterns:
@@ -234,7 +250,7 @@ class TestErrorMessagePatterns:
"Error: No such file or directory",
"Error: Permission denied",
"Error: Invalid agent name",
"Error: Configuration file not found"
"Error: Configuration file not found",
]
for pattern in legitimate_patterns:
@@ -243,4 +259,4 @@ class TestErrorMessagePatterns:
if __name__ == "__main__":
pytest.main([__file__])
pytest.main([__file__])

View File

@@ -22,11 +22,11 @@ from kaizen_agentic.cli import cli
from kaizen_agentic.metrics import MetricsStore, OptimizerStore
from kaizen_agentic.optimization import MIN_SAMPLES_FOR_RECOMMENDATIONS
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _sys_medic_memory() -> str:
"""Realistic sys-medic memory after two simulated sessions."""
return textwrap.dedent("""\
@@ -124,6 +124,7 @@ def _project_management_memory() -> str:
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def project(tmp_path):
"""A temporary 'project' directory with a name."""
@@ -136,10 +137,13 @@ def project(tmp_path):
# Tests
# ---------------------------------------------------------------------------
class TestMemoryInit:
def test_init_creates_file(self, project):
runner = CliRunner()
result = runner.invoke(cli, ["memory", "init", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "init", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0, result.output
assert "Initialized memory" in result.output
@@ -166,7 +170,9 @@ class TestMemoryInit:
def test_init_idempotent(self, project):
runner = CliRunner()
runner.invoke(cli, ["memory", "init", "sys-medic", "--target", str(project)])
result = runner.invoke(cli, ["memory", "init", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "init", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "already exists" in result.output
@@ -178,14 +184,18 @@ class TestMemoryShow:
memory_file.write_text(_sys_medic_memory())
runner = CliRunner()
result = runner.invoke(cli, ["memory", "show", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "show", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "Node Profiles" in result.output
assert "tegpi-01" in result.output
def test_show_missing_prints_guidance(self, project):
runner = CliRunner()
result = runner.invoke(cli, ["memory", "show", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "show", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "No memory found" in result.output
assert "memory init" in result.output
@@ -205,7 +215,9 @@ class TestMemoryBrief:
def test_brief_includes_own_memory(self, project):
self._populate(project)
runner = CliRunner()
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "Orientation Brief for: sys-medic" in result.output
assert "Your Memory" in result.output
@@ -214,7 +226,9 @@ class TestMemoryBrief:
def test_brief_includes_cross_agent_context(self, project):
self._populate(project)
runner = CliRunner()
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "Context From Other Agents" in result.output
assert "project-management" in result.output
@@ -222,20 +236,26 @@ class TestMemoryBrief:
def test_brief_coach_tip_present(self, project):
self._populate(project)
runner = CliRunner()
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "agent-coach" in result.output
def test_brief_no_memory_gives_guidance(self, project):
runner = CliRunner()
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "No agent memory files found" in result.output
def test_brief_raw_flag_skips_header(self, project):
self._populate(project)
runner = CliRunner()
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project), "--raw"])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project), "--raw"]
)
assert result.exit_code == 0
assert "=== sys-medic ===" in result.output
# Raw mode should not include the orientation header
@@ -275,7 +295,9 @@ class TestMemoryBrief:
],
)
result = runner.invoke(cli, ["memory", "brief", "sys-medic", "--target", str(project)])
result = runner.invoke(
cli, ["memory", "brief", "sys-medic", "--target", str(project)]
)
assert result.exit_code == 0
assert "## Performance Summary" in result.output
@@ -367,7 +389,10 @@ class TestTddWorkflowMetricsPilot:
["metrics", "show", "tdd-workflow", "--target", str(project)],
)
assert show_result.exit_code == 0
assert "test_pass_rate" in show_result.output or "2 execution" in show_result.output.lower()
assert (
"test_pass_rate" in show_result.output
or "2 execution" in show_result.output.lower()
)
store = MetricsStore(project, "tdd-workflow")
for i in range(MIN_SAMPLES_FOR_RECOMMENDATIONS - len(sessions)):
@@ -422,7 +447,9 @@ class TestProtocolsCommand:
def test_protocols_show_outputs_content(self):
runner = CliRunner()
result = runner.invoke(cli, ["protocols", "show", "sys-medic", "k3s-node-health-assessment"])
result = runner.invoke(
cli, ["protocols", "show", "sys-medic", "k3s-node-health-assessment"]
)
assert result.exit_code == 0
# Protocol should contain key structural sections
assert "k3s" in result.output.lower()

View File

@@ -0,0 +1,27 @@
"""Tests for developer feedback CLI (WP-0001 T01)."""
from __future__ import annotations
import json
from click.testing import CliRunner
from kaizen_agentic.cli import cli
def test_feedback_human_output():
runner = CliRunner()
result = runner.invoke(cli, ["feedback"])
assert result.exit_code == 0
assert "feedback channels" in result.output.lower()
assert "gitea.coulomb.social" in result.output
assert "bug report" in result.output.lower()
def test_feedback_json_output():
runner = CliRunner()
result = runner.invoke(cli, ["feedback", "--json"])
assert result.exit_code == 0
payload = json.loads(result.output)
assert "channels" in payload
assert "bug_report" in payload["templates"]

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import json
import os
import sqlite3
from pathlib import Path
@@ -158,4 +157,4 @@ class TestHelixCorrelationCli:
["memory", "brief", "tdd-workflow", "--target", str(tmp_path)],
)
assert brief.exit_code == 0
assert "## Performance Summary" in brief.output
assert "## Performance Summary" in brief.output

View File

@@ -6,7 +6,6 @@ from pathlib import Path
import yaml
DEFINITIONS_DIR = (
Path(__file__).parent.parent / "docs" / "integrations" / "activity-definitions"
)
@@ -30,4 +29,4 @@ def test_integration_docs_exist():
root = Path(__file__).parent.parent / "docs"
assert (root / "INTEGRATION_PATTERNS.md").exists()
assert (root / "integrations" / "helix-forge-correlation.md").exists()
assert (root / "integrations" / "optimizer-artifact-manifest.md").exists()
assert (root / "integrations" / "optimizer-artifact-manifest.md").exists()

View File

@@ -104,4 +104,4 @@ class TestMetricsStore:
assert agents == ["coach", "tdd-workflow"]
def test_default_retention_matches_adr(self):
assert DEFAULT_RETENTION_DAYS == 180
assert DEFAULT_RETENTION_DAYS == 180

View File

@@ -45,7 +45,9 @@ class TestMetricsCli:
assert record.exit_code == 0
assert "Recorded metrics" in record.output
show = runner.invoke(cli, ["metrics", "show", "tdd-workflow", "--target", target])
show = runner.invoke(
cli, ["metrics", "show", "tdd-workflow", "--target", target]
)
assert show.exit_code == 0
assert '"execution_count": 1' in show.output
assert '"success": true' in show.output
@@ -54,7 +56,9 @@ class TestMetricsCli:
assert listed.exit_code == 0
assert "tdd-workflow" in listed.output
export = runner.invoke(cli, ["metrics", "export", "tdd-workflow", "--target", target])
export = runner.invoke(
cli, ["metrics", "export", "tdd-workflow", "--target", target]
)
assert export.exit_code == 0
lines = [line for line in export.output.splitlines() if line.strip()]
assert len(lines) == 1
@@ -69,7 +73,9 @@ class TestMetricsCli:
)
assert result.exit_code == 0
show = runner.invoke(cli, ["metrics", "show", "coach", "--target", str(project_dir)])
show = runner.invoke(
cli, ["metrics", "show", "coach", "--target", str(project_dir)]
)
assert '"success": false' in show.output
def test_record_idempotency_key_skips_duplicate(
@@ -96,7 +102,9 @@ class TestMetricsCli:
)
assert len(export.output.strip().splitlines()) == 1
def test_record_requires_outcome_without_json(self, runner: CliRunner, project_dir: Path):
def test_record_requires_outcome_without_json(
self, runner: CliRunner, project_dir: Path
):
result = runner.invoke(
cli,
["metrics", "record", "tdd-workflow", "--target", str(project_dir)],
@@ -133,7 +141,9 @@ class TestMetricsCli:
],
)
result = runner.invoke(cli, ["memory", "brief", "tdd-workflow", "--target", target])
result = runner.invoke(
cli, ["memory", "brief", "tdd-workflow", "--target", target]
)
assert result.exit_code == 0
assert "## Performance Summary" in result.output
assert "Success rate: 100.0%" in result.output
@@ -144,4 +154,4 @@ class TestMetricsCli:
["memory", "init", "coach", "--target", str(project_dir), "--no-metrics"],
)
assert result.exit_code == 0
assert not (project_dir / ".kaizen" / "metrics" / "coach").exists()
assert not (project_dir / ".kaizen" / "metrics" / "coach").exists()

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import patch
@@ -140,4 +139,4 @@ def test_publish_against_live_artifact_store(project_with_optimizer: Path):
token=token,
)
assert result.package_id
assert result.files_uploaded >= 1
assert result.files_uploaded >= 1

View File

@@ -85,7 +85,9 @@ class TestOptimizationFromMetricsStore:
store = MetricsStore(project_dir, "coach")
_seed_executions(store, 4)
report = OptimizationLoop.from_metrics_store(store).get_optimization_report_json()
report = OptimizationLoop.from_metrics_store(
store
).get_optimization_report_json()
json.dumps(report)
@@ -130,4 +132,7 @@ class TestMetricsOptimizeCli:
optimizer = OptimizerStore(project_dir)
assert optimizer.analysis_path.exists()
assert optimizer.recommendations_path.exists()
assert '"type": "reliability"' in result.output or '"type": "quality"' in result.output
assert (
'"type": "reliability"' in result.output
or '"type": "quality"' in result.output
)

19
tests/test_path_compat.py Normal file
View File

@@ -0,0 +1,19 @@
"""Cross-platform path handling smoke tests (WP-0001 T07)."""
from __future__ import annotations
from pathlib import Path, PureWindowsPath
from kaizen_agentic.metrics import MetricsStore
def test_metrics_store_accepts_string_project_root(tmp_path: Path):
store = MetricsStore(str(tmp_path), "coach")
store.append({"success": True}, idempotency_key="win-path-test")
assert store.executions_path.exists()
def test_metrics_paths_use_forward_join_semantics(tmp_path: Path):
store = MetricsStore(tmp_path, "tdd-workflow")
suffix = PureWindowsPath(".kaizen/metrics/tdd-workflow/executions.jsonl")
assert store.executions_path.as_posix().endswith(suffix.as_posix())