From 4f1dfbf10fafa2b4a4e52dfcca4dfdb669a39a3e Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 21:30:57 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_kin_111_regression.py | 135 +++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/tests/test_kin_111_regression.py b/tests/test_kin_111_regression.py index 2b1ba4d..03f3684 100644 --- a/tests/test_kin_111_regression.py +++ b/tests/test_kin_111_regression.py @@ -285,3 +285,138 @@ class TestApiFollowupEmptyArrayNeedsDecision: ) assert data["created"] == [] assert data["pending_actions"] == [] + + +# --------------------------------------------------------------------------- +# Additional edge cases — deeper investigation +# --------------------------------------------------------------------------- + +class TestRunPipelineNoneSteps: + """run_pipeline with steps=None — must also return empty_pipeline, not crash.""" + + @patch("agents.runner.check_claude_auth") + def test_none_steps_returns_empty_pipeline_error(self, mock_auth, conn): + """run_pipeline(steps=None) must return {success: False, error: 'empty_pipeline'}.""" + from agents.runner import run_pipeline + result = run_pipeline(conn, "PROJ-001", None) + assert result["success"] is False + assert result.get("error") == "empty_pipeline", ( + f"Expected error='empty_pipeline' for None steps, got: {result}" + ) + + @patch("agents.runner.check_claude_auth") + def test_none_steps_does_not_mutate_task(self, mock_auth, conn): + """run_pipeline(steps=None) must not change task status.""" + from agents.runner import run_pipeline + before = models.get_task(conn, "PROJ-001")["status"] + run_pipeline(conn, "PROJ-001", None) + after = models.get_task(conn, "PROJ-001")["status"] + assert after == before, ( + f"Task status changed from '{before}' to '{after}' after None steps" + ) + + +class TestRunPipelineEmptyStepsDryRun: + """run_pipeline(steps=[], dry_run=True) — must bail before auth check.""" + + def test_empty_steps_dry_run_returns_error_without_auth(self, conn): + """run_pipeline(steps=[], dry_run=True) must return early without auth check.""" + from agents.runner import run_pipeline + # No @patch for check_claude_auth — if auth is called, it may raise; empty guard must fire first + result = run_pipeline(conn, "PROJ-001", [], dry_run=True) + assert result["success"] is False + assert result.get("error") == "empty_pipeline" + + +class TestCliRunTaskNonListPipeline: + """PM returns pipeline as a non-list non-null value (dict or string) — CLI must exit(1).""" + + @patch("agents.runner.run_pipeline") + @patch("agents.runner.run_agent") + def test_dict_pipeline_exits_1(self, mock_run_agent, mock_run_pipeline, tmp_path): + """PM returning pipeline={} (dict) → CLI exits 1.""" + from cli.main import cli as kin_cli + db_path = _seed_db(tmp_path) + mock_run_agent.return_value = { + "success": True, + "output": json.dumps({"pipeline": {"steps": []}, "analysis": "..."}), + } + runner = CliRunner() + result = runner.invoke(kin_cli, ["--db", db_path, "run", "PROJ-001"]) + assert result.exit_code == 1, ( + f"Expected exit_code=1 for dict pipeline, got {result.exit_code}" + ) + mock_run_pipeline.assert_not_called() + + @patch("agents.runner.run_pipeline") + @patch("agents.runner.run_agent") + def test_string_pipeline_exits_1(self, mock_run_agent, mock_run_pipeline, tmp_path): + """PM returning pipeline='[]' (JSON-string-encoded) → CLI exits 1.""" + from cli.main import cli as kin_cli + db_path = _seed_db(tmp_path) + mock_run_agent.return_value = { + "success": True, + "output": json.dumps({"pipeline": "[]", "analysis": "..."}), + } + runner = CliRunner() + result = runner.invoke(kin_cli, ["--db", db_path, "run", "PROJ-001"]) + assert result.exit_code == 1, ( + f"Expected exit_code=1 for string pipeline, got {result.exit_code}" + ) + mock_run_pipeline.assert_not_called() + + +class TestGenerateFollowupsNullAndDict: + """Additional generate_followups edge cases: null output, dict with empty tasks.""" + + @patch("agents.runner._run_claude") + def test_agent_returns_null_gives_empty_result(self, mock_claude, conn): + """generate_followups: agent returning 'null' → {created: [], pending_actions: []}.""" + from core.followup import generate_followups + mock_claude.return_value = {"output": "null", "returncode": 0} + result = generate_followups(conn, "PROJ-001") + assert result["created"] == [], f"Expected created=[], got: {result['created']}" + assert result["pending_actions"] == [] + + @patch("agents.runner._run_claude") + def test_agent_returns_null_creates_no_tasks(self, mock_claude, conn): + """generate_followups: agent returning 'null' must not create any tasks.""" + from core.followup import generate_followups + mock_claude.return_value = {"output": "null", "returncode": 0} + tasks_before = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] + generate_followups(conn, "PROJ-001") + tasks_after = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] + assert tasks_after == tasks_before + + @patch("agents.runner._run_claude") + def test_agent_returns_dict_with_empty_tasks_list(self, mock_claude, conn): + """generate_followups: agent returning {"tasks": []} → empty result, no tasks created.""" + from core.followup import generate_followups + mock_claude.return_value = {"output": '{"tasks": []}', "returncode": 0} + tasks_before = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] + result = generate_followups(conn, "PROJ-001") + tasks_after = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] + assert result["created"] == [] + assert tasks_after == tasks_before + + @patch("agents.runner._run_claude") + def test_agent_returns_empty_string_gives_empty_result(self, mock_claude, conn): + """generate_followups: agent returning '' (empty string) → {created: [], pending_actions: []}.""" + from core.followup import generate_followups + mock_claude.return_value = {"output": "", "returncode": 0} + result = generate_followups(conn, "PROJ-001") + assert result["created"] == [], ( + f"Expected created=[] for empty string output, got: {result['created']}" + ) + assert result["pending_actions"] == [] + + @patch("agents.runner._run_claude") + def test_agent_returns_whitespace_wrapped_empty_array(self, mock_claude, conn): + """generate_followups: agent returning ' [] ' (whitespace-wrapped) → no tasks created.""" + from core.followup import generate_followups + mock_claude.return_value = {"output": " [] ", "returncode": 0} + result = generate_followups(conn, "PROJ-001") + assert result["created"] == [], ( + f"Expected created=[] for whitespace-wrapped '[]', got: {result['created']}" + ) + assert result["pending_actions"] == []