kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 20:21:52 +02:00
parent 35d258935a
commit e270d10832
3 changed files with 155 additions and 3 deletions

View file

@ -6,7 +6,7 @@ Uses core.models for all data access, never raw SQL.
import json import json
import os import os
import sys import sys
from datetime import datetime from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
import click import click
@ -618,13 +618,13 @@ def run_task(ctx, task_id, dry_run, allow_write):
# Step 1: PM decomposes # Step 1: PM decomposes
click.echo("Running PM to decompose task...") click.echo("Running PM to decompose task...")
pm_started_at = datetime.utcnow().isoformat() pm_started_at = datetime.now(timezone.utc).isoformat()
pm_result = run_agent( pm_result = run_agent(
conn, "pm", task_id, project_id, conn, "pm", task_id, project_id,
model="sonnet", dry_run=dry_run, model="sonnet", dry_run=dry_run,
allow_write=allow_write, noninteractive=is_noninteractive, allow_write=allow_write, noninteractive=is_noninteractive,
) )
pm_ended_at = datetime.utcnow().isoformat() pm_ended_at = datetime.now(timezone.utc).isoformat()
if dry_run: if dry_run:
click.echo("\n--- PM Prompt (dry-run) ---") click.echo("\n--- PM Prompt (dry-run) ---")

View file

@ -2342,3 +2342,102 @@ def test_get_pipeline_logs_since_id_returns_pm_entries(client):
done_log = next(log for log in logs if "PM done" in log["message"]) done_log = next(log for log in logs if "PM done" in log["message"])
assert done_log["extra_json"]["steps_count"] == 2 assert done_log["extra_json"]["steps_count"] == 2
assert done_log["extra_json"]["tokens_used"] == 1000 assert done_log["extra_json"]["tokens_used"] == 1000
# ---------------------------------------------------------------------------
# KIN-103 — PATCH /api/projects/{id} — worktrees_enabled toggle
# ---------------------------------------------------------------------------
def test_patch_project_worktrees_enabled_true(client):
"""PATCH с worktrees_enabled=true → 200, поле установлено в 1."""
r = client.patch("/api/projects/p1", json={"worktrees_enabled": True})
assert r.status_code == 200
assert r.json()["worktrees_enabled"] == 1
def test_patch_project_worktrees_enabled_false(client):
"""После включения PATCH с worktrees_enabled=false → 200, поле установлено в 0."""
client.patch("/api/projects/p1", json={"worktrees_enabled": True})
r = client.patch("/api/projects/p1", json={"worktrees_enabled": False})
assert r.status_code == 200
assert r.json()["worktrees_enabled"] == 0
def test_patch_project_worktrees_enabled_true_persisted_via_sql(client):
"""После PATCH worktrees_enabled=True прямой SQL подтверждает значение 1."""
client.patch("/api/projects/p1", json={"worktrees_enabled": True})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT worktrees_enabled FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row is not None
assert row[0] == 1
def test_patch_project_worktrees_enabled_false_persisted_via_sql(client):
"""После PATCH worktrees_enabled=False прямой SQL подтверждает значение 0."""
client.patch("/api/projects/p1", json={"worktrees_enabled": True})
client.patch("/api/projects/p1", json={"worktrees_enabled": False})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT worktrees_enabled FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row is not None
assert row[0] == 0
def test_patch_project_worktrees_enabled_null_before_first_update(client):
"""Новый проект имеет worktrees_enabled=0 (DEFAULT) до первого обновления."""
client.post("/api/projects", json={"id": "p_new", "name": "New", "path": "/new"})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT worktrees_enabled FROM projects WHERE id = 'p_new'").fetchone()
conn.close()
assert row is not None
assert not row[0] # DEFAULT 0 или NULL — в любом случае falsy
def test_patch_project_worktrees_enabled_get_includes_field(client):
"""GET проекта включает worktrees_enabled в ответе."""
r = client.get("/api/projects/p1")
assert r.status_code == 200
data = r.json()
assert "worktrees_enabled" in data
def test_patch_project_worktrees_enabled_and_autocommit_together(client):
"""PATCH с worktrees_enabled и autocommit_enabled → оба поля обновлены."""
r = client.patch("/api/projects/p1", json={
"worktrees_enabled": True,
"autocommit_enabled": True,
})
assert r.status_code == 200
data = r.json()
assert data["worktrees_enabled"] == 1
assert data["autocommit_enabled"] == 1
def test_patch_project_worktrees_enabled_no_change_when_not_in_patch(client):
"""PATCH без worktrees_enabled → поле не меняется."""
# Сначала установим worktrees_enabled=1
client.patch("/api/projects/p1", json={"worktrees_enabled": True})
# Потом патч без worktrees_enabled не должен его менять
r = client.patch("/api/projects/p1", json={"autocommit_enabled": True})
assert r.status_code == 200
assert r.json()["worktrees_enabled"] == 1
def test_patch_project_worktrees_enabled_toggle_sequence(client):
"""Последовательные включение/выключение worktrees_enabled."""
# Включаем
r1 = client.patch("/api/projects/p1", json={"worktrees_enabled": True})
assert r1.json()["worktrees_enabled"] == 1
# Отключаем
r2 = client.patch("/api/projects/p1", json={"worktrees_enabled": False})
assert r2.json()["worktrees_enabled"] == 0
# Снова включаем
r3 = client.patch("/api/projects/p1", json={"worktrees_enabled": True})
assert r3.json()["worktrees_enabled"] == 1

View file

@ -0,0 +1,53 @@
"""Regression tests for KIN-104 — замена datetime.utcnow() на datetime.now(timezone.utc) в cli/main.py.
AC1: datetime.utcnow() отсутствует в cli/main.py.
AC2: timezone импортирован из datetime в cli/main.py.
AC3: datetime.now(timezone.utc).isoformat() возвращает строку с суффиксом +00:00 (aware datetime).
"""
import re
from datetime import datetime, timezone
from pathlib import Path
import pytest
CLI_MAIN = Path(__file__).parent.parent / "cli" / "main.py"
# ---------------------------------------------------------------------------
# AC1: utcnow() отсутствует в источнике
# ---------------------------------------------------------------------------
def test_cli_main_does_not_contain_utcnow():
"""cli/main.py не должен содержать вызовов datetime.utcnow()."""
source = CLI_MAIN.read_text(encoding="utf-8")
assert "utcnow" not in source, "Найден устаревший вызов utcnow() в cli/main.py"
# ---------------------------------------------------------------------------
# AC2: timezone импортирован
# ---------------------------------------------------------------------------
def test_cli_main_imports_timezone():
"""cli/main.py должен импортировать timezone из datetime."""
source = CLI_MAIN.read_text(encoding="utf-8")
# Принимаем: from datetime import datetime, timezone или from datetime import ..., timezone, ...
assert re.search(r"from datetime import [^#\n]*timezone", source), \
"timezone не импортирован из datetime в cli/main.py"
# ---------------------------------------------------------------------------
# AC3: aware datetime → isoformat содержит +00:00
# ---------------------------------------------------------------------------
def test_now_timezone_utc_isoformat_contains_utc_offset():
"""datetime.now(timezone.utc).isoformat() должен содержать '+00:00'."""
ts = datetime.now(timezone.utc).isoformat()
assert "+00:00" in ts, f"Ожидался суффикс +00:00, получено: {ts}"
def test_now_timezone_utc_is_aware():
"""datetime.now(timezone.utc) должен возвращать aware datetime (tzinfo не None)."""
dt = datetime.now(timezone.utc)
assert dt.tzinfo is not None, "datetime.now(timezone.utc) вернул naive datetime"