kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 18:23:04 +02:00
parent f6bccdceb3
commit c767c6157a
5 changed files with 61 additions and 13 deletions

View file

@ -139,7 +139,6 @@ def _render_ps(rows: list[dict]) -> None:
def cmd_watch(conn, task_id: str) -> None: def cmd_watch(conn, task_id: str) -> None:
"""Polling monitor for a task. Updates every 5s. Exits on terminal pipeline state.""" """Polling monitor for a task. Updates every 5s. Exits on terminal pipeline state."""
import sqlite3
task = models.get_task(conn, task_id) task = models.get_task(conn, task_id)
if not task: if not task:
print(f"Task '{task_id}' not found.") print(f"Task '{task_id}' not found.")
@ -152,10 +151,7 @@ def cmd_watch(conn, task_id: str) -> None:
if pipeline: if pipeline:
log = models.get_current_agent_log(conn, task_id, pipeline['created_at']) log = models.get_current_agent_log(conn, task_id, pipeline['created_at'])
step_num = conn.execute( step_num = models.count_agent_logs_since(conn, task_id, pipeline['created_at'])
'SELECT COUNT(*) FROM agent_logs WHERE task_id = ? AND created_at >= ?',
(task_id, pipeline['created_at']),
).fetchone()[0]
total = _parse_total_steps(pipeline.get('steps')) total = _parse_total_steps(pipeline.get('steps'))
else: else:
log, step_num, total = None, 0, '?' log, step_num, total = None, 0, '?'

View file

@ -543,6 +543,15 @@ def get_pipeline_for_watch(conn: sqlite3.Connection, task_id: str) -> dict | Non
return _row_to_dict(row) return _row_to_dict(row)
def count_agent_logs_since(conn: sqlite3.Connection, task_id: str, since_iso: str) -> int:
"""Return number of agent_logs for a task created at or after since_iso."""
row = conn.execute(
"SELECT COUNT(*) FROM agent_logs WHERE task_id = ? AND created_at >= ?",
(task_id, since_iso),
).fetchone()
return row[0]
def get_current_agent_log( def get_current_agent_log(
conn: sqlite3.Connection, task_id: str, since_iso: str conn: sqlite3.Connection, task_id: str, since_iso: str
) -> dict | None: ) -> dict | None:

View file

@ -373,6 +373,49 @@ def test_log_agent_run(conn):
assert log["success"] == 1 # SQLite boolean assert log["success"] == 1 # SQLite boolean
def test_count_agent_logs_since_returns_correct_count(conn):
"""count_agent_logs_since возвращает количество логов >= since_iso."""
models.create_project(conn, "p1", "P1", "/p1")
models.create_task(conn, "P1-001", "p1", "Task")
models.log_agent_run(conn, "p1", "developer", "implement", task_id="P1-001")
models.log_agent_run(conn, "p1", "reviewer", "review", task_id="P1-001")
count = models.count_agent_logs_since(conn, "P1-001", "2000-01-01T00:00:00")
assert count == 2
def test_count_agent_logs_since_filters_by_task_id(conn):
"""count_agent_logs_since не считает логи других задач."""
models.create_project(conn, "p1", "P1", "/p1")
models.create_task(conn, "P1-001", "p1", "Task A")
models.create_task(conn, "P1-002", "p1", "Task B")
models.log_agent_run(conn, "p1", "developer", "implement", task_id="P1-001")
models.log_agent_run(conn, "p1", "developer", "implement", task_id="P1-002")
assert models.count_agent_logs_since(conn, "P1-001", "2000-01-01T00:00:00") == 1
assert models.count_agent_logs_since(conn, "P1-002", "2000-01-01T00:00:00") == 1
def test_count_agent_logs_since_excludes_before_cutoff(conn):
"""count_agent_logs_since не считает логи строго до since_iso."""
models.create_project(conn, "p1", "P1", "/p1")
models.create_task(conn, "P1-001", "p1", "Task")
models.log_agent_run(conn, "p1", "developer", "implement", task_id="P1-001")
# since_iso в далёком будущем — ни один лог не попадает
count = models.count_agent_logs_since(conn, "P1-001", "2099-01-01T00:00:00")
assert count == 0
def test_count_agent_logs_since_empty_returns_zero(conn):
"""count_agent_logs_since возвращает 0 при отсутствии логов."""
models.create_project(conn, "p1", "P1", "/p1")
models.create_task(conn, "P1-001", "p1", "Task")
count = models.count_agent_logs_since(conn, "P1-001", "2000-01-01T00:00:00")
assert count == 0
# -- Pipelines -- # -- Pipelines --
def test_create_and_update_pipeline(conn): def test_create_and_update_pipeline(conn):

View file

@ -189,7 +189,7 @@ export interface ProjectLink {
id: number id: number
from_project: string from_project: string
to_project: string to_project: string
link_type: string type: string
description: string | null description: string | null
created_at: string created_at: string
} }
@ -411,7 +411,7 @@ export const api = {
get<PipelineLog[]>(`/pipelines/${pipelineId}/logs?since_id=${sinceId}`), get<PipelineLog[]>(`/pipelines/${pipelineId}/logs?since_id=${sinceId}`),
projectLinks: (projectId: string) => projectLinks: (projectId: string) =>
get<ProjectLink[]>(`/projects/${projectId}/links`), get<ProjectLink[]>(`/projects/${projectId}/links`),
createProjectLink: (data: { from_project: string; to_project: string; link_type: string; description?: string }) => createProjectLink: (data: { from_project: string; to_project: string; type: string; description?: string }) =>
post<ProjectLink>('/project-links', data), post<ProjectLink>('/project-links', data),
deleteProjectLink: (id: number) => deleteProjectLink: (id: number) =>
del<void>(`/project-links/${id}`), del<void>(`/project-links/${id}`),

View file

@ -395,7 +395,7 @@ const links = ref<ProjectLink[]>([])
const linksLoading = ref(false) const linksLoading = ref(false)
const linksError = ref('') const linksError = ref('')
const showAddLink = ref(false) const showAddLink = ref(false)
const linkForm = ref({ to_project: '', link_type: 'depends_on', description: '' }) const linkForm = ref({ to_project: '', type: 'depends_on', description: '' })
const linkFormError = ref('') const linkFormError = ref('')
const linkSaving = ref(false) const linkSaving = ref(false)
@ -419,11 +419,11 @@ async function addLink() {
await api.createProjectLink({ await api.createProjectLink({
from_project: props.id, from_project: props.id,
to_project: linkForm.value.to_project, to_project: linkForm.value.to_project,
link_type: linkForm.value.link_type, type: linkForm.value.type,
description: linkForm.value.description || undefined, description: linkForm.value.description || undefined,
}) })
showAddLink.value = false showAddLink.value = false
linkForm.value = { to_project: '', link_type: 'depends_on', description: '' } linkForm.value = { to_project: '', type: 'depends_on', description: '' }
await loadLinks() await loadLinks()
} catch (e: any) { } catch (e: any) {
linkFormError.value = e.message linkFormError.value = e.message
@ -1368,7 +1368,7 @@ async function addDecision() {
<span class="text-gray-400 font-mono text-xs">{{ link.from_project }}</span> <span class="text-gray-400 font-mono text-xs">{{ link.from_project }}</span>
<span class="text-gray-600">-></span> <span class="text-gray-600">-></span>
<span class="text-gray-400 font-mono text-xs">{{ link.to_project }}</span> <span class="text-gray-400 font-mono text-xs">{{ link.to_project }}</span>
<span class="px-1.5 py-0.5 text-[10px] bg-indigo-900/30 text-indigo-400 border border-indigo-800 rounded">{{ link.link_type }}</span> <span class="px-1.5 py-0.5 text-[10px] bg-indigo-900/30 text-indigo-400 border border-indigo-800 rounded">{{ link.type }}</span>
<span v-if="link.description" class="text-gray-500 text-xs">{{ link.description }}</span> <span v-if="link.description" class="text-gray-500 text-xs">{{ link.description }}</span>
</div> </div>
<button @click="deleteLink(link.id)" <button @click="deleteLink(link.id)"
@ -1379,7 +1379,7 @@ async function addDecision() {
</div> </div>
<!-- Add Link Modal --> <!-- Add Link Modal -->
<Modal v-if="showAddLink" title="Add Link" @close="showAddLink = false; linkForm = { to_project: '', link_type: 'depends_on', description: '' }; linkFormError = ''"> <Modal v-if="showAddLink" title="Add Link" @close="showAddLink = false; linkForm = { to_project: '', type: 'depends_on', description: '' }; linkFormError = ''">
<form @submit.prevent="addLink" class="space-y-3"> <form @submit.prevent="addLink" class="space-y-3">
<div> <div>
<label class="block text-xs text-gray-500 mb-1">From (current project)</label> <label class="block text-xs text-gray-500 mb-1">From (current project)</label>
@ -1396,7 +1396,7 @@ async function addDecision() {
</div> </div>
<div> <div>
<label class="block text-xs text-gray-500 mb-1">Link type</label> <label class="block text-xs text-gray-500 mb-1">Link type</label>
<select v-model="linkForm.link_type" <select v-model="linkForm.type"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300"> class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300">
<option value="depends_on">depends_on</option> <option value="depends_on">depends_on</option>
<option value="triggers">triggers</option> <option value="triggers">triggers</option>