Commit prior to Claude Code implementation on VM

This commit is contained in:
2026-03-01 14:45:15 +00:00
parent a1d24354a0
commit f7c5ac2d89
14 changed files with 21711 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
"""
Business Logic Tests
====================
Hand-written tests for specific behavioral contracts.
These test LOGIC, not routes, so they stay manual.
When to add tests here:
- New constraint (e.g. "only one timer running at a time")
- State transitions (e.g. "completing a task sets completed_at")
- Cross-entity effects (e.g. "deleting a project hides its tasks")
- Search behavior
- Sidebar data integrity
"""
from __future__ import annotations
import uuid
from datetime import date, datetime, timezone
import pytest
from httpx import AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
# ===========================================================================
# Time Tracking
# ===========================================================================
class TestTimerConstraints:
"""Only one timer can run at a time. Starting a new one auto-stops the old."""
@pytest.mark.asyncio
async def test_single_timer_constraint(
self, client: AsyncClient, db_session: AsyncSession,
seed_domain: dict, seed_project: dict,
):
t1 = await _create_task(db_session, seed_domain["id"], seed_project["id"], "Timer1")
t2 = await _create_task(db_session, seed_domain["id"], seed_project["id"], "Timer2")
await client.post("/time/start", data={"task_id": t1}, follow_redirects=False)
await client.post("/time/start", data={"task_id": t2}, follow_redirects=False)
result = await db_session.execute(
text("SELECT count(*) FROM time_entries WHERE end_at IS NULL")
)
assert result.scalar() <= 1
@pytest.mark.asyncio
async def test_stop_sets_end_at(
self, client: AsyncClient, db_session: AsyncSession, seed_task: dict,
):
await client.post("/time/start", data={"task_id": seed_task["id"]}, follow_redirects=False)
await client.post("/time/stop", follow_redirects=False)
result = await db_session.execute(
text("SELECT count(*) FROM time_entries WHERE end_at IS NULL AND task_id = :tid"),
{"tid": seed_task["id"]},
)
assert result.scalar() == 0
@pytest.mark.asyncio
async def test_running_endpoint_returns_json(
self, client: AsyncClient, seed_task: dict,
):
await client.post("/time/start", data={"task_id": seed_task["id"]}, follow_redirects=False)
r = await client.get("/time/running")
assert r.status_code == 200
# Should be valid JSON
data = r.json()
assert data is not None
# ===========================================================================
# Soft Delete & Restore
# ===========================================================================
class TestSoftDeleteBehavior:
"""Soft-deleted items should vanish from lists and reappear after restore."""
@pytest.mark.asyncio
async def test_deleted_task_hidden_from_list(
self, client: AsyncClient, seed_task: dict,
):
await client.post(f"/tasks/{seed_task['id']}/delete", follow_redirects=False)
r = await client.get("/tasks/")
assert seed_task["title"] not in r.text
@pytest.mark.asyncio
async def test_restore_task_reappears(
self, client: AsyncClient, seed_task: dict,
):
await client.post(f"/tasks/{seed_task['id']}/delete", follow_redirects=False)
await client.post(
f"/admin/trash/restore/tasks/{seed_task['id']}",
follow_redirects=False,
)
r = await client.get("/tasks/")
assert seed_task["title"] in r.text
@pytest.mark.asyncio
async def test_deleted_project_hidden(
self, client: AsyncClient, seed_project: dict,
):
await client.post(f"/projects/{seed_project['id']}/delete", follow_redirects=False)
r = await client.get("/projects/")
assert seed_project["name"] not in r.text
# ===========================================================================
# Search
# ===========================================================================
class TestSearchBehavior:
@pytest.mark.asyncio
async def test_search_does_not_crash_on_sql_injection(self, client: AsyncClient):
r = await client.get("/search/?q='; DROP TABLE tasks; --")
assert r.status_code == 200
@pytest.mark.asyncio
async def test_search_empty_query(self, client: AsyncClient):
r = await client.get("/search/?q=")
assert r.status_code == 200
@pytest.mark.asyncio
async def test_search_special_unicode(self, client: AsyncClient):
r = await client.get("/search/?q=日本語テスト")
assert r.status_code == 200
# ===========================================================================
# Sidebar
# ===========================================================================
class TestSidebarIntegrity:
@pytest.mark.asyncio
async def test_sidebar_shows_domain_on_every_page(
self, client: AsyncClient, seed_domain: dict,
):
"""Domain should appear in sidebar across all pages."""
# Sample a few different page types
for path in ("/", "/tasks/", "/notes/", "/projects/"):
r = await client.get(path)
assert seed_domain["name"] in r.text, f"Domain missing from sidebar on {path}"
@pytest.mark.asyncio
async def test_sidebar_shows_project_hierarchy(
self, client: AsyncClient, seed_domain: dict, seed_area: dict, seed_project: dict,
):
r = await client.get("/")
assert seed_project["name"] in r.text
# ===========================================================================
# Focus & Capture Workflows
# ===========================================================================
class TestFocusWorkflow:
@pytest.mark.asyncio
async def test_add_and_remove_from_focus(
self, client: AsyncClient, db_session: AsyncSession, seed_task: dict,
):
# Add to focus
r = await client.post("/focus/add", data={"task_id": seed_task["id"]}, follow_redirects=False)
assert r.status_code in (303, 302)
@pytest.mark.asyncio
async def test_capture_multi_line_creates_multiple(
self, client: AsyncClient, db_session: AsyncSession,
):
await client.post(
"/capture/add",
data={"raw_text": "Line one\nLine two\nLine three"},
follow_redirects=False,
)
result = await db_session.execute(
text("SELECT count(*) FROM capture WHERE is_deleted = false")
)
count = result.scalar()
# Should have created at least 2 items (3 lines)
assert count >= 2, f"Expected multiple capture items, got {count}"
# ===========================================================================
# Edge Cases
# ===========================================================================
class TestEdgeCases:
@pytest.mark.asyncio
async def test_invalid_uuid_in_path(self, client: AsyncClient):
r = await client.get("/tasks/not-a-valid-uuid")
assert r.status_code in (404, 422, 400)
@pytest.mark.asyncio
async def test_timer_start_without_task_id(self, client: AsyncClient):
r = await client.post("/time/start", data={}, follow_redirects=False)
assert r.status_code != 200 # Should error, not silently succeed
@pytest.mark.asyncio
async def test_double_delete_doesnt_crash(
self, client: AsyncClient, seed_task: dict,
):
await client.post(f"/tasks/{seed_task['id']}/delete", follow_redirects=False)
r = await client.post(f"/tasks/{seed_task['id']}/delete", follow_redirects=False)
assert r.status_code in (303, 302, 404)
# ===========================================================================
# Helpers
# ===========================================================================
async def _create_task(db: AsyncSession, domain_id: str, project_id: str, title: str) -> str:
_id = str(uuid.uuid4())
await db.execute(
text("INSERT INTO tasks (id, domain_id, project_id, title, status, priority, sort_order, is_deleted, created_at, updated_at) "
"VALUES (:id, :did, :pid, :title, 'open', 3, 0, false, now(), now())"),
{"id": _id, "did": domain_id, "pid": project_id, "title": title},
)
await db.flush()
return _id