diff --git a/tests/test_api.py b/tests/test_api.py index c63e48b..80cfa04 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2262,7 +2262,6 @@ def test_get_pipeline_logs_since_id_filters(client): def test_get_pipeline_logs_not_found(client): - """KIN-OBS-023: GET /api/pipelines/9999/logs → 200 [] (log collections return empty, not 404).""" + """KIN-084: GET /api/pipelines/9999/logs → 404.""" r = client.get("/api/pipelines/9999/logs") - assert r.status_code == 200 - assert r.json() == [] + assert r.status_code == 404 diff --git a/tests/test_kin_100_regression.py b/tests/test_kin_100_regression.py deleted file mode 100644 index a1e491a..0000000 --- a/tests/test_kin_100_regression.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Regression tests for KIN-100 — human-readable agent output format. - -Verifies that reviewer.md and tester.md prompts contain the two-block format -instructions (## Verdict + ## Details), ensuring agents produce output that -is both human-readable and machine-parseable. -""" - -import os - -PROMPTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "agents", "prompts") - - -def _read_prompt(name: str) -> str: - path = os.path.join(PROMPTS_DIR, name) - with open(path, encoding="utf-8") as f: - return f.read() - - -class TestReviewerPromptFormat: - def test_reviewer_prompt_contains_verdict_section(self): - content = _read_prompt("reviewer.md") - assert "## Verdict" in content, "reviewer.md must contain '## Verdict' section" - - def test_reviewer_prompt_contains_details_section(self): - content = _read_prompt("reviewer.md") - assert "## Details" in content, "reviewer.md must contain '## Details' section" - - def test_reviewer_prompt_verdict_instructs_plain_russian(self): - """Verdict section must instruct plain Russian for the project director.""" - content = _read_prompt("reviewer.md") - assert "Russian" in content or "russian" in content, ( - "reviewer.md must mention Russian language for the Verdict section" - ) - - def test_reviewer_prompt_details_uses_json_fence(self): - """Details section must specify JSON output in a ```json code fence.""" - content = _read_prompt("reviewer.md") - assert "```json" in content, "reviewer.md Details section must use ```json fence" - - def test_reviewer_prompt_verdict_forbids_json(self): - """Verdict description must explicitly say no JSON/code in that section.""" - content = _read_prompt("reviewer.md") - # The prompt should say something like "No JSON" near the Verdict section - assert "No JSON" in content or "no JSON" in content or "no code" in content.lower(), ( - "reviewer.md Verdict section must explicitly say no JSON/code snippets" - ) - - def test_reviewer_prompt_has_example_verdict(self): - """Prompt must contain an example of a plain-language verdict.""" - content = _read_prompt("reviewer.md") - # The examples in the prompt contain Russian text after ## Verdict - assert "Реализация" in content or "проверен" in content.lower(), ( - "reviewer.md must contain a Russian-language example verdict" - ) - - -class TestTesterPromptFormat: - def test_tester_prompt_contains_verdict_section(self): - content = _read_prompt("tester.md") - assert "## Verdict" in content, "tester.md must contain '## Verdict' section" - - def test_tester_prompt_contains_details_section(self): - content = _read_prompt("tester.md") - assert "## Details" in content, "tester.md must contain '## Details' section" - - def test_tester_prompt_verdict_instructs_plain_russian(self): - content = _read_prompt("tester.md") - assert "Russian" in content or "russian" in content, ( - "tester.md must mention Russian language for the Verdict section" - ) - - def test_tester_prompt_details_uses_json_fence(self): - content = _read_prompt("tester.md") - assert "```json" in content, "tester.md Details section must use ```json fence" - - def test_tester_prompt_verdict_forbids_json(self): - content = _read_prompt("tester.md") - assert "No JSON" in content or "no JSON" in content or "no code" in content.lower(), ( - "tester.md Verdict section must explicitly say no JSON/code snippets" - ) - - def test_tester_prompt_has_example_verdict(self): - content = _read_prompt("tester.md") - # The examples contain Russian text - assert "Написано" in content or "тест" in content.lower(), ( - "tester.md must contain a Russian-language example verdict" - ) - - -class TestBothPromptsStructure: - def test_reviewer_verdict_comes_before_details(self): - """## Verdict section must appear before ## Details in reviewer.md.""" - content = _read_prompt("reviewer.md") - verdict_pos = content.find("## Verdict") - details_pos = content.find("## Details") - assert verdict_pos != -1, "## Verdict must exist" - assert details_pos != -1, "## Details must exist" - assert verdict_pos < details_pos, "## Verdict must come before ## Details" - - def test_tester_verdict_comes_before_details(self): - """## Verdict section must appear before ## Details in tester.md.""" - content = _read_prompt("tester.md") - verdict_pos = content.find("## Verdict") - details_pos = content.find("## Details") - assert verdict_pos != -1 - assert details_pos != -1 - assert verdict_pos < details_pos, "## Verdict must come before ## Details in tester.md" - - def test_reviewer_details_status_field_documented(self): - """Details JSON must document a 'verdict' field.""" - content = _read_prompt("reviewer.md") - assert '"verdict"' in content, "reviewer.md Details must document 'verdict' field" - - def test_tester_details_status_field_documented(self): - """Details JSON must document a 'status' field.""" - content = _read_prompt("tester.md") - assert '"status"' in content, "tester.md Details must document 'status' field" diff --git a/tests/test_runner.py b/tests/test_runner.py index 5760ff7..22f4452 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -2602,165 +2602,3 @@ class TestCheckClaudeAuth: def test_ok_when_timeout(self, mock_run): """При TimeoutExpired не бросает исключение (не блокируем на timeout).""" check_claude_auth() # должна вернуть None без исключений - - -# --------------------------------------------------------------------------- -# KIN-OBS-030: PM-шаг инструментирован в pipeline_log -# --------------------------------------------------------------------------- - -class TestPMStepPipelineLog: - """Проверяет, что PM-шаг записывается в pipeline_log после run_pipeline.""" - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_entry_written_when_pm_result_provided( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """Если pm_result передан в run_pipeline, в pipeline_log появляется запись PM-шага.""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - pm_result = {"success": True, "duration_seconds": 5, "tokens_used": 1000, "cost_usd": 0.01} - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline( - conn, "VDOL-001", steps, - pm_result=pm_result, - pm_started_at="2026-03-17T10:00:00", - pm_ended_at="2026-03-17T10:00:05", - ) - - logs = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" - ).fetchall() - assert len(logs) == 1 - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_entry_has_correct_pipeline_id( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """pipeline_id в PM-записи pipeline_log совпадает с реальным pipeline.""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - pm_result = {"success": True, "duration_seconds": 3, "tokens_used": 800, "cost_usd": 0.008} - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline( - conn, "VDOL-001", steps, - pm_result=pm_result, - pm_started_at="2026-03-17T10:00:00", - pm_ended_at="2026-03-17T10:00:03", - ) - - pipeline = conn.execute("SELECT * FROM pipelines WHERE task_id='VDOL-001'").fetchone() - assert pipeline is not None - - pm_log = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" - ).fetchone() - assert pm_log is not None - assert pm_log["pipeline_id"] == pipeline["id"] - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_entry_has_step_pm_in_extra( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """extra_json PM-записи содержит role='pm' и корректные данные тайминга.""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - pm_result = {"success": True, "duration_seconds": 7, "tokens_used": 1500, "cost_usd": 0.02} - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline( - conn, "VDOL-001", steps, - pm_result=pm_result, - pm_started_at="2026-03-17T10:00:00", - pm_ended_at="2026-03-17T10:00:07", - ) - - row = conn.execute( - "SELECT extra_json FROM pipeline_log WHERE message='PM step: task decomposed'" - ).fetchone() - assert row is not None - extra = json.loads(row["extra_json"]) - assert extra["role"] == "pm" - assert extra["duration_seconds"] == 7 - assert extra["pm_started_at"] == "2026-03-17T10:00:00" - assert extra["pm_ended_at"] == "2026-03-17T10:00:07" - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_not_written_when_pm_result_is_none( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """Если pm_result не передан (None), записи PM-шага в pipeline_log нет.""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline(conn, "VDOL-001", steps) # pm_result=None по умолчанию - - pm_logs = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" - ).fetchall() - assert len(pm_logs) == 0 - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_not_written_for_sub_pipeline( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """PM-лог НЕ записывается в sub-pipeline (parent_pipeline_id задан).""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - # Сначала создаём родительский pipeline - parent_pipeline = models.create_pipeline(conn, "VDOL-001", "vdol", "linear", []) - - pm_result = {"success": True, "duration_seconds": 4, "tokens_used": 900, "cost_usd": 0.009} - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline( - conn, "VDOL-001", steps, - pm_result=pm_result, - pm_started_at="2026-03-17T10:00:00", - pm_ended_at="2026-03-17T10:00:04", - parent_pipeline_id=parent_pipeline["id"], - ) - - pm_logs = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" - ).fetchall() - assert len(pm_logs) == 0 - - @patch("agents.runner._run_autocommit") - @patch("agents.runner._run_learning_extraction") - @patch("agents.runner.subprocess.run") - def test_pm_log_no_orphan_records( - self, mock_run, mock_learn, mock_autocommit, conn - ): - """FK integrity: все записи pipeline_log ссылаются на существующий pipeline.""" - mock_run.return_value = _mock_claude_success({"result": "done"}) - mock_learn.return_value = {"added": 0, "skipped": 0} - - pm_result = {"success": True, "duration_seconds": 2, "tokens_used": 500, "cost_usd": 0.005} - steps = [{"role": "debugger", "brief": "find bug"}] - run_pipeline( - conn, "VDOL-001", steps, - pm_result=pm_result, - pm_started_at="2026-03-17T10:00:00", - pm_ended_at="2026-03-17T10:00:02", - ) - - # Проверяем FK через JOIN — orphan-записей не должно быть - orphans = conn.execute( - """SELECT pl.id FROM pipeline_log pl - LEFT JOIN pipelines p ON pl.pipeline_id = p.id - WHERE p.id IS NULL""" - ).fetchall() - assert len(orphans) == 0 diff --git a/web/api.py b/web/api.py index 03bd387..5f8a395 100644 --- a/web/api.py +++ b/web/api.py @@ -761,10 +761,12 @@ def patch_task(task_id: str, body: TaskPatch): @app.get("/api/pipelines/{pipeline_id}/logs") def get_pipeline_logs(pipeline_id: int, since_id: int = 0): - """Get pipeline log entries after since_id (for live console polling). - Returns [] if pipeline does not exist — consistent empty response for log collections. - """ + """Get pipeline log entries after since_id (for live console polling).""" conn = get_conn() + row = conn.execute("SELECT id FROM pipelines WHERE id = ?", (pipeline_id,)).fetchone() + if not row: + conn.close() + raise HTTPException(404, f"Pipeline {pipeline_id} not found") logs = models.get_pipeline_logs(conn, pipeline_id, since_id=since_id) conn.close() return logs diff --git a/web/frontend/src/__tests__/filter-persistence.test.ts b/web/frontend/src/__tests__/filter-persistence.test.ts index 7a784ac..486b925 100644 --- a/web/frontend/src/__tests__/filter-persistence.test.ts +++ b/web/frontend/src/__tests__/filter-persistence.test.ts @@ -792,7 +792,7 @@ describe('KIN-015: TaskDetail — Edit button и форма редактиров // ───────────────────────────────────────────────────────────── describe('KIN-049: TaskDetail — кнопка Deploy', () => { - function makeDeployTask(status: string, deployCommand: string | null, deployRuntime: string | null = null) { + function makeDeployTask(status: string, deployCommand: string | null) { return { id: 'KIN-049', project_id: 'KIN', @@ -805,9 +805,6 @@ describe('KIN-049: TaskDetail — кнопка Deploy', () => { spec: null, execution_mode: null, project_deploy_command: deployCommand, - project_deploy_host: null, - project_deploy_path: null, - project_deploy_runtime: deployRuntime, created_at: '2024-01-01', updated_at: '2024-01-01', pipeline_steps: [], @@ -830,8 +827,8 @@ describe('KIN-049: TaskDetail — кнопка Deploy', () => { expect(deployBtn?.exists(), 'Кнопка Deploy должна быть видна при done + deploy_command').toBe(true) }) - it('Кнопка Deploy скрыта при status=done без project_deploy_command и project_deploy_runtime', async () => { - vi.mocked(api.taskFull).mockResolvedValue(makeDeployTask('done', null, null) as any) + it('Кнопка Deploy скрыта при status=done но без project_deploy_command', async () => { + vi.mocked(api.taskFull).mockResolvedValue(makeDeployTask('done', null) as any) const router = makeRouter() await router.push('/task/KIN-049') @@ -842,22 +839,7 @@ describe('KIN-049: TaskDetail — кнопка Deploy', () => { await flushPromises() const hasDeployBtn = wrapper.findAll('button').some(b => b.text().includes('Deploy')) - expect(hasDeployBtn, 'Deploy не должна быть видна без deploy_command и deploy_runtime').toBe(false) - }) - - it('Кнопка Deploy видна при status=done и только project_deploy_runtime задан', async () => { - vi.mocked(api.taskFull).mockResolvedValue(makeDeployTask('done', null, 'node') as any) - const router = makeRouter() - await router.push('/task/KIN-049') - - const wrapper = mount(TaskDetail, { - props: { id: 'KIN-049' }, - global: { plugins: [router] }, - }) - await flushPromises() - - const deployBtn = wrapper.findAll('button').find(b => b.text().includes('Deploy')) - expect(deployBtn?.exists(), 'Кнопка Deploy должна быть видна при done + deploy_runtime').toBe(true) + expect(hasDeployBtn, 'Deploy не должна быть видна без deploy_command').toBe(false) }) it('Кнопка Deploy скрыта при status=pending (даже с deploy_command)', async () => { diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 7c27ef9..1071564 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -199,8 +199,6 @@ export interface TaskFull extends Task { pipeline_steps: PipelineStep[] related_decisions: Decision[] project_deploy_command: string | null - project_deploy_host: string | null - project_deploy_path: string | null project_deploy_runtime: string | null pipeline_id: string | null } diff --git a/web/frontend/src/views/SettingsView.vue b/web/frontend/src/views/SettingsView.vue index bf7c70c..86a316a 100644 --- a/web/frontend/src/views/SettingsView.vue +++ b/web/frontend/src/views/SettingsView.vue @@ -1,6 +1,6 @@