diff --git a/core/db.py b/core/db.py index d35c90a..618523a 100644 --- a/core/db.py +++ b/core/db.py @@ -222,9 +222,6 @@ CREATE TABLE IF NOT EXISTS project_links ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX IF NOT EXISTS idx_project_links_to ON project_links(to_project); -CREATE INDEX IF NOT EXISTS idx_project_links_from ON project_links(from_project); - -- Тикеты от пользователей CREATE TABLE IF NOT EXISTS support_tickets ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -719,23 +716,6 @@ def _migrate(conn: sqlite3.Connection): ) conn.commit() - # Create indexes for project_links (KIN-INFRA-008). - # Guard: indexes must be created AFTER the table exists. - if "project_links" in existing_tables: - existing_indexes = {r[0] for r in conn.execute( - "SELECT name FROM sqlite_master WHERE type='index'" - ).fetchall()} - if "idx_project_links_to" not in existing_indexes: - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_project_links_to ON project_links(to_project)" - ) - conn.commit() - if "idx_project_links_from" not in existing_indexes: - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_project_links_from ON project_links(from_project)" - ) - conn.commit() - def _seed_default_hooks(conn: sqlite3.Connection): """Seed default hooks for the kin project (idempotent). diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 38dae85..a198bc9 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -624,103 +624,3 @@ class TestBuildDeployStepsPythonRestartCmd: assert steps[2] == "pm2 restart all" assert steps[3] == "pm2 restart myservice" assert len(steps) == 4 - - -# --------------------------------------------------------------------------- -# 11. Migration: project_links indexes (KIN-INFRA-008) -# Convention #433: set-assert all columns/indexes after migration -# Convention #384: three test cases for conditional guard in _migrate() -# --------------------------------------------------------------------------- - -def _schema_with_project_links_no_indexes(): - """Minimal schema: project_links table exists but its indexes are absent.""" - import sqlite3 as _sqlite3 - conn = _sqlite3.connect(":memory:") - conn.row_factory = _sqlite3.Row - conn.executescript(""" - CREATE TABLE projects ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - path TEXT, - status TEXT DEFAULT 'active', - language TEXT DEFAULT 'ru', - execution_mode TEXT NOT NULL DEFAULT 'review', - project_type TEXT DEFAULT 'development' - ); - CREATE TABLE tasks ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL, - title TEXT NOT NULL, - status TEXT DEFAULT 'pending' - ); - CREATE TABLE project_links ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_project TEXT NOT NULL REFERENCES projects(id), - to_project TEXT NOT NULL REFERENCES projects(id), - type TEXT NOT NULL, - description TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - """) - conn.commit() - return conn - - -def _get_indexes(conn) -> set: - return {r[0] for r in conn.execute( - "SELECT name FROM sqlite_master WHERE type='index'" - ).fetchall()} - - -class TestProjectLinksIndexMigration: - """KIN-INFRA-008: индексы idx_project_links_to / idx_project_links_from.""" - - # --- fresh schema --- - - def test_fresh_schema_has_idx_project_links_to(self): - conn = init_db(db_path=":memory:") - assert "idx_project_links_to" in _get_indexes(conn) - conn.close() - - def test_fresh_schema_has_idx_project_links_from(self): - conn = init_db(db_path=":memory:") - assert "idx_project_links_from" in _get_indexes(conn) - conn.close() - - # Convention #433: assert all columns of project_links after fresh init - def test_fresh_schema_project_links_columns(self): - conn = init_db(db_path=":memory:") - cols = {r["name"] for r in conn.execute("PRAGMA table_info(project_links)").fetchall()} - assert cols == {"id", "from_project", "to_project", "type", "description", "created_at"} - conn.close() - - # --- Convention #384: три кейса для guard в _migrate() --- - - # Кейс 1: без таблицы — guard не падает, индексы не создаются - def test_migrate_without_project_links_table_no_error(self): - conn = _old_schema_no_deploy() # project_links отсутствует - _migrate(conn) # не должно упасть - indexes = _get_indexes(conn) - assert "idx_project_links_to" not in indexes - assert "idx_project_links_from" not in indexes - conn.close() - - # Кейс 2: таблица есть, индексов нет → _migrate() создаёт оба - def test_migrate_creates_both_indexes_when_table_exists(self): - conn = _schema_with_project_links_no_indexes() - _migrate(conn) - indexes = _get_indexes(conn) - assert "idx_project_links_to" in indexes - assert "idx_project_links_from" in indexes - conn.close() - - # Кейс 3: таблица есть, индексы уже есть → _migrate() идемпотентен - def test_migrate_is_idempotent_when_indexes_already_exist(self): - conn = init_db(db_path=":memory:") - before = _get_indexes(conn) - _migrate(conn) - after = _get_indexes(conn) - assert "idx_project_links_to" in after - assert "idx_project_links_from" in after - assert before == after - conn.close() diff --git a/web/frontend/src/__tests__/live-console.test.ts b/web/frontend/src/__tests__/live-console.test.ts index 2d44d04..c05a9b4 100644 --- a/web/frontend/src/__tests__/live-console.test.ts +++ b/web/frontend/src/__tests__/live-console.test.ts @@ -329,72 +329,6 @@ describe('отображение логов', () => { expect(wrapper.text()).toContain('Network fail') wrapper.unmount() }) - - // KIN-OBS-026: catch (e: unknown) — type narrowing - it('не-Error строка отображается через String(e) (catch unknown narrowing)', async () => { - vi.mocked(api.getPipelineLogs).mockRejectedValueOnce('строковая ошибка') - - const wrapper = mountConsole('done') - await wrapper.find('button').trigger('click') - await flushPromises() - - expect(wrapper.text()).toContain('Ошибка') - expect(wrapper.text()).toContain('строковая ошибка') - wrapper.unmount() - }) - - it('не-Error число отображается через String(e) (catch unknown narrowing)', async () => { - vi.mocked(api.getPipelineLogs).mockRejectedValueOnce(500) - - const wrapper = mountConsole('done') - await wrapper.find('button').trigger('click') - await flushPromises() - - expect(wrapper.text()).toContain('Ошибка') - expect(wrapper.text()).toContain('500') - wrapper.unmount() - }) -}) - -// ───────────────────────────────────────────────────────────── -// KIN-OBS-026: scrollTimer cleanup -// ───────────────────────────────────────────────────────────── -describe('scrollTimer cleanup (KIN-OBS-026)', () => { - it('onUnmounted очищает scrollTimer если он был активен', async () => { - // fetchLogs с логами создаёт scrollTimer = setTimeout(scrollToBottom, 0) - vi.mocked(api.getPipelineLogs).mockResolvedValueOnce([makeLog(1)] as any) - - const wrapper = mountConsole('done') - await wrapper.find('button').trigger('click') - await flushPromises() - - // С fake timers setTimeout(scrollToBottom, 0) ещё не выполнен — таймер висит - expect(vi.getTimerCount()).toBeGreaterThan(0) - - wrapper.unmount() - // stopPolling() очищает scrollTimer через clearTimeout - expect(vi.getTimerCount()).toBe(0) - }) - - it('закрытие панели очищает scrollTimer вместе с setInterval', async () => { - vi.mocked(api.getPipelineLogs) - .mockResolvedValueOnce([makeLog(1)] as any) - .mockResolvedValue([]) - - const wrapper = mountConsole('running') - await wrapper.find('button').trigger('click') - await flushPromises() - - // Есть как минимум setInterval + scrollTimer - expect(vi.getTimerCount()).toBeGreaterThanOrEqual(2) - - // Закрываем панель → stopPolling() вызывает clearInterval + clearTimeout - await wrapper.find('button').trigger('click') - await flushPromises() - - expect(vi.getTimerCount()).toBe(0) - wrapper.unmount() - }) }) // ───────────────────────────────────────────────────────────── diff --git a/web/frontend/src/views/TaskDetail.vue b/web/frontend/src/views/TaskDetail.vue index 7359c76..d6f23aa 100644 --- a/web/frontend/src/views/TaskDetail.vue +++ b/web/frontend/src/views/TaskDetail.vue @@ -37,11 +37,6 @@ const rejectReason = ref('') const showRevise = ref(false) const reviseComment = ref('') -const parsedSelectedOutput = computed(() => { - if (!selectedStep.value) return null - return parseAgentOutput(selectedStep.value.output_summary) -}) - // Auto/Review mode (per-task, persisted in DB; falls back to localStorage per project) const autoMode = ref(false) @@ -140,28 +135,6 @@ function formatOutput(text: string | null): string { } } -interface ParsedAgentOutput { - verdict: string | null - details: string | null - raw: string -} - -function parseAgentOutput(text: string | null): ParsedAgentOutput { - if (!text) return { verdict: null, details: null, raw: '' } - const verdictMatch = text.match(/##\s*Verdict\s*\n([\s\S]*?)(?=##\s*Details|$)/m) - const detailsJsonMatch = text.match(/##\s*Details[\s\S]*?```json\n([\s\S]*?)```/) - const verdict = verdictMatch ? verdictMatch[1].trim() : null - let details: string | null = null - if (detailsJsonMatch) { - try { - details = JSON.stringify(JSON.parse(detailsJsonMatch[1].trim()), null, 2) - } catch { - details = detailsJsonMatch[1].trim() - } - } - return { verdict, details, raw: text } -} - async function approve() { if (!task.value) return approveLoading.value = true