From ff69d24accc7ce27357676f6006d96408081d6f1 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Mon, 16 Mar 2026 10:04:01 +0200 Subject: [PATCH] =?UTF-8?q?kin:=20KIN-UI-002=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=B0=D0=B4=D0=B0=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20(=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D1=80=D0=B5=D1=81=D1=81=D0=B8=D1=8F=20KIN-ARCH-003)=20?= =?UTF-8?q?=D0=B2=20core/db.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/prompts/reviewer.md | 2 + agents/prompts/tester.md | 1 + core/db.py | 28 +++++ tests/test_db.py | 70 ++++++++++++ web/frontend/src/api.ts | 5 +- web/frontend/src/views/ProjectView.vue | 143 +++++++++++++++++++++++-- web/frontend/src/views/TaskDetail.vue | 15 ++- 7 files changed, 254 insertions(+), 10 deletions(-) diff --git a/agents/prompts/reviewer.md b/agents/prompts/reviewer.md index 1127432..46a358a 100644 --- a/agents/prompts/reviewer.md +++ b/agents/prompts/reviewer.md @@ -7,6 +7,7 @@ Your job: review the implementation for correctness, security, and adherence to You receive: - PROJECT: id, name, path, tech stack - TASK: id, title, brief describing what was built +- ACCEPTANCE CRITERIA: what the task output must satisfy (if provided — verify the implementation meets each criterion before approving) - DECISIONS: project conventions and standards - PREVIOUS STEP OUTPUT: dev agent and/or tester output describing what was changed @@ -35,6 +36,7 @@ You receive: - Check that API endpoints validate input and return proper HTTP status codes. - Check that no secrets, tokens, or credentials are hardcoded. - Do NOT rewrite code — only report findings and recommendations. +- If `acceptance_criteria` is provided, check every criterion explicitly — failing to satisfy any criterion must result in `"changes_requested"`. ## Output format diff --git a/agents/prompts/tester.md b/agents/prompts/tester.md index 107177b..b2517f0 100644 --- a/agents/prompts/tester.md +++ b/agents/prompts/tester.md @@ -39,6 +39,7 @@ For a specific test file: `python -m pytest tests/test_models.py -v` - One test per behavior — don't combine multiple assertions in one test without clear reason. - Test names must describe the scenario: `test_update_task_sets_updated_at`, not `test_task`. - Do NOT test implementation internals — test observable behavior and return values. +- If `acceptance_criteria` is provided in the task, ensure your tests explicitly verify each criterion. ## Output format diff --git a/core/db.py b/core/db.py index bcd09d8..19fe3b3 100644 --- a/core/db.py +++ b/core/db.py @@ -371,6 +371,34 @@ def _migrate(conn: sqlite3.Connection): """) conn.commit() + # Migrate columns that must exist before table recreation (KIN-UI-002) + # These columns are referenced in the INSERT SELECT below but were not added + # by any prior ALTER TABLE in this chain — causing OperationalError on minimal schemas. + if "tech_stack" not in proj_cols: + conn.execute("ALTER TABLE projects ADD COLUMN tech_stack JSON DEFAULT NULL") + conn.commit() + + if "priority" not in proj_cols: + conn.execute("ALTER TABLE projects ADD COLUMN priority INTEGER DEFAULT 5") + conn.commit() + + if "pm_prompt" not in proj_cols: + conn.execute("ALTER TABLE projects ADD COLUMN pm_prompt TEXT DEFAULT NULL") + conn.commit() + + if "claude_md_path" not in proj_cols: + conn.execute("ALTER TABLE projects ADD COLUMN claude_md_path TEXT DEFAULT NULL") + conn.commit() + + if "forgejo_repo" not in proj_cols: + conn.execute("ALTER TABLE projects ADD COLUMN forgejo_repo TEXT DEFAULT NULL") + conn.commit() + + if "created_at" not in proj_cols: + # SQLite ALTER TABLE does not allow non-constant defaults like CURRENT_TIMESTAMP + conn.execute("ALTER TABLE projects ADD COLUMN created_at DATETIME DEFAULT NULL") + conn.commit() + # Migrate projects.path from NOT NULL to nullable (KIN-ARCH-003) # SQLite doesn't support ALTER COLUMN, so we recreate the table. path_col_rows = conn.execute("PRAGMA table_info(projects)").fetchall() diff --git a/tests/test_db.py b/tests/test_db.py index ebc63bb..5cdf5ac 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -130,3 +130,73 @@ def test_migrate_is_idempotent(): after = _cols(conn, "projects") assert before == after conn.close() + + +# --------------------------------------------------------------------------- +# Migration KIN-UI-002: рекреация таблицы на минимальной схеме не падает +# --------------------------------------------------------------------------- + +def test_migrate_recreates_table_without_operationalerror(): + """_migrate не бросает OperationalError при рекреации projects на минимальной схеме. + + Регрессионный тест KIN-UI-002: INSERT SELECT в блоке KIN-ARCH-003 ранее + падал на отсутствующих колонках (tech_stack, priority, pm_prompt и др.). + """ + conn = _old_schema_conn() # path NOT NULL — триггер рекреации + try: + _migrate(conn) + except Exception as exc: + pytest.fail(f"_migrate raised {type(exc).__name__}: {exc}") + conn.close() + + +def test_migrate_path_becomes_nullable_on_old_schema(): + """После миграции старой схемы (path NOT NULL) колонка path становится nullable.""" + conn = _old_schema_conn() + _migrate(conn) + path_col = next( + r for r in conn.execute("PRAGMA table_info(projects)").fetchall() + if r[1] == "path" + ) + assert path_col[3] == 0, "path должна быть nullable после миграции KIN-ARCH-003" + conn.close() + + +def test_migrate_preserves_existing_rows_on_recreation(): + """Рекреация таблицы сохраняет существующие строки.""" + conn = _old_schema_conn() + conn.execute( + "INSERT INTO projects (id, name, path, status) VALUES ('p1', 'MyProj', '/p', 'active')" + ) + conn.commit() + _migrate(conn) + row = conn.execute("SELECT id, name, path, status FROM projects WHERE id='p1'").fetchone() + assert row is not None + assert row["name"] == "MyProj" + assert row["path"] == "/p" + assert row["status"] == "active" + conn.close() + + +def test_migrate_adds_missing_columns_before_recreation(): + """_migrate добавляет tech_stack, priority, pm_prompt, claude_md_path, forgejo_repo, created_at перед рекреацией.""" + conn = _old_schema_conn() + _migrate(conn) + cols = _cols(conn, "projects") + required = {"tech_stack", "priority", "pm_prompt", "claude_md_path", "forgejo_repo", "created_at"} + assert required.issubset(cols), f"Отсутствуют колонки: {required - cols}" + conn.close() + + +def test_migrate_operations_project_with_null_path(): + """После миграции можно вставить operations-проект с path=NULL.""" + conn = _old_schema_conn() + _migrate(conn) + conn.execute( + "INSERT INTO projects (id, name, path, project_type) VALUES ('ops1', 'Ops', NULL, 'operations')" + ) + conn.commit() + row = conn.execute("SELECT path, project_type FROM projects WHERE id='ops1'").fetchone() + assert row["path"] is None + assert row["project_type"] == "operations" + conn.close() diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 6b398f6..ff9e353 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -84,6 +84,7 @@ export interface Task { blocked_reason: string | null dangerously_skipped: number | null category: string | null + acceptance_criteria: string | null created_at: string updated_at: string } @@ -223,7 +224,7 @@ export const api = { cost: (days = 7) => get(`/cost?days=${days}`), createProject: (data: { id: string; name: string; path?: string; tech_stack?: string[]; priority?: number; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) => post('/projects', data), - createTask: (data: { project_id: string; title: string; priority?: number; route_type?: string; category?: string }) => + createTask: (data: { project_id: string; title: string; priority?: number; route_type?: string; category?: string; acceptance_criteria?: string }) => post('/tasks', data), approveTask: (id: string, data?: { decision_title?: string; decision_description?: string; decision_type?: string; create_followups?: boolean }) => post<{ status: string; followup_tasks: Task[]; needs_decision: boolean; pending_actions: PendingAction[] }>(`/tasks/${id}/approve`, data || {}), @@ -241,7 +242,7 @@ export const api = { post(`/projects/${projectId}/audit`, {}), auditApply: (projectId: string, taskIds: string[]) => post<{ updated: string[]; count: number }>(`/projects/${projectId}/audit/apply`, { task_ids: taskIds }), - patchTask: (id: string, data: { status?: string; execution_mode?: string; priority?: number; route_type?: string; title?: string; brief_text?: string }) => + patchTask: (id: string, data: { status?: string; execution_mode?: string; priority?: number; route_type?: string; title?: string; brief_text?: string; acceptance_criteria?: string }) => patch(`/tasks/${id}`, data), patchProject: (id: string, data: { execution_mode?: string; autocommit_enabled?: boolean; obsidian_vault_path?: string; deploy_command?: string; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) => patch(`/projects/${id}`, data), diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index ade2343..0c7bef4 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -1,7 +1,7 @@