Compare commits

..

No commits in common. "6ffe4ffb9f32c565821848dd3db316c9c8b9a70a" and "348aa07fecd8f5398423d0532434e92f2b8a290f" have entirely different histories.

8 changed files with 37 additions and 74 deletions

View file

@ -91,22 +91,24 @@ 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 "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). If verdict is "blocked", include `"blocked_reason": "..."` (e.g. unable to read files).
**Full response structure (write exactly this, two sections):** **Full response example:**
## Verdict ```
Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. ## Verdict
Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать.
## Details ## Details
```json ```json
{ {
"verdict": "approved", "verdict": "approved",
"findings": [...], "findings": [...],
"security_issues": [], "security_issues": [],
"conventions_violations": [], "conventions_violations": [],
"test_coverage": "adequate", "test_coverage": "adequate",
"summary": "..." "summary": "..."
} }
``` ` ` `
```
## Verdict definitions ## Verdict definitions

View file

@ -43,27 +43,7 @@ For a specific test file: `python -m pytest tests/test_models.py -v`
## Output format ## Output format
Return TWO sections in your response: Return ONLY valid JSON (no markdown, no explanation):
### 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 ```json
{ {
@ -88,24 +68,6 @@ Valid values for `status`: `"passed"`, `"failed"`, `"blocked"`.
If status is "failed", populate `"failures"` with `[{"test": "...", "error": "..."}]`. If status is "failed", populate `"failures"` with `[{"test": "...", "error": "..."}]`.
If status is "blocked", include `"blocked_reason": "..."`. 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 ## 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: If you cannot perform the task (no file access, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output:

View file

@ -7,7 +7,6 @@ import errno as _errno
import json import json
import logging import logging
import os import os
import re
import shlex import shlex
import shutil import shutil
import sqlite3 import sqlite3
@ -16,6 +15,8 @@ import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import re
_logger = logging.getLogger("kin.runner") _logger = logging.getLogger("kin.runner")

View file

@ -330,7 +330,7 @@ class TestProjectLinksAPI:
"type": "depends_on", "type": "depends_on",
"description": "P1 depends on P2", "description": "P1 depends on P2",
}) })
assert r.status_code == 201 assert r.status_code == 200
data = r.json() data = r.json()
assert data["from_project"] == "p1" assert data["from_project"] == "p1"
assert data["to_project"] == "p2" assert data["to_project"] == "p2"

View file

@ -439,7 +439,7 @@ class ProjectLinkCreate(BaseModel):
description: str | None = None description: str | None = None
@app.post("/api/project-links", status_code=201) @app.post("/api/project-links")
def create_project_link(body: ProjectLinkCreate): def create_project_link(body: ProjectLinkCreate):
"""Create a project dependency link.""" """Create a project dependency link."""
conn = get_conn() conn = get_conn()

View file

@ -53,14 +53,14 @@ describe('api.projectLinks', () => {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
describe('api.createProjectLink', () => { describe('api.createProjectLink', () => {
it('делает POST /api/project-links', async () => { it('делает POST /api/project-links', async () => {
const spy = mockFetch({ id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '' }) 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', type: 'depends_on' }) await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on' })
expect(spy).toHaveBeenCalledWith('/api/project-links', expect.objectContaining({ method: 'POST' })) expect(spy).toHaveBeenCalledWith('/api/project-links', expect.objectContaining({ method: 'POST' }))
}) })
it('передаёт from_project, to_project, type, description в теле', async () => { it('передаёт from_project, to_project, link_type, description в теле', async () => {
const spy = mockFetch({ id: 1 }) const spy = mockFetch({ id: 1 })
const data = { from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'API used by frontend' } const data = { from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'API used by frontend' }
await api.createProjectLink(data) await api.createProjectLink(data)
const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string)
expect(body).toMatchObject(data) expect(body).toMatchObject(data)
@ -68,10 +68,10 @@ describe('api.createProjectLink', () => {
it('передаёт запрос без description когда она не указана', async () => { it('передаёт запрос без description когда она не указана', async () => {
const spy = mockFetch({ id: 1 }) const spy = mockFetch({ id: 1 })
await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', type: 'triggers' }) await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'triggers' })
const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string)
expect(body.from_project).toBe('KIN') expect(body.from_project).toBe('KIN')
expect(body.type).toBe('triggers') expect(body.link_type).toBe('triggers')
}) })
}) })

View file

@ -106,7 +106,7 @@ beforeEach(() => {
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2, success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
} as any) } as any)
vi.mocked(api.createProjectLink).mockResolvedValue({ vi.mocked(api.createProjectLink).mockResolvedValue({
id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01', id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01',
} as any) } as any)
vi.mocked(api.deleteProjectLink).mockResolvedValue(undefined as any) vi.mocked(api.deleteProjectLink).mockResolvedValue(undefined as any)
}) })
@ -496,7 +496,7 @@ describe('ProjectView — Links таб', () => {
it('связи отображаются при links.length > 0', async () => { it('связи отображаются при links.length > 0', async () => {
const links = [ const links = [
{ id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'test', created_at: '2026-01-01' }, { id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'test', created_at: '2026-01-01' },
] ]
vi.mocked(api.projectLinks).mockResolvedValue(links as any) vi.mocked(api.projectLinks).mockResolvedValue(links as any)
const wrapper = await mountProjectView() const wrapper = await mountProjectView()
@ -505,9 +505,9 @@ describe('ProjectView — Links таб', () => {
expect(wrapper.text()).toContain('depends_on') expect(wrapper.text()).toContain('depends_on')
}) })
it('type и description отображаются для каждой связи', async () => { it('link_type и description отображаются для каждой связи', async () => {
const links = [ const links = [
{ id: 2, from_project: 'KIN', to_project: 'API', type: 'triggers', description: 'API call', created_at: '2026-01-01' }, { id: 2, from_project: 'KIN', to_project: 'API', link_type: 'triggers', description: 'API call', created_at: '2026-01-01' },
] ]
vi.mocked(api.projectLinks).mockResolvedValue(links as any) vi.mocked(api.projectLinks).mockResolvedValue(links as any)
const wrapper = await mountProjectView() const wrapper = await mountProjectView()
@ -547,12 +547,12 @@ describe('ProjectView — Links таб', () => {
const fromInput = disabledInputs.find(i => (i.element as HTMLInputElement).value === 'KIN') const fromInput = disabledInputs.find(i => (i.element as HTMLInputElement).value === 'KIN')
expect(fromInput).toBeDefined() expect(fromInput).toBeDefined()
// to_project и type — select элементы // to_project и link_type — select элементы
const selects = wrapper.findAll('select') const selects = wrapper.findAll('select')
expect(selects.length).toBeGreaterThanOrEqual(1) expect(selects.length).toBeGreaterThanOrEqual(1)
}) })
it('форма type select содержит depends_on, triggers, related_to', async () => { it('форма link_type select содержит depends_on, triggers, related_to', async () => {
const wrapper = await mountProjectView() const wrapper = await mountProjectView()
await switchToLinksTab(wrapper) await switchToLinksTab(wrapper)
const plusBtn = wrapper.findAll('button').find(b => b.text().includes('+') && b.text().includes('Link')) 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 () => { it('Delete вызывает api.deleteProjectLink с id связи', async () => {
const links = [ const links = [
{ id: 7, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01' }, { id: 7, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01' },
] ]
vi.mocked(api.projectLinks).mockResolvedValue(links as any) vi.mocked(api.projectLinks).mockResolvedValue(links as any)
vi.spyOn(window, 'confirm').mockReturnValue(true) vi.spyOn(window, 'confirm').mockReturnValue(true)

View file

@ -14,7 +14,6 @@ const consoleEl = ref<HTMLElement | null>(null)
let sinceId = 0 let sinceId = 0
let userScrolled = false let userScrolled = false
let timer: ReturnType<typeof setInterval> | null = null let timer: ReturnType<typeof setInterval> | null = null
let scrollTimer: ReturnType<typeof setTimeout> | null = null
const MAX_LOGS = 500 const MAX_LOGS = 500
@ -45,9 +44,9 @@ async function fetchLogs() {
sinceId = Math.max(...newLogs.map(l => l.id)) sinceId = Math.max(...newLogs.map(l => l.id))
logs.value = [...logs.value, ...newLogs].slice(-MAX_LOGS) logs.value = [...logs.value, ...newLogs].slice(-MAX_LOGS)
// Scroll after DOM update // Scroll after DOM update
scrollTimer = setTimeout(scrollToBottom, 0) setTimeout(scrollToBottom, 0)
} catch (e: unknown) { } catch (e: any) {
error.value = e instanceof Error ? e.message : String(e) error.value = e.message
} }
} }
@ -63,7 +62,6 @@ function startPolling() {
function stopPolling() { function stopPolling() {
if (timer) { clearInterval(timer); timer = null } if (timer) { clearInterval(timer); timer = null }
if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null }
} }
async function toggle() { async function toggle() {