diff --git a/agents/runner.py b/agents/runner.py index 04a1615..cf8b02d 100644 --- a/agents/runner.py +++ b/agents/runner.py @@ -47,9 +47,13 @@ def _build_claude_env() -> dict: if bin_dir.is_dir(): extra.append(str(bin_dir)) - seen = set(existing) - new_dirs = [d for d in extra if d and d not in seen] - env["PATH"] = ":".join(new_dirs + existing) + seen: set[str] = set() + deduped: list[str] = [] + for d in extra + existing: + if d and d not in seen: + seen.add(d) + deduped.append(d) + env["PATH"] = ":".join(deduped) return env diff --git a/tests/test_runner.py b/tests/test_runner.py index 1e10e06..720a870 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1173,8 +1173,14 @@ class TestClaudePath: ) def test_build_claude_env_no_duplicate_paths(self): - """_build_claude_env не должен дублировать уже существующие пути.""" - env = _build_claude_env() + """_build_claude_env не должен дублировать уже существующие пути. + + Мокируем PATH на фиксированное значение, чтобы тест не зависел от + реального окружения (решение #48). + """ + fixed_path = "/usr/bin:/bin" + with patch.dict("os.environ", {"PATH": fixed_path}, clear=False): + env = _build_claude_env() path_dirs = env["PATH"].split(":") seen = set() for d in path_dirs: diff --git a/web/api.py b/web/api.py index 3fe79c7..4ac7a5c 100644 --- a/web/api.py +++ b/web/api.py @@ -136,19 +136,27 @@ class ProjectCreate(BaseModel): class ProjectPatch(BaseModel): - execution_mode: str + execution_mode: str | None = None + autocommit_enabled: bool | None = None @app.patch("/api/projects/{project_id}") def patch_project(project_id: str, body: ProjectPatch): - if body.execution_mode not in VALID_EXECUTION_MODES: + if body.execution_mode is None and body.autocommit_enabled is None: + raise HTTPException(400, "Nothing to update. Provide execution_mode or autocommit_enabled.") + if body.execution_mode is not None and body.execution_mode not in VALID_EXECUTION_MODES: raise HTTPException(400, f"Invalid execution_mode '{body.execution_mode}'. Must be one of: {', '.join(VALID_EXECUTION_MODES)}") conn = get_conn() p = models.get_project(conn, project_id) if not p: conn.close() raise HTTPException(404, f"Project '{project_id}' not found") - models.update_project(conn, project_id, execution_mode=body.execution_mode) + fields = {} + if body.execution_mode is not None: + fields["execution_mode"] = body.execution_mode + if body.autocommit_enabled is not None: + fields["autocommit_enabled"] = int(body.autocommit_enabled) + models.update_project(conn, project_id, **fields) p = models.get_project(conn, project_id) conn.close() return p diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index d4b8274..080bfcf 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -40,6 +40,7 @@ export interface Project { priority: number tech_stack: string[] | null execution_mode: string | null + autocommit_enabled: number | null created_at: string total_tasks: number done_tasks: number @@ -169,7 +170,7 @@ export const api = { post<{ updated: string[]; count: number }>(`/projects/${projectId}/audit/apply`, { task_ids: taskIds }), patchTask: (id: string, data: { status?: string; execution_mode?: string }) => patch(`/tasks/${id}`, data), - patchProject: (id: string, data: { execution_mode: string }) => + patchProject: (id: string, data: { execution_mode?: string; autocommit_enabled?: boolean }) => patch(`/projects/${id}`, data), deleteDecision: (projectId: string, decisionId: number) => del<{ deleted: number }>(`/projects/${projectId}/decisions/${decisionId}`), diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index 744fdf2..0f5877b 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -62,6 +62,24 @@ async function toggleMode() { } } +// Autocommit toggle +const autocommit = ref(false) + +function loadAutocommit() { + autocommit.value = !!(project.value?.autocommit_enabled) +} + +async function toggleAutocommit() { + autocommit.value = !autocommit.value + try { + await api.patchProject(props.id, { autocommit_enabled: autocommit.value }) + if (project.value) project.value = { ...project.value, autocommit_enabled: autocommit.value ? 1 : 0 } + } catch (e: any) { + error.value = e.message + autocommit.value = !autocommit.value + } +} + // Audit const auditLoading = ref(false) const auditResult = ref(null) @@ -124,7 +142,7 @@ watch(selectedStatuses, (val) => { router.replace({ query: { ...route.query, status: val.length ? val.join(',') : undefined } }) }, { deep: true }) -onMounted(() => { load(); loadMode() }) +onMounted(async () => { await load(); loadMode(); loadAutocommit() }) const filteredTasks = computed(() => { if (!project.value) return []