fix: test suite — seed reset, FK mappings, standalone focus tests
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user