Compare commits
No commits in common. "12d95b2e137b767bd35bb8dd77ebf63e10f26b13" and "8be9a52559d191dc038bfa268e765b718f3999a9" have entirely different histories.
12d95b2e13
...
8be9a52559
5 changed files with 11 additions and 344 deletions
|
|
@ -1139,7 +1139,7 @@ def _execute_department_head_step(
|
||||||
try:
|
try:
|
||||||
models.create_handoff(
|
models.create_handoff(
|
||||||
conn,
|
conn,
|
||||||
pipeline_id=sub_result.get("pipeline_id") or parent_pipeline_id,
|
pipeline_id=parent_pipeline_id or child_pipeline["id"],
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
from_department=dept_name,
|
from_department=dept_name,
|
||||||
to_department=next_department,
|
to_department=next_department,
|
||||||
|
|
@ -1216,8 +1216,6 @@ def run_pipeline(
|
||||||
allow_write: bool = False,
|
allow_write: bool = False,
|
||||||
noninteractive: bool = False,
|
noninteractive: bool = False,
|
||||||
initial_previous_output: str | None = None,
|
initial_previous_output: str | None = None,
|
||||||
parent_pipeline_id: int | None = None,
|
|
||||||
department: str | None = None,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Execute a multi-step pipeline of agents.
|
"""Execute a multi-step pipeline of agents.
|
||||||
|
|
||||||
|
|
@ -1260,11 +1258,8 @@ def run_pipeline(
|
||||||
# Create pipeline in DB
|
# Create pipeline in DB
|
||||||
pipeline = None
|
pipeline = None
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
effective_route_type = "dept_sub" if parent_pipeline_id else route_type
|
|
||||||
pipeline = models.create_pipeline(
|
pipeline = models.create_pipeline(
|
||||||
conn, task_id, project_id, effective_route_type, steps,
|
conn, task_id, project_id, route_type, steps,
|
||||||
parent_pipeline_id=parent_pipeline_id,
|
|
||||||
department=department,
|
|
||||||
)
|
)
|
||||||
# Save PID so watchdog can detect dead subprocesses (KIN-099)
|
# Save PID so watchdog can detect dead subprocesses (KIN-099)
|
||||||
models.update_pipeline(conn, pipeline["id"], pid=os.getpid())
|
models.update_pipeline(conn, pipeline["id"], pid=os.getpid())
|
||||||
|
|
|
||||||
|
|
@ -660,14 +660,6 @@ def _migrate(conn: sqlite3.Connection):
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Fix orphaned dept_sub pipelines left in 'running' state due to double-create bug
|
|
||||||
# (KIN-ARCH-012): before the fix, _execute_department_head_step created a pipeline
|
|
||||||
# that was never updated, leaving ghost 'running' records in prod DB.
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE pipelines SET status = 'failed' WHERE route_type = 'dept_sub' AND status = 'running'"
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def _seed_default_hooks(conn: sqlite3.Connection):
|
def _seed_default_hooks(conn: sqlite3.Connection):
|
||||||
"""Seed default hooks for the kin project (idempotent).
|
"""Seed default hooks for the kin project (idempotent).
|
||||||
|
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
"""
|
|
||||||
Regression tests for KIN-ARCH-014:
|
|
||||||
blocked_reason из dept_result должен корректно пробрасываться в БД при неуспешном sub-pipeline.
|
|
||||||
|
|
||||||
Два уровня цепочки (convention #305 — отдельный тест на каждый уровень):
|
|
||||||
|
|
||||||
Уровень 1: _execute_department_head_step должен включать blocked_reason из sub_result
|
|
||||||
в возвращаемый dict. Без фикса — dict возвращался без этого поля.
|
|
||||||
|
|
||||||
Уровень 2: run_pipeline должен передавать dept_result.blocked_reason в update_task
|
|
||||||
(а не generic 'Department X sub-pipeline failed').
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Fixtures
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def conn():
|
|
||||||
from core.db import init_db
|
|
||||||
from core import models
|
|
||||||
c = init_db(":memory:")
|
|
||||||
models.create_project(c, "proj1", "Project1", "/tmp/proj1", tech_stack=["python"])
|
|
||||||
models.create_task(c, "PROJ1-001", "proj1", "Parent task",
|
|
||||||
brief={"route_type": "department"})
|
|
||||||
yield c
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _dept_head_result_with_sub_pipeline():
|
|
||||||
"""Валидный вывод dept head с sub_pipeline — используется в обоих уровнях."""
|
|
||||||
return {
|
|
||||||
"raw_output": json.dumps({
|
|
||||||
"sub_pipeline": [
|
|
||||||
{"role": "backend_dev", "brief": "implement feature"}
|
|
||||||
],
|
|
||||||
"artifacts": {},
|
|
||||||
"handoff_notes": "proceed",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _failing_sub_result(blocked_reason=None, error=None):
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"blocked_reason": blocked_reason,
|
|
||||||
"error": error,
|
|
||||||
"total_cost_usd": 0,
|
|
||||||
"total_tokens": 0,
|
|
||||||
"total_duration_seconds": 0,
|
|
||||||
"steps_completed": 0,
|
|
||||||
"results": [],
|
|
||||||
"pipeline_id": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _mock_agent_success(output="done"):
|
|
||||||
"""Мок успешного subprocess.run для run_agent."""
|
|
||||||
m = MagicMock()
|
|
||||||
m.stdout = json.dumps({"result": output})
|
|
||||||
m.stderr = ""
|
|
||||||
m.returncode = 0
|
|
||||||
return m
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Уровень 1: _execute_department_head_step propagates blocked_reason
|
|
||||||
# Convention #304: имя теста описывает сломанное поведение
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class TestDeptHeadStepBlockedReasonPropagation:
|
|
||||||
"""Уровень 1 цепочки: _execute_department_head_step → sub_result → returned dict."""
|
|
||||||
|
|
||||||
def test_blocked_reason_dropped_from_returned_dict_when_sub_pipeline_fails(self, conn):
|
|
||||||
"""Broken behavior: _execute_department_head_step отбрасывал blocked_reason
|
|
||||||
из sub_result — возвращаемый dict не содержал этого поля.
|
|
||||||
|
|
||||||
Fixed (KIN-ARCH-014): blocked_reason из sub_result прокидывается в dict.
|
|
||||||
"""
|
|
||||||
from agents.runner import _execute_department_head_step
|
|
||||||
|
|
||||||
failing = _failing_sub_result(
|
|
||||||
blocked_reason="Конкретная причина: tester заблокировал задачу",
|
|
||||||
error="TestError: endpoint not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("agents.runner.run_pipeline", return_value=failing), \
|
|
||||||
patch("agents.runner.models.create_pipeline", return_value={"id": 99}), \
|
|
||||||
patch("agents.runner.models.create_handoff"):
|
|
||||||
result = _execute_department_head_step(
|
|
||||||
conn=conn,
|
|
||||||
task_id="PROJ1-001",
|
|
||||||
project_id="proj1",
|
|
||||||
parent_pipeline_id=None,
|
|
||||||
step={"role": "backend_head", "brief": "plan"},
|
|
||||||
dept_head_result=_dept_head_result_with_sub_pipeline(),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["success"] is False
|
|
||||||
assert "blocked_reason" in result, (
|
|
||||||
"blocked_reason должен быть в возвращаемом dict "
|
|
||||||
"— без фикса KIN-ARCH-014 это поле отсутствовало"
|
|
||||||
)
|
|
||||||
assert result["blocked_reason"] == "Конкретная причина: tester заблокировал задачу"
|
|
||||||
|
|
||||||
def test_error_field_used_as_fallback_when_blocked_reason_is_none(self, conn):
|
|
||||||
"""Если sub_result.blocked_reason is None, но есть error —
|
|
||||||
error должен использоваться как blocked_reason (fallback).
|
|
||||||
"""
|
|
||||||
from agents.runner import _execute_department_head_step
|
|
||||||
|
|
||||||
failing = _failing_sub_result(
|
|
||||||
blocked_reason=None,
|
|
||||||
error="RuntimeError: Claude CLI exited with code 1",
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("agents.runner.run_pipeline", return_value=failing), \
|
|
||||||
patch("agents.runner.models.create_pipeline", return_value={"id": 99}), \
|
|
||||||
patch("agents.runner.models.create_handoff"):
|
|
||||||
result = _execute_department_head_step(
|
|
||||||
conn=conn,
|
|
||||||
task_id="PROJ1-001",
|
|
||||||
project_id="proj1",
|
|
||||||
parent_pipeline_id=None,
|
|
||||||
step={"role": "backend_head", "brief": "plan"},
|
|
||||||
dept_head_result=_dept_head_result_with_sub_pipeline(),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["success"] is False
|
|
||||||
assert result.get("blocked_reason") == "RuntimeError: Claude CLI exited with code 1"
|
|
||||||
|
|
||||||
def test_success_result_does_not_inject_blocked_reason(self, conn):
|
|
||||||
"""При успешном sub_result blocked_reason не добавляется в dict."""
|
|
||||||
from agents.runner import _execute_department_head_step
|
|
||||||
|
|
||||||
success_sub_result = {
|
|
||||||
"success": True,
|
|
||||||
"total_cost_usd": 0.01,
|
|
||||||
"total_tokens": 100,
|
|
||||||
"total_duration_seconds": 5.0,
|
|
||||||
"steps_completed": 1,
|
|
||||||
"results": [{"role": "backend_dev", "output": "done"}],
|
|
||||||
"pipeline_id": 99,
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch("agents.runner.run_pipeline", return_value=success_sub_result), \
|
|
||||||
patch("agents.runner.models.create_pipeline", return_value={"id": 99}), \
|
|
||||||
patch("agents.runner.models.create_handoff"):
|
|
||||||
result = _execute_department_head_step(
|
|
||||||
conn=conn,
|
|
||||||
task_id="PROJ1-001",
|
|
||||||
project_id="proj1",
|
|
||||||
parent_pipeline_id=None,
|
|
||||||
step={"role": "backend_head", "brief": "plan"},
|
|
||||||
dept_head_result=_dept_head_result_with_sub_pipeline(),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["success"] is True
|
|
||||||
assert not result.get("blocked_reason"), (
|
|
||||||
"blocked_reason не должен появляться при успешном sub_result"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Уровень 2: run_pipeline passes dept_result.blocked_reason to update_task
|
|
||||||
# Convention #305: отдельный класс для каждого уровня
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class TestRunPipelineDeptBlockedReasonSavedToDb:
|
|
||||||
"""Уровень 2 цепочки: run_pipeline → dept_result → models.update_task → БД."""
|
|
||||||
|
|
||||||
@patch("agents.runner._run_autocommit")
|
|
||||||
@patch("agents.runner._execute_department_head_step")
|
|
||||||
@patch("agents.runner._is_department_head")
|
|
||||||
@patch("agents.runner.subprocess.run")
|
|
||||||
@patch("agents.runner.check_claude_auth")
|
|
||||||
def test_generic_error_msg_saved_instead_of_dept_blocked_reason(
|
|
||||||
self, mock_auth, mock_run, mock_is_dept, mock_dept_step, mock_autocommit, conn
|
|
||||||
):
|
|
||||||
"""Broken behavior: run_pipeline сохранял в БД generic строку
|
|
||||||
'Department X sub-pipeline failed' вместо реального blocked_reason из dept_result.
|
|
||||||
|
|
||||||
Fixed (KIN-ARCH-014): в БД сохраняется dept_result["blocked_reason"].
|
|
||||||
"""
|
|
||||||
from agents.runner import run_pipeline
|
|
||||||
from core import models
|
|
||||||
|
|
||||||
mock_run.return_value = _mock_agent_success()
|
|
||||||
mock_is_dept.return_value = True
|
|
||||||
mock_dept_step.return_value = {
|
|
||||||
"success": False,
|
|
||||||
"blocked_reason": "Специфическая причина: backend_dev упал с OOM",
|
|
||||||
"error": "OOMError",
|
|
||||||
"output": "",
|
|
||||||
"cost_usd": 0,
|
|
||||||
"tokens_used": 0,
|
|
||||||
"duration_seconds": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
steps = [{"role": "backend_head", "brief": "plan the work"}]
|
|
||||||
result = run_pipeline(conn, "PROJ1-001", steps)
|
|
||||||
|
|
||||||
assert result["success"] is False
|
|
||||||
task = models.get_task(conn, "PROJ1-001")
|
|
||||||
assert task["status"] == "blocked"
|
|
||||||
assert task["blocked_reason"] == "Специфическая причина: backend_dev упал с OOM", (
|
|
||||||
f"Ожидался реальный blocked_reason из dept_result, "
|
|
||||||
f"получено: {task['blocked_reason']!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("agents.runner._run_autocommit")
|
|
||||||
@patch("agents.runner._execute_department_head_step")
|
|
||||||
@patch("agents.runner._is_department_head")
|
|
||||||
@patch("agents.runner.subprocess.run")
|
|
||||||
@patch("agents.runner.check_claude_auth")
|
|
||||||
def test_generic_string_not_saved_when_specific_reason_available(
|
|
||||||
self, mock_auth, mock_run, mock_is_dept, mock_dept_step, mock_autocommit, conn
|
|
||||||
):
|
|
||||||
"""run_pipeline НЕ должен сохранять generic 'Department X sub-pipeline failed'
|
|
||||||
в blocked_reason когда dept_result содержит конкретный blocked_reason.
|
|
||||||
"""
|
|
||||||
from agents.runner import run_pipeline
|
|
||||||
from core import models
|
|
||||||
|
|
||||||
specific_reason = "Tester blocked: API endpoint /api/users не реализован"
|
|
||||||
mock_run.return_value = _mock_agent_success()
|
|
||||||
mock_is_dept.return_value = True
|
|
||||||
mock_dept_step.return_value = {
|
|
||||||
"success": False,
|
|
||||||
"blocked_reason": specific_reason,
|
|
||||||
"error": specific_reason,
|
|
||||||
"output": "",
|
|
||||||
"cost_usd": 0,
|
|
||||||
"tokens_used": 0,
|
|
||||||
"duration_seconds": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
steps = [{"role": "backend_head", "brief": "plan"}]
|
|
||||||
run_pipeline(conn, "PROJ1-001", steps)
|
|
||||||
|
|
||||||
task = models.get_task(conn, "PROJ1-001")
|
|
||||||
# Generic строка из old code не должна быть в БД
|
|
||||||
assert "sub-pipeline failed" not in (task["blocked_reason"] or ""), (
|
|
||||||
f"Найдена generic строка в blocked_reason: {task['blocked_reason']!r}. "
|
|
||||||
f"Ожидался специфичный blocked_reason из dept_result."
|
|
||||||
)
|
|
||||||
assert task["blocked_reason"] == specific_reason
|
|
||||||
|
|
||||||
@patch("agents.runner._run_autocommit")
|
|
||||||
@patch("agents.runner._execute_department_head_step")
|
|
||||||
@patch("agents.runner._is_department_head")
|
|
||||||
@patch("agents.runner.subprocess.run")
|
|
||||||
@patch("agents.runner.check_claude_auth")
|
|
||||||
def test_fallback_to_generic_when_dept_result_has_no_blocked_reason(
|
|
||||||
self, mock_auth, mock_run, mock_is_dept, mock_dept_step, mock_autocommit, conn
|
|
||||||
):
|
|
||||||
"""Если dept_result не содержит ни blocked_reason, ни error —
|
|
||||||
используется fallback generic строка (не KeyError, задача блокируется).
|
|
||||||
"""
|
|
||||||
from agents.runner import run_pipeline
|
|
||||||
from core import models
|
|
||||||
|
|
||||||
mock_run.return_value = _mock_agent_success()
|
|
||||||
mock_is_dept.return_value = True
|
|
||||||
mock_dept_step.return_value = {
|
|
||||||
"success": False,
|
|
||||||
"blocked_reason": None,
|
|
||||||
"error": None,
|
|
||||||
"output": "",
|
|
||||||
"cost_usd": 0,
|
|
||||||
"tokens_used": 0,
|
|
||||||
"duration_seconds": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
steps = [{"role": "backend_head", "brief": "plan"}]
|
|
||||||
result = run_pipeline(conn, "PROJ1-001", steps)
|
|
||||||
|
|
||||||
assert result["success"] is False
|
|
||||||
task = models.get_task(conn, "PROJ1-001")
|
|
||||||
assert task["status"] == "blocked"
|
|
||||||
# Должен быть хоть какой-то blocked_reason (fallback, не None)
|
|
||||||
assert task["blocked_reason"] is not None
|
|
||||||
assert len(task["blocked_reason"]) > 0
|
|
||||||
|
|
@ -246,8 +246,11 @@ class TestMissingDeptRoutes:
|
||||||
route a task exclusively to the infra or research department via route template.
|
route a task exclusively to the infra or research department via route template.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_infra_head_has_dedicated_dept_route(self):
|
def test_infra_head_has_no_dedicated_dept_route(self):
|
||||||
"""dept_infra route exists in specialists.yaml with steps: [infra_head]."""
|
"""Confirms infra_head is NOT reachable via any standalone dept_infra route.
|
||||||
|
|
||||||
|
This test documents the gap. It will FAIL once a dept_infra route is added.
|
||||||
|
"""
|
||||||
import yaml
|
import yaml
|
||||||
with open("agents/specialists.yaml") as f:
|
with open("agents/specialists.yaml") as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
|
|
@ -255,15 +258,15 @@ class TestMissingDeptRoutes:
|
||||||
routes = data["routes"]
|
routes = data["routes"]
|
||||||
dept_routes = {k: v for k, v in routes.items() if k.startswith("dept_")}
|
dept_routes = {k: v for k, v in routes.items() if k.startswith("dept_")}
|
||||||
|
|
||||||
|
# Check no route exclusively using infra_head as its only step
|
||||||
infra_only_routes = [
|
infra_only_routes = [
|
||||||
name for name, route in dept_routes.items()
|
name for name, route in dept_routes.items()
|
||||||
if route["steps"] == ["infra_head"]
|
if route["steps"] == ["infra_head"]
|
||||||
]
|
]
|
||||||
assert len(infra_only_routes) == 1, (
|
assert len(infra_only_routes) == 0, (
|
||||||
f"Expected exactly one dept_infra route with steps=[infra_head], "
|
f"Found unexpected dept_infra route(s): {infra_only_routes}. "
|
||||||
f"found: {infra_only_routes}"
|
"Update this test if route was intentionally added."
|
||||||
)
|
)
|
||||||
assert "dept_infra" in infra_only_routes
|
|
||||||
|
|
||||||
def test_research_head_has_no_dedicated_dept_route(self):
|
def test_research_head_has_no_dedicated_dept_route(self):
|
||||||
"""Confirms research_head is NOT reachable via any standalone dept_research route.
|
"""Confirms research_head is NOT reachable via any standalone dept_research route.
|
||||||
|
|
|
||||||
|
|
@ -238,24 +238,6 @@ async function toggleAutocommit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-test toggle
|
|
||||||
const autoTest = ref(false)
|
|
||||||
|
|
||||||
function loadAutoTest() {
|
|
||||||
autoTest.value = !!(project.value?.auto_test_enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleAutoTest() {
|
|
||||||
autoTest.value = !autoTest.value
|
|
||||||
try {
|
|
||||||
await api.patchProject(props.id, { auto_test_enabled: autoTest.value })
|
|
||||||
if (project.value) project.value = { ...project.value, auto_test_enabled: autoTest.value ? 1 : 0 }
|
|
||||||
} catch (e: any) {
|
|
||||||
error.value = e.message
|
|
||||||
autoTest.value = !autoTest.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
const auditLoading = ref(false)
|
const auditLoading = ref(false)
|
||||||
const auditResult = ref<AuditResult | null>(null)
|
const auditResult = ref<AuditResult | null>(null)
|
||||||
|
|
@ -407,7 +389,6 @@ async function load() {
|
||||||
project.value = await api.project(props.id)
|
project.value = await api.project(props.id)
|
||||||
loadMode()
|
loadMode()
|
||||||
loadAutocommit()
|
loadAutocommit()
|
||||||
loadAutoTest()
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -774,14 +755,6 @@ async function addDecision() {
|
||||||
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
||||||
{{ autocommit ? '✓ Autocommit' : 'Autocommit' }}
|
{{ autocommit ? '✓ Autocommit' : 'Autocommit' }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleAutoTest"
|
|
||||||
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
||||||
:class="autoTest
|
|
||||||
? 'bg-blue-900/30 text-blue-400 border-blue-800 hover:bg-blue-900/50'
|
|
||||||
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
||||||
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
|
||||||
{{ autoTest ? '✓ Автотест' : 'Автотест' }}
|
|
||||||
</button>
|
|
||||||
<button @click="runAudit" :disabled="auditLoading"
|
<button @click="runAudit" :disabled="auditLoading"
|
||||||
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
||||||
title="Check which pending tasks are already done">
|
title="Check which pending tasks are already done">
|
||||||
|
|
@ -1085,14 +1058,6 @@ async function addDecision() {
|
||||||
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
||||||
{{ autocommit ? '✓ Автокомит' : 'Автокомит' }}
|
{{ autocommit ? '✓ Автокомит' : 'Автокомит' }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleAutoTest"
|
|
||||||
class="px-2 py-1 text-xs border rounded transition-colors"
|
|
||||||
:class="autoTest
|
|
||||||
? 'bg-blue-900/30 text-blue-400 border-blue-800 hover:bg-blue-900/50'
|
|
||||||
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
|
||||||
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
|
||||||
{{ autoTest ? '✓ Автотест' : 'Автотест' }}
|
|
||||||
</button>
|
|
||||||
<button @click="runAudit" :disabled="auditLoading"
|
<button @click="runAudit" :disabled="auditLoading"
|
||||||
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
||||||
title="Check which pending tasks are already done">
|
title="Check which pending tasks are already done">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue