diff --git a/agents/prompts/reviewer.md b/agents/prompts/reviewer.md index 4a5066b..fe6183a 100644 --- a/agents/prompts/reviewer.md +++ b/agents/prompts/reviewer.md @@ -91,24 +91,22 @@ If verdict is "changes_requested", findings must be non-empty with actionable su If verdict is "revise", include `"target_role": "..."` and findings must be non-empty with actionable suggestions. If verdict is "blocked", include `"blocked_reason": "..."` (e.g. unable to read files). -**Full response example:** +**Full response structure (write exactly this, two sections):** -``` -## Verdict -Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. + ## Verdict + Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. -## Details -```json -{ - "verdict": "approved", - "findings": [...], - "security_issues": [], - "conventions_violations": [], - "test_coverage": "adequate", - "summary": "..." -} -` ` ` -``` + ## Details + ```json + { + "verdict": "approved", + "findings": [...], + "security_issues": [], + "conventions_violations": [], + "test_coverage": "adequate", + "summary": "..." + } + ``` ## Verdict definitions diff --git a/agents/prompts/tester.md b/agents/prompts/tester.md index b2517f0..9eafbbf 100644 --- a/agents/prompts/tester.md +++ b/agents/prompts/tester.md @@ -43,7 +43,27 @@ For a specific test file: `python -m pytest tests/test_models.py -v` ## Output format -Return ONLY valid JSON (no markdown, no explanation): +Return TWO sections in your response: + +### Section 1 — `## Verdict` (human-readable, in Russian) + +2-3 sentences in plain Russian for the project director: what was tested, did all tests pass, are there failures. No JSON, no code snippets, no technical details. + +Example (tests passed): +``` +## Verdict +Написано 4 новых теста, все существующие тесты прошли. Новая функциональность покрыта полностью. Всё в порядке. +``` + +Example (tests failed): +``` +## Verdict +Тесты выявили проблему: 2 из 6 новых тестов упали из-за ошибки в функции обработки пустого ввода. Требуется исправление в backend. +``` + +### Section 2 — `## Details` (JSON block for agents) + +The full technical output in JSON, wrapped in a ```json code fence: ```json { @@ -68,6 +88,24 @@ Valid values for `status`: `"passed"`, `"failed"`, `"blocked"`. If status is "failed", populate `"failures"` with `[{"test": "...", "error": "..."}]`. If status is "blocked", include `"blocked_reason": "..."`. +**Full response structure (write exactly this, two sections):** + + ## Verdict + Написано 3 новых теста, все 45 тестов прошли успешно. Новые кейсы покрывают основные сценарии. Всё в порядке. + + ## Details + ```json + { + "status": "passed", + "tests_written": [...], + "tests_run": 45, + "tests_passed": 45, + "tests_failed": 0, + "failures": [], + "notes": "..." + } + ``` + ## Blocked Protocol If you cannot perform the task (no file access, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output: diff --git a/agents/runner.py b/agents/runner.py index 40832a2..fc3a69d 100644 --- a/agents/runner.py +++ b/agents/runner.py @@ -7,6 +7,7 @@ import errno as _errno import json import logging import os +import re import shlex import shutil import sqlite3 @@ -15,8 +16,6 @@ import time from pathlib import Path from typing import Any -import re - _logger = logging.getLogger("kin.runner") diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 3f4bf20..a198bc9 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -330,7 +330,7 @@ class TestProjectLinksAPI: "type": "depends_on", "description": "P1 depends on P2", }) - assert r.status_code == 200 + assert r.status_code == 201 data = r.json() assert data["from_project"] == "p1" assert data["to_project"] == "p2" diff --git a/web/api.py b/web/api.py index cce912b..82ca5e9 100644 --- a/web/api.py +++ b/web/api.py @@ -439,7 +439,7 @@ class ProjectLinkCreate(BaseModel): description: str | None = None -@app.post("/api/project-links") +@app.post("/api/project-links", status_code=201) def create_project_link(body: ProjectLinkCreate): """Create a project dependency link.""" conn = get_conn() diff --git a/web/frontend/src/__tests__/deploy-api.test.ts b/web/frontend/src/__tests__/deploy-api.test.ts index 3e2582d..a579f7d 100644 --- a/web/frontend/src/__tests__/deploy-api.test.ts +++ b/web/frontend/src/__tests__/deploy-api.test.ts @@ -53,14 +53,14 @@ describe('api.projectLinks', () => { // ───────────────────────────────────────────────────────────── describe('api.createProjectLink', () => { it('делает POST /api/project-links', async () => { - const spy = mockFetch({ id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '' }) - await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on' }) + const spy = mockFetch({ id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '' }) + await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', type: 'depends_on' }) expect(spy).toHaveBeenCalledWith('/api/project-links', expect.objectContaining({ method: 'POST' })) }) - it('передаёт from_project, to_project, link_type, description в теле', async () => { + it('передаёт from_project, to_project, type, description в теле', async () => { const spy = mockFetch({ id: 1 }) - const data = { from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'API used by frontend' } + const data = { from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'API used by frontend' } await api.createProjectLink(data) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) expect(body).toMatchObject(data) @@ -68,10 +68,10 @@ describe('api.createProjectLink', () => { it('передаёт запрос без description когда она не указана', async () => { const spy = mockFetch({ id: 1 }) - await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'triggers' }) + await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', type: 'triggers' }) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) expect(body.from_project).toBe('KIN') - expect(body.link_type).toBe('triggers') + expect(body.type).toBe('triggers') }) }) diff --git a/web/frontend/src/__tests__/deploy-standardized.test.ts b/web/frontend/src/__tests__/deploy-standardized.test.ts index 567d7f2..9593a5e 100644 --- a/web/frontend/src/__tests__/deploy-standardized.test.ts +++ b/web/frontend/src/__tests__/deploy-standardized.test.ts @@ -106,7 +106,7 @@ beforeEach(() => { success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2, } as any) vi.mocked(api.createProjectLink).mockResolvedValue({ - id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01', + id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01', } as any) vi.mocked(api.deleteProjectLink).mockResolvedValue(undefined as any) }) @@ -496,7 +496,7 @@ describe('ProjectView — Links таб', () => { it('связи отображаются при links.length > 0', async () => { const links = [ - { id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'test', created_at: '2026-01-01' }, + { id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'test', created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) const wrapper = await mountProjectView() @@ -505,9 +505,9 @@ describe('ProjectView — Links таб', () => { expect(wrapper.text()).toContain('depends_on') }) - it('link_type и description отображаются для каждой связи', async () => { + it('type и description отображаются для каждой связи', async () => { const links = [ - { id: 2, from_project: 'KIN', to_project: 'API', link_type: 'triggers', description: 'API call', created_at: '2026-01-01' }, + { id: 2, from_project: 'KIN', to_project: 'API', type: 'triggers', description: 'API call', created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) const wrapper = await mountProjectView() @@ -547,12 +547,12 @@ describe('ProjectView — Links таб', () => { const fromInput = disabledInputs.find(i => (i.element as HTMLInputElement).value === 'KIN') expect(fromInput).toBeDefined() - // to_project и link_type — select элементы + // to_project и type — select элементы const selects = wrapper.findAll('select') expect(selects.length).toBeGreaterThanOrEqual(1) }) - it('форма link_type select содержит depends_on, triggers, related_to', async () => { + it('форма type select содержит depends_on, triggers, related_to', async () => { const wrapper = await mountProjectView() await switchToLinksTab(wrapper) const plusBtn = wrapper.findAll('button').find(b => b.text().includes('+') && b.text().includes('Link')) @@ -605,7 +605,7 @@ describe('ProjectView — Links таб', () => { it('Delete вызывает api.deleteProjectLink с id связи', async () => { const links = [ - { id: 7, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01' }, + { id: 7, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) vi.spyOn(window, 'confirm').mockReturnValue(true) diff --git a/web/frontend/src/components/LiveConsole.vue b/web/frontend/src/components/LiveConsole.vue index 9e67404..dac380f 100644 --- a/web/frontend/src/components/LiveConsole.vue +++ b/web/frontend/src/components/LiveConsole.vue @@ -14,6 +14,7 @@ const consoleEl = ref(null) let sinceId = 0 let userScrolled = false let timer: ReturnType | null = null +let scrollTimer: ReturnType | null = null const MAX_LOGS = 500 @@ -44,9 +45,9 @@ async function fetchLogs() { sinceId = Math.max(...newLogs.map(l => l.id)) logs.value = [...logs.value, ...newLogs].slice(-MAX_LOGS) // Scroll after DOM update - setTimeout(scrollToBottom, 0) - } catch (e: any) { - error.value = e.message + scrollTimer = setTimeout(scrollToBottom, 0) + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : String(e) } } @@ -62,6 +63,7 @@ function startPolling() { function stopPolling() { if (timer) { clearInterval(timer); timer = null } + if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null } } async function toggle() {