Compare commits
4 commits
348aa07fec
...
6ffe4ffb9f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ffe4ffb9f | ||
|
|
e118b87a3e | ||
|
|
a4e5497401 | ||
|
|
669ed2fbc9 |
8 changed files with 74 additions and 37 deletions
|
|
@ -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 "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 example:**
|
**Full response structure (write exactly this, two sections):**
|
||||||
|
|
||||||
```
|
## 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,27 @@ For a specific test file: `python -m pytest tests/test_models.py -v`
|
||||||
|
|
||||||
## Output format
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -68,6 +88,24 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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
|
||||||
|
|
@ -15,8 +16,6 @@ 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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 == 200
|
assert r.status_code == 201
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,7 @@ class ProjectLinkCreate(BaseModel):
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/project-links")
|
@app.post("/api/project-links", status_code=201)
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -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', link_type: 'depends_on', description: null, created_at: '' })
|
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', link_type: 'depends_on' })
|
await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', 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, link_type, description в теле', async () => {
|
it('передаёт from_project, to_project, type, description в теле', async () => {
|
||||||
const spy = mockFetch({ id: 1 })
|
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)
|
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', 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)
|
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.link_type).toBe('triggers')
|
expect(body.type).toBe('triggers')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', 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)
|
} 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', 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)
|
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('link_type и description отображаются для каждой связи', async () => {
|
it('type и description отображаются для каждой связи', async () => {
|
||||||
const links = [
|
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)
|
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 и link_type — select элементы
|
// to_project и type — select элементы
|
||||||
const selects = wrapper.findAll('select')
|
const selects = wrapper.findAll('select')
|
||||||
expect(selects.length).toBeGreaterThanOrEqual(1)
|
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()
|
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', 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.mocked(api.projectLinks).mockResolvedValue(links as any)
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -44,9 +45,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
|
||||||
setTimeout(scrollToBottom, 0)
|
scrollTimer = setTimeout(scrollToBottom, 0)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
error.value = e.message
|
error.value = e instanceof Error ? e.message : String(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@ 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() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue