diff --git a/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md b/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md index 23b7c5c..a017a8e 100644 --- a/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md +++ b/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md @@ -1,11 +1,11 @@ --- phase: 3 slug: document-migration-multi-user-isolation -status: partial +status: compliant nyquist_compliant: true wave_0_complete: true created: 2026-05-23 -audited: 2026-05-31 +audited: 2026-06-01 --- # Phase 3 — Validation Strategy @@ -43,6 +43,7 @@ audited: 2026-05-31 | Quota exceeded response | 02 | 2 | STORE-05 | STORE-05 | 413 with {used_bytes, limit_bytes, rejected_bytes} | unit | `pytest tests/test_quota.py::test_quota_exceeded_response -x` | ✅ | ✅ green | | Upload-url endpoint | 02 | 2 | D-05 | SEC-04 | Creates pending Document row + returns presigned URL | unit | `pytest tests/test_documents.py::test_upload_url_endpoint -x` | ✅ | ✅ green | | Confirm endpoint | 02 | 2 | D-05 | STORE-03 | stat_object size used, status=uploaded set | unit | `pytest tests/test_documents.py::test_confirm_endpoint -x` | ✅ | ✅ green | +| Atomic quota decrement | 02 | 2 | STORE-06 | STORE-06 | DELETE decrements used_bytes via CASE WHEN; no underflow | unit | `pytest tests/test_quota.py::test_delete_decrements_quota -x` | ✅ | ✅ green | | Quota bar endpoint | 02 | 2 | STORE-04 | — | GET /api/me/quota returns {used_bytes, limit_bytes} | unit | `pytest tests/test_documents.py::test_get_quota -x` | ✅ | ✅ green | | Cross-user access | 03 | 3 | SEC-04 | SEC-04 | Cross-user document access returns 404 | unit | `pytest tests/test_documents.py::test_cross_user_access_404 -x` | ✅ | ✅ green | | Admin 403 on documents | 03 | 3 | SEC-04 (SC4) | SEC-04 | Admin JWT on /api/documents/* returns 403 | unit | `pytest tests/test_documents.py::test_admin_cannot_access_documents -x` | ✅ | ✅ green | @@ -69,7 +70,6 @@ audited: 2026-05-31 | Migration null-user cleanup | 01 | D-02 | Alembic upgrade requires a real PostgreSQL instance + migration scripts | `INTEGRATION=1 pytest tests/test_alembic.py::test_migration_0003 -x` against real DB | | Quota reconciliation | 01 | D-03 | Same migration test; verifies used_bytes matches SUM(size_bytes) post-migration | `INTEGRATION=1 pytest tests/test_alembic.py::test_migration_0003 -x` against real DB | | Concurrent quota race | 02 | STORE-03 SC2 | PostgreSQL row-level locking required; SQLite cannot emulate concurrent atomic UPDATE | `INTEGRATION=1 pytest tests/test_quota.py::test_concurrent_quota_race -x` against PostgreSQL | -| Atomic quota decrement | 02 | STORE-06 | SQLite UUID format mismatch in GREATEST() WHERE clause; real PostgreSQL required | `INTEGRATION=1 pytest tests/test_quota.py::test_delete_decrements_quota -x` against PostgreSQL | --- @@ -101,3 +101,22 @@ audited: 2026-05-31 - Removed stale `xfail` markers from `test_quota_increment_atomic` and `test_quota_exceeded_response` (both XPASS → now clean PASSED) - Added 4 unlisted-but-passing tests to task map: `test_default_provider_fallback` (D-15), `test_documents_require_auth` (D-16), `test_admin_create_system_topic` (D-09), `test_regular_user_cannot_create_system_topic` (D-09) - Moved 4 infrastructure-blocked tasks to Manual-Only: migration null-user cleanup, quota reconciliation, concurrent quota race, atomic quota decrement + +--- + +## Validation Audit 2026-06-01 + +| Metric | Count | +|--------|-------| +| Gaps found (PARTIAL) | 3 | +| Root-cause fixes applied | 2 (api/documents.py + services/storage.py) | +| Resolved (now green) | 4 (confirm_endpoint, quota_increment_atomic, quota_exceeded_response, delete_decrements_quota) | +| Promoted from manual-only | 1 (test_delete_decrements_quota — STORE-06) | +| Still manual-only | 3 (migration cleanup, quota reconciliation, concurrent quota race) | + +**Changes made:** +- Fixed `api/documents.py` `confirm_upload`: changed `str(doc.user_id)` → `doc.user_id.hex` in both raw SQL parameter dicts — SQLite stores UUID as 32-char hex (no dashes); `str(uuid)` was 36-char dashed format causing `WHERE user_id = :uid` to never match +- Fixed `services/storage.py` `delete_document`: same `str(doc.user_id)` → `doc.user_id.hex` fix for quota decrement SQL +- Removed stale `xfail` from `test_delete_decrements_quota` — now passes cleanly on SQLite after fix +- Promoted `test_delete_decrements_quota` (STORE-06) from Manual-Only to Per-Task Verification Map +- Suite result: 53 passed, 3 skipped, 8 xfailed, 0 failed