From a2183af6e2a630dbf289c28ffbd926ae10697129 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 4 Mar 2026 03:08:53 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20test=20suite=20=E2=80=94=20seed=20reset,?= =?UTF-8?q?=20FK=20mappings,=20standalone=20focus=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change all seed inserts from ON CONFLICT DO NOTHING to DO UPDATE SET so seeds always reset to canonical values between test runs - Add meeting_id, decision_id, link_id to FK_FIELD_MAP in form_factory - Add item_ids, direction, label, content to NAME_PATTERNS - Add TestStandaloneFocusItems (7 tests): quick-add, edit, project tab - Add TestFocusConversion (6 tests): convert to task/note/link/list item - Add focus_standalone seed with fixture - Update focus_page_loads test for permanent (non-date-scoped) items - All 398 tests passing Co-Authored-By: Claude Opus 4.6 --- tests/conftest.py | 92 ++++++---- tests/form_factory.py | 10 +- tests/test_business_logic.py | 317 ++++++++++++++++++++++++++++++++++- 3 files changed, 386 insertions(+), 33 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 89ef551..24db71b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,7 @@ SEED_IDS = { "process_run": "a0000000-0000-0000-0000-000000000012", "time_budget": "a0000000-0000-0000-0000-000000000013", "file": "a0000000-0000-0000-0000-000000000014", + "focus_standalone": "a0000000-0000-0000-0000-000000000015", } @@ -93,85 +94,98 @@ def all_seeds(sync_conn): cur.execute(""" INSERT INTO domains (id, name, color, description, sort_order, is_deleted, created_at, updated_at) VALUES (%s, 'Test Domain', '#FF5733', 'Auto test domain', 0, false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET name='Test Domain', color='#FF5733', description='Auto test domain', + sort_order=0, is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["domain"],)) # Area cur.execute(""" INSERT INTO areas (id, name, domain_id, description, status, sort_order, is_deleted, created_at, updated_at) VALUES (%s, 'Test Area', %s, 'Auto test area', 'active', 0, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["area"], d["domain"])) + ON CONFLICT (id) DO UPDATE SET name='Test Area', domain_id=%s, description='Auto test area', + status='active', sort_order=0, is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["area"], d["domain"], d["domain"])) # Project cur.execute(""" INSERT INTO projects (id, name, domain_id, area_id, description, status, priority, sort_order, is_deleted, created_at, updated_at) VALUES (%s, 'Test Project', %s, %s, 'Auto test project', 'active', 2, 0, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["project"], d["domain"], d["area"])) + ON CONFLICT (id) DO UPDATE SET name='Test Project', domain_id=%s, area_id=%s, + description='Auto test project', status='active', priority=2, sort_order=0, + is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["project"], d["domain"], d["area"], d["domain"], d["area"])) # Task (status='open' matches DB default, not 'todo') cur.execute(""" INSERT INTO tasks (id, title, domain_id, project_id, description, priority, status, sort_order, is_deleted, created_at, updated_at) VALUES (%s, 'Test Task', %s, %s, 'Auto test task', 2, 'open', 0, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["task"], d["domain"], d["project"])) + ON CONFLICT (id) DO UPDATE SET title='Test Task', domain_id=%s, project_id=%s, + description='Auto test task', priority=2, status='open', sort_order=0, + is_deleted=false, deleted_at=NULL, completed_at=NULL, updated_at=now() + """, (d["task"], d["domain"], d["project"], d["domain"], d["project"])) # Contact cur.execute(""" INSERT INTO contacts (id, first_name, last_name, company, email, is_deleted, created_at, updated_at) VALUES (%s, 'Test', 'Contact', 'TestCorp', 'test@example.com', false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET first_name='Test', last_name='Contact', company='TestCorp', + email='test@example.com', is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["contact"],)) # Note cur.execute(""" INSERT INTO notes (id, title, domain_id, body, content_format, is_deleted, created_at, updated_at) VALUES (%s, 'Test Note', %s, 'Test body content', 'markdown', false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["note"], d["domain"])) + ON CONFLICT (id) DO UPDATE SET title='Test Note', domain_id=%s, body='Test body content', + content_format='markdown', is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["note"], d["domain"], d["domain"])) # Meeting cur.execute(""" INSERT INTO meetings (id, title, meeting_date, status, is_deleted, created_at, updated_at) VALUES (%s, 'Test Meeting', '2025-06-15', 'scheduled', false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET title='Test Meeting', meeting_date='2025-06-15', + status='scheduled', is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["meeting"],)) # Decision cur.execute(""" INSERT INTO decisions (id, title, status, impact, is_deleted, created_at, updated_at) VALUES (%s, 'Test Decision', 'decided', 'high', false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET title='Test Decision', status='decided', impact='high', + is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["decision"],)) # Appointment cur.execute(""" INSERT INTO appointments (id, title, start_at, end_at, all_day, is_deleted, created_at, updated_at) VALUES (%s, 'Test Appointment', '2025-06-15 10:00:00', '2025-06-15 11:00:00', false, false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET title='Test Appointment', start_at='2025-06-15 10:00:00', + end_at='2025-06-15 11:00:00', all_day=false, is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["appointment"],)) # Link folder cur.execute(""" INSERT INTO link_folders (id, name, is_deleted, created_at, updated_at) VALUES (%s, 'Test Folder', false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET name='Test Folder', is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["link_folder"],)) # List cur.execute(""" INSERT INTO lists (id, name, domain_id, project_id, list_type, is_deleted, created_at, updated_at) VALUES (%s, 'Test List', %s, %s, 'checklist', false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["list"], d["domain"], d["project"])) + ON CONFLICT (id) DO UPDATE SET name='Test List', domain_id=%s, project_id=%s, + list_type='checklist', is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["list"], d["domain"], d["project"], d["domain"], d["project"])) # Link cur.execute(""" INSERT INTO links (id, label, url, domain_id, is_deleted, created_at, updated_at) VALUES (%s, 'Test Link', 'https://example.com', %s, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["link"], d["domain"])) + ON CONFLICT (id) DO UPDATE SET label='Test Link', url='https://example.com', domain_id=%s, + is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["link"], d["domain"], d["domain"])) # Link folder junction cur.execute(""" @@ -183,15 +197,26 @@ def all_seeds(sync_conn): cur.execute(""" INSERT INTO capture (id, raw_text, processed, is_deleted, created_at, updated_at) VALUES (%s, 'Test capture item', false, false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET raw_text='Test capture item', processed=false, + is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["capture"],)) - # Daily focus + # Daily focus (task-linked) cur.execute(""" INSERT INTO daily_focus (id, task_id, focus_date, completed, created_at, updated_at) VALUES (%s, %s, CURRENT_DATE, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["focus"], d["task"])) + ON CONFLICT (id) DO UPDATE SET task_id=%s, focus_date=CURRENT_DATE, completed=false, + is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["focus"], d["task"], d["task"])) + + # Daily focus (standalone text item) + cur.execute(""" + INSERT INTO daily_focus (id, focus_date, completed, title, domain_id, project_id, created_at, updated_at) + VALUES (%s, CURRENT_DATE, false, 'Test Standalone Focus', %s, %s, now(), now()) + ON CONFLICT (id) DO UPDATE SET focus_date=CURRENT_DATE, completed=false, + title='Test Standalone Focus', domain_id=%s, project_id=%s, + is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["focus_standalone"], d["domain"], d["project"], d["domain"], d["project"])) # Junction table seeds for contact tabs cur.execute(""" @@ -215,29 +240,33 @@ def all_seeds(sync_conn): cur.execute(""" INSERT INTO processes (id, name, process_type, status, category, is_deleted, created_at, updated_at) VALUES (%s, 'Test Process', 'checklist', 'active', 'Testing', false, now(), now()) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id) DO UPDATE SET name='Test Process', process_type='checklist', status='active', + category='Testing', is_deleted=false, deleted_at=NULL, updated_at=now() """, (d["process"],)) # Process step cur.execute(""" INSERT INTO process_steps (id, process_id, title, instructions, sort_order, is_deleted, created_at, updated_at) VALUES (%s, %s, 'Test Step', 'Do the thing', 0, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["process_step"], d["process"])) + ON CONFLICT (id) DO UPDATE SET process_id=%s, title='Test Step', instructions='Do the thing', + sort_order=0, is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["process_step"], d["process"], d["process"])) # Process run cur.execute(""" INSERT INTO process_runs (id, process_id, title, status, process_type, task_generation, is_deleted, created_at, updated_at) VALUES (%s, %s, 'Test Run', 'not_started', 'checklist', 'all_at_once', false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["process_run"], d["process"])) + ON CONFLICT (id) DO UPDATE SET process_id=%s, title='Test Run', status='not_started', + process_type='checklist', task_generation='all_at_once', is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["process_run"], d["process"], d["process"])) # Time budget cur.execute(""" INSERT INTO time_budgets (id, domain_id, weekly_hours, effective_from, is_deleted, created_at, updated_at) VALUES (%s, %s, 10, CURRENT_DATE, false, now(), now()) - ON CONFLICT (id) DO NOTHING - """, (d["time_budget"], d["domain"])) + ON CONFLICT (id) DO UPDATE SET domain_id=%s, weekly_hours=10, effective_from=CURRENT_DATE, + is_deleted=false, deleted_at=NULL, updated_at=now() + """, (d["time_budget"], d["domain"], d["domain"])) # File (create a dummy file on disk for download/serve tests) import os @@ -275,6 +304,7 @@ def all_seeds(sync_conn): cur.execute("DELETE FROM process_runs WHERE id = %s", (d["process_run"],)) cur.execute("DELETE FROM process_steps WHERE id = %s", (d["process_step"],)) cur.execute("DELETE FROM processes WHERE id = %s", (d["process"],)) + cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus_standalone"],)) cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus"],)) cur.execute("DELETE FROM capture WHERE id = %s", (d["capture"],)) cur.execute("DELETE FROM folder_links WHERE link_id = %s", (d["link"],)) @@ -350,3 +380,7 @@ def seed_list(all_seeds): @pytest.fixture(scope="session") def seed_appointment(all_seeds): return {"id": all_seeds["appointment"], "title": "Test Appointment"} + +@pytest.fixture(scope="session") +def seed_focus_standalone(all_seeds): + return {"id": all_seeds["focus_standalone"], "title": "Test Standalone Focus"} diff --git a/tests/form_factory.py b/tests/form_factory.py index 407ace5..031bcfc 100644 --- a/tests/form_factory.py +++ b/tests/form_factory.py @@ -24,15 +24,18 @@ from tests.introspect import FormField # FK fields map to seed fixture keys FK_FIELD_MAP = { "domain_id": "domain", + "quick_domain_id": "domain", "area_id": "area", "project_id": "project", "task_id": "task", "folder_id": "weblink_folder", "parent_id": None, # Usually optional, skip - "meeting_id": None, + "meeting_id": "meeting", "contact_id": "contact", "release_id": None, "note_id": "note", + "link_id": "link", + "decision_id": "decision", "list_id": "list", "process_id": "process", "run_id": "process_run", @@ -80,6 +83,11 @@ NAME_PATTERNS: list[tuple[str, Any]] = [ ("notes", "Test notes field"), ("weekly_hours", "10"), ("effective_from", None), # Resolved dynamically as date + ("item_ids", ""), # Comma-separated UUIDs for reorder + ("item_id", ""), # Single item ID for reorder + ("direction", "up"), # Reorder direction + ("label", "Test Label Auto"), # Link label + ("content", "Test content"), # List item content ] diff --git a/tests/test_business_logic.py b/tests/test_business_logic.py index 833cc81..0a9178c 100644 --- a/tests/test_business_logic.py +++ b/tests/test_business_logic.py @@ -1165,9 +1165,9 @@ class TestFocusWorkflow: await _delete_tasks(db_session, [tid]) @pytest.mark.asyncio - async def test_focus_page_loads_with_date(self, client: AsyncClient): - """Focus page loads for specific date.""" - r = await client.get(f"/focus/?focus_date={date.today()}") + async def test_focus_page_loads(self, client: AsyncClient): + """Focus page loads without date filter (permanent items).""" + r = await client.get("/focus/", follow_redirects=True) assert r.status_code == 200 @@ -2505,6 +2505,317 @@ class TestFocusFilters: assert r.status_code == 200 +# =========================================================================== +# Standalone Focus Items +# =========================================================================== +class TestStandaloneFocusItems: + """Test quick-add standalone items, edit, and project tab.""" + + @pytest.mark.asyncio + async def test_quick_add_standalone_item( + self, client: AsyncClient, db_session: AsyncSession, seed_domain: dict, + ): + """Quick-add creates a standalone focus item with title.""" + tag = _uid() + r = await client.post("/focus/quick-add", data={ + "title": f"Standalone-{tag}", + "quick_domain_id": seed_domain["id"], + }, follow_redirects=False) + assert r.status_code == 303 + + result = await db_session.execute( + text("SELECT title, domain_id FROM daily_focus WHERE title = :t AND is_deleted = false"), + {"t": f"Standalone-{tag}"}, + ) + row = result.first() + assert row is not None + assert str(row.domain_id) == seed_domain["id"] + + # Cleanup + await db_session.execute( + text("DELETE FROM daily_focus WHERE title = :t"), {"t": f"Standalone-{tag}"} + ) + await db_session.commit() + + @pytest.mark.asyncio + async def test_quick_add_without_domain( + self, client: AsyncClient, db_session: AsyncSession, + ): + """Quick-add works without a domain.""" + tag = _uid() + r = await client.post("/focus/quick-add", data={ + "title": f"NoDomain-{tag}", + }, follow_redirects=False) + assert r.status_code == 303 + + result = await db_session.execute( + text("SELECT title, domain_id FROM daily_focus WHERE title = :t AND is_deleted = false"), + {"t": f"NoDomain-{tag}"}, + ) + row = result.first() + assert row is not None + assert row.domain_id is None + + await db_session.execute( + text("DELETE FROM daily_focus WHERE title = :t"), {"t": f"NoDomain-{tag}"} + ) + await db_session.commit() + + @pytest.mark.asyncio + async def test_edit_standalone_page_loads( + self, client: AsyncClient, seed_focus_standalone: dict, + ): + """Edit page loads for standalone focus items.""" + r = await client.get(f"/focus/{seed_focus_standalone['id']}/edit") + assert r.status_code == 200 + assert "Test Standalone Focus" in r.text + + @pytest.mark.asyncio + async def test_edit_standalone_redirects_for_task_items( + self, client: AsyncClient, all_seeds: dict, + ): + """Edit page redirects away for task-linked focus items.""" + r = await client.get(f"/focus/{all_seeds['focus']}/edit", follow_redirects=False) + assert r.status_code == 303 + + @pytest.mark.asyncio + async def test_update_standalone_item( + self, client: AsyncClient, db_session: AsyncSession, + seed_domain: dict, seed_project: dict, + ): + """Editing a standalone item updates title, domain, project.""" + tag = _uid() + # Create a standalone item + fid = str(uuid.uuid4()) + await db_session.execute( + text("INSERT INTO daily_focus (id, focus_date, completed, title, sort_order, is_deleted, created_at, updated_at) " + "VALUES (:id, CURRENT_DATE, false, :title, 0, false, now(), now())"), + {"id": fid, "title": f"EditMe-{tag}"}, + ) + await db_session.commit() + + r = await client.post(f"/focus/{fid}/edit", data={ + "title": f"Edited-{tag}", + "domain_id": seed_domain["id"], + "project_id": seed_project["id"], + }, follow_redirects=False) + assert r.status_code == 303 + + result = await db_session.execute( + text("SELECT title, domain_id, project_id FROM daily_focus WHERE id = :id"), + {"id": fid}, + ) + row = result.first() + assert row.title == f"Edited-{tag}" + assert str(row.domain_id) == seed_domain["id"] + assert str(row.project_id) == seed_project["id"] + + await db_session.execute(text("DELETE FROM daily_focus WHERE id = :id"), {"id": fid}) + await db_session.commit() + + @pytest.mark.asyncio + async def test_focus_page_shows_standalone_items( + self, client: AsyncClient, seed_focus_standalone: dict, + ): + """Standalone items appear on the focus page.""" + r = await client.get("/focus/", follow_redirects=True) + assert r.status_code == 200 + assert "Test Standalone Focus" in r.text + + @pytest.mark.asyncio + async def test_project_focus_tab( + self, client: AsyncClient, seed_project: dict, seed_focus_standalone: dict, + ): + """Focus tab on project detail shows standalone items assigned to that project.""" + r = await client.get(f"/projects/{seed_project['id']}?tab=focus") + assert r.status_code == 200 + assert "Test Standalone Focus" in r.text + + +# =========================================================================== +# Focus Item Conversion +# =========================================================================== +class TestFocusConversion: + """Test converting standalone focus items to other entity types.""" + + @pytest.mark.asyncio + async def test_convert_to_task( + self, client: AsyncClient, db_session: AsyncSession, seed_domain: dict, + ): + """Convert standalone focus item to task.""" + tag = _uid() + fid = str(uuid.uuid4()) + await db_session.execute( + text("INSERT INTO daily_focus (id, focus_date, completed, title, domain_id, sort_order, is_deleted, created_at, updated_at) " + "VALUES (:id, CURRENT_DATE, false, :title, :did, 0, false, now(), now())"), + {"id": fid, "title": f"ConvTask-{tag}", "did": seed_domain["id"]}, + ) + await db_session.commit() + + r = await client.post(f"/focus/{fid}/convert-to-task", follow_redirects=False) + assert r.status_code == 303 + + # Focus item should now have task_id set and title cleared + result = await db_session.execute( + text("SELECT task_id, title FROM daily_focus WHERE id = :id"), {"id": fid}, + ) + row = result.first() + assert row.task_id is not None + assert row.title is None + + # Task should exist + result = await db_session.execute( + text("SELECT title, status FROM tasks WHERE id = :id"), {"id": str(row.task_id)}, + ) + task = result.first() + assert task.title == f"ConvTask-{tag}" + assert task.status == "open" + + # Cleanup + await db_session.execute(text("DELETE FROM daily_focus WHERE id = :id"), {"id": fid}) + await db_session.execute(text("DELETE FROM tasks WHERE id = :id"), {"id": str(row.task_id)}) + await db_session.commit() + + @pytest.mark.asyncio + async def test_convert_to_note( + self, client: AsyncClient, db_session: AsyncSession, seed_domain: dict, + ): + """Convert standalone focus item to note.""" + tag = _uid() + fid = str(uuid.uuid4()) + await db_session.execute( + text("INSERT INTO daily_focus (id, focus_date, completed, title, domain_id, sort_order, is_deleted, created_at, updated_at) " + "VALUES (:id, CURRENT_DATE, false, :title, :did, 0, false, now(), now())"), + {"id": fid, "title": f"ConvNote-{tag}", "did": seed_domain["id"]}, + ) + await db_session.commit() + + r = await client.post(f"/focus/{fid}/convert-to-note", follow_redirects=False) + assert r.status_code == 303 + + # Focus item should be soft-deleted + result = await db_session.execute( + text("SELECT is_deleted FROM daily_focus WHERE id = :id"), {"id": fid}, + ) + assert result.first().is_deleted is True + + # Note should exist + result = await db_session.execute( + text("SELECT title, domain_id FROM notes WHERE title = :t AND is_deleted = false"), + {"t": f"ConvNote-{tag}"}, + ) + note = result.first() + assert note is not None + assert str(note.domain_id) == seed_domain["id"] + + # Cleanup + await db_session.execute(text("DELETE FROM daily_focus WHERE id = :id"), {"id": fid}) + await db_session.execute( + text("DELETE FROM notes WHERE title = :t"), {"t": f"ConvNote-{tag}"} + ) + await db_session.commit() + + @pytest.mark.asyncio + async def test_convert_to_link( + self, client: AsyncClient, db_session: AsyncSession, seed_domain: dict, + ): + """Convert standalone focus item to link.""" + tag = _uid() + fid = str(uuid.uuid4()) + await db_session.execute( + text("INSERT INTO daily_focus (id, focus_date, completed, title, domain_id, sort_order, is_deleted, created_at, updated_at) " + "VALUES (:id, CURRENT_DATE, false, :title, :did, 0, false, now(), now())"), + {"id": fid, "title": f"ConvLink-{tag}", "did": seed_domain["id"]}, + ) + await db_session.commit() + + r = await client.post(f"/focus/{fid}/convert-to-link", follow_redirects=False) + assert r.status_code == 303 + + # Focus item should be soft-deleted + result = await db_session.execute( + text("SELECT is_deleted FROM daily_focus WHERE id = :id"), {"id": fid}, + ) + assert result.first().is_deleted is True + + # Link should exist with title as label + result = await db_session.execute( + text("SELECT label, domain_id FROM links WHERE label = :l AND is_deleted = false"), + {"l": f"ConvLink-{tag}"}, + ) + link = result.first() + assert link is not None + assert str(link.domain_id) == seed_domain["id"] + + # Cleanup + await db_session.execute(text("DELETE FROM daily_focus WHERE id = :id"), {"id": fid}) + await db_session.execute( + text("DELETE FROM links WHERE label = :l"), {"l": f"ConvLink-{tag}"} + ) + await db_session.commit() + + @pytest.mark.asyncio + async def test_convert_to_list_item( + self, client: AsyncClient, db_session: AsyncSession, + seed_domain: dict, seed_list: dict, + ): + """Convert standalone focus item to list item.""" + tag = _uid() + fid = str(uuid.uuid4()) + await db_session.execute( + text("INSERT INTO daily_focus (id, focus_date, completed, title, domain_id, sort_order, is_deleted, created_at, updated_at) " + "VALUES (:id, CURRENT_DATE, false, :title, :did, 0, false, now(), now())"), + {"id": fid, "title": f"ConvLI-{tag}", "did": seed_domain["id"]}, + ) + await db_session.commit() + + r = await client.post(f"/focus/{fid}/convert-to-list-item", data={ + "list_id": seed_list["id"], + }, follow_redirects=False) + assert r.status_code == 303 + + # Focus item should now point to a list_item_id with title cleared + result = await db_session.execute( + text("SELECT list_item_id, title FROM daily_focus WHERE id = :id"), {"id": fid}, + ) + row = result.first() + assert row.list_item_id is not None + assert row.title is None + + # List item should exist + result = await db_session.execute( + text("SELECT content, list_id FROM list_items WHERE id = :id"), + {"id": str(row.list_item_id)}, + ) + li = result.first() + assert li.content == f"ConvLI-{tag}" + assert str(li.list_id) == seed_list["id"] + + # Cleanup + await db_session.execute(text("DELETE FROM daily_focus WHERE id = :id"), {"id": fid}) + await db_session.execute( + text("DELETE FROM list_items WHERE id = :id"), {"id": str(row.list_item_id)} + ) + await db_session.commit() + + @pytest.mark.asyncio + async def test_convert_nonexistent_item_redirects( + self, client: AsyncClient, + ): + """Converting a nonexistent focus item redirects to /focus.""" + fake_id = str(uuid.uuid4()) + r = await client.post(f"/focus/{fake_id}/convert-to-task", follow_redirects=False) + assert r.status_code == 303 + + @pytest.mark.asyncio + async def test_convert_task_linked_item_redirects( + self, client: AsyncClient, all_seeds: dict, + ): + """Converting a task-linked focus item (no title) redirects without action.""" + r = await client.post(f"/focus/{all_seeds['focus']}/convert-to-note", follow_redirects=False) + assert r.status_code == 303 + + # =========================================================================== # Entity Create with task_id/meeting_id # ===========================================================================