12 KiB
Life OS - Development Status & Continuation Guide (Test Infrastructure - Convo Test1)
Last Updated: 2026-03-01 Current State: Test suite deployed, introspection verified (121 routes discovered), first test run pending GitHub: mdombaugh/lifeos-dev (main branch)
1. What Was Built in This Conversation
Dynamic Introspection-Based Test Suite (DEPLOYED)
Built and deployed an automated test suite that discovers routes from the live FastAPI app at runtime. Zero hardcoded routes. When a new router is added, smoke and CRUD tests auto-expand on next run.
Architecture (11 files in /opt/lifeos/dev/tests/):
| File | Purpose | Lines |
|---|---|---|
| introspect.py | Route discovery engine: walks app.routes, extracts paths/methods/Form() fields/path params, classifies routes | 357 |
| form_factory.py | Generates valid POST form data from introspected Form() signatures + seed data UUIDs | 195 |
| registry.py | Imports app, runs introspection once, exposes route registry + PREFIX_TO_SEED mapping + resolve_path() | 79 |
| conftest.py | Fixtures only: test DB engine, per-test rollback session, httpx client, 15 seed data fixtures, all_seeds composite | 274 |
| test_smoke_dynamic.py | 3 parametrized functions expanding to ~59 tests: all GETs (no params) return 200, all GETs (with seed ID) return 200, all detail/edit GETs (fake UUID) return 404 | 100 |
| test_crud_dynamic.py | 5 parametrized functions expanding to ~62 tests: all POST create/edit/delete redirect 303, all actions non-500, create-then-verify-in-list | 161 |
| test_business_logic.py | 16 hand-written tests: timer single-run constraint, stop sets end_at, soft delete/restore visibility, search SQL injection, sidebar integrity, focus/capture workflows, edge cases | 212 |
| route_report.py | CLI tool: dumps all discovered routes with classification, form fields, seed mapping coverage | 65 |
| run_tests.sh | Test runner with aliases: smoke, crud, logic, report, fast, full, custom args | 22 |
| init.py | Package marker | 0 |
| pytest.ini | Config: asyncio_mode=auto, verbose output, short tracebacks | 7 |
Key design decisions:
registry.pyseparated fromconftest.pyto avoid pytest auto-loading conflicts (test files import from registry, not conftest)- Form() detection uses
__class__.__name__check, notissubclass(), because FastAPI'sFormis a function not a class - Test DB schema cloned from live dev DB via pg_dump (not from stale SQL files)
- Seed data uses raw SQL INSERT matching actual table columns
Introspection Verification Results
Deploy script step 4 confirmed:
Routes discovered: 121
GET (no params): 36
GET (with params): 23
POST create: 13
POST edit: 13
POST delete: 17
POST action: 19
Entity prefixes: 32
What Was NOT Done
- First test run not yet executed -- introspection works, tests deployed, but
run_tests.shhas not been run yet - Seed data column mismatches likely -- seed INSERTs written from architecture docs, not actual table inspection. First run will surface these as SQL errors
- No test for file upload routes -- file routes skipped (has_file_upload flag) because they need multipart handling
2. Test Infrastructure Inventory
2.1 Database
| Component | Details |
|---|---|
| Test DB | lifeos_test on lifeos-db container |
| Schema source | Cloned from lifeos_dev via pg_dump --schema-only |
| Tables | 48 (matches dev) |
| Isolation | Per-test transaction rollback (no data persists between tests) |
| Credentials | Same as dev: postgres:UCTOQDZiUhN8U |
2.2 How Introspection Works
registry.pyimportsmain.app(sets DATABASE_URL to test DB first)introspect.pywalksapp.routes, for eachAPIRoute:- Extracts path, HTTP methods, endpoint function reference
- Parses
{id}path parameters via regex - Inspects endpoint function signature for
Form()parameters (checksdefault.__class__.__name__for "FieldInfo") - Extracts query parameters (non-Form, non-Depends, non-Request params)
- Classifies route: list / detail / create_form / edit_form / create / edit / delete / toggle / action / json / page
- Builds
ROUTE_REGISTRYdict keyed by kind (get_no_params, post_create, etc.) and by prefix - Test files parametrize from this registry at collection time
2.3 How Dynamic Tests Work
Smoke (test_smoke_dynamic.py):
@pytest.mark.parametrize("path", [r.path for r in GET_NO_PARAMS])
async def test_get_no_params_returns_200(client, path):
r = await client.get(path)
assert r.status_code == 200
N discovered GET routes = N smoke tests. No manual updates.
CRUD (test_crud_dynamic.py):
- Collects all POST create/edit/delete routes from registry
- Calls
build_form_data(route.form_fields, all_seeds)to generate valid payloads form_factory.pyresolves FK fields to seed UUIDs, generates values by field name pattern- Asserts 303 redirect for create/edit/delete, non-500 for actions
Business Logic (test_business_logic.py):
- Hand-written, tests behavioral contracts not discoverable via introspection
- Timer: single running constraint, stop sets end_at, /time/running returns JSON
- Soft deletes: deleted task hidden from list, restore reappears
- Search: SQL injection doesn't crash, empty query works, unicode works
- Sidebar: domain appears on every page, project hierarchy renders
- Focus/capture: add to focus, multi-line capture creates multiple items
- Edge cases: invalid UUID, timer without task_id, double delete
2.4 Seed Data Fixtures (15 entities)
| Fixture | Table | Dependencies | Key Fields |
|---|---|---|---|
| seed_domain | domains | none | id, name, color |
| seed_area | areas | seed_domain | id, domain_id |
| seed_project | projects | seed_domain, seed_area | id, domain_id, area_id |
| seed_task | tasks | seed_domain, seed_project | id, domain_id, project_id, title |
| seed_contact | contacts | none | id, first_name, last_name |
| seed_note | notes | seed_domain | id, domain_id, title |
| seed_meeting | meetings | none | id, title, meeting_date |
| seed_decision | decisions | seed_domain, seed_project | id, title |
| seed_appointment | appointments | seed_domain | id, title, start_at, end_at |
| seed_weblink_folder | weblink_folders | none | id, name |
| seed_list | lists | seed_domain, seed_project | id, name |
| seed_link | links | seed_domain | id, title, url |
| seed_weblink | weblinks | seed_weblink_folder | id, title, url |
| seed_capture | capture | none | id, raw_text |
| seed_focus | daily_focus | seed_task | id, task_id |
2.5 PREFIX_TO_SEED Mapping
Maps route prefixes to seed fixture keys so resolve_path() can replace {id} with real UUIDs:
/domains -> domain /contacts -> contact
/areas -> area /meetings -> meeting
/projects -> project /decisions -> decision
/tasks -> task /appointments -> appointment
/notes -> note /weblinks -> weblink
/links -> link /weblinks/folders -> weblink_folder
/lists -> list /focus -> focus
/capture -> capture /time -> task
/files -> None (skipped) /admin/trash -> None (skipped)
3. File Locations on Server
/opt/lifeos/dev/
tests/
__init__.py
introspect.py # Route discovery engine
form_factory.py # Form data generation
registry.py # Route registry + PREFIX_TO_SEED + resolve_path
conftest.py # Fixtures (DB, client, seeds)
route_report.py # CLI route dump
test_smoke_dynamic.py # Auto-parametrized GET tests
test_crud_dynamic.py # Auto-parametrized POST tests
test_business_logic.py # Hand-written behavioral tests
run_tests.sh # Test runner
pytest.ini # pytest config
deploy-tests.sh # Deployment script (can re-run to reset)
4. Known Issues & Expected First-Run Failures
4.1 Likely Seed Data Mismatches
Seed INSERT statements were written from architecture docs, not from inspecting actual table columns. The first test run will likely produce errors like:
column "X" of relation "Y" does not exist-- seed INSERT has a column the actual table doesn't havenull value in column "X" violates not-null constraint-- seed INSERT is missing a required column
Fix process: Run tests, read the SQL errors, adjust the INSERT in conftest.py to match actual columns (query with \d table_name), redeploy.
4.2 Possible Form Field Discovery Gaps
Some routers may use patterns the introspection engine doesn't handle:
Annotated[str, Form()]style (handled via__metadata__check, but untested against live code)- Form fields with non-standard defaults
- Routes that accept both Form and query params
The route report (run_tests.sh report) will show warnings for POST create/edit routes with zero discovered Form fields. Those need investigation.
4.3 Route Classification Edge Cases
Some routes may be misclassified:
- Admin trash restore routes (
/admin/trash/restore/{entity}/{id}) may not match the standard patterns - Capture routes (
/capture/add,/capture/{id}/convert,/capture/{id}/dismiss) use non-standard action patterns - Focus routes (
/focus/add,/focus/{id}/remove) are action routes, not standard CRUD
These will show up as action route tests (non-500 assertion) rather than typed CRUD tests.
4.4 Not Yet Pushed to GitHub
Test files need to be committed: cd /opt/lifeos/dev && git add . && git commit -m "Dynamic test suite" && git push origin main
5. How to Continue (Convo Test2)
Immediate Next Steps
- Run the route report to verify introspection output:
docker exec lifeos-dev bash /app/tests/run_tests.sh report - Run smoke tests first (most likely to pass):
docker exec lifeos-dev bash /app/tests/run_tests.sh smoke - Fix seed data failures by inspecting actual tables and adjusting conftest.py INSERTs
- Run CRUD tests after seeds are fixed:
docker exec lifeos-dev bash /app/tests/run_tests.sh crud - Run business logic tests last:
docker exec lifeos-dev bash /app/tests/run_tests.sh logic - Run full suite once individual categories pass:
docker exec lifeos-dev bash /app/tests/run_tests.sh
When Adding a New Entity Router
- Add seed fixture to
conftest.py(INSERT matching actual table columns) - Add entry to
PREFIX_TO_SEEDinregistry.py - Run tests -- smoke and CRUD auto-expand to cover new routes
- Add behavioral tests to
test_business_logic.pyif entity has constraints or state machines
When Schema Changes
Re-run deploy-tests.sh (step 1 drops and recreates lifeos_test from current dev schema).
Or manually:
docker exec lifeos-db psql -U postgres -c "DROP DATABASE IF EXISTS lifeos_test;"
docker exec lifeos-db psql -U postgres -c "CREATE DATABASE lifeos_test;"
docker exec lifeos-db pg_dump -U postgres -d lifeos_dev --schema-only -f /tmp/s.sql
docker exec lifeos-db psql -U postgres -d lifeos_test -f /tmp/s.sql -q
Test Runner Commands
docker exec lifeos-dev bash /app/tests/run_tests.sh # Full suite
docker exec lifeos-dev bash /app/tests/run_tests.sh report # Route introspection dump
docker exec lifeos-dev bash /app/tests/run_tests.sh smoke # All GET endpoints
docker exec lifeos-dev bash /app/tests/run_tests.sh crud # All POST create/edit/delete
docker exec lifeos-dev bash /app/tests/run_tests.sh logic # Business logic
docker exec lifeos-dev bash /app/tests/run_tests.sh fast # Smoke, stop on first fail
docker exec lifeos-dev bash /app/tests/run_tests.sh -k "timer" # pytest keyword filter
6. Application Development Remaining (Unchanged from Convo 4)
Tier 3 Remaining (4 features)
- Processes / process_runs -- Most complex. 4 tables. Template CRUD, run instantiation, step completion, task generation.
- Calendar view -- Unified read-only view of appointments + meetings + tasks.
- Time budgets -- Simple CRUD: domain_id + weekly_hours + effective_from.
- Eisenhower matrix -- Derived 2x2 grid from task priority + due_date.
Tier 4, UX Polish, Production Deployment
See lifeos-development-status-convo4.md sections 5, 8, 9.