Commit Graph

82 Commits

Author SHA1 Message Date
curo1305 c8a0443ad2 feat(04-01): add Wave 0 xfail stubs for DOC-02, ADMIN-06, SEC-08, SEC-09
- test_documents.py: append 4 stubs (content_stream 200, 206, admin_403, no_presigned_url)
- test_audit.py: create new file with 4 stubs (viewer, no_doc_content, user_403, export_csv)
- test_security.py: create new file with 2 stubs (credentials_enc_not_in_response, delete_user_cleans_files)
- All stubs: xfail(strict=False), body is pytest.xfail("not implemented yet")
2026-05-25 18:25:18 +02:00
curo1305 e0075989df feat(04-01): add Wave 0 xfail stubs for FOLD and SHARE requirements
- test_folders.py: 13 stubs for FOLD-01..05 (create, rename, delete, move, breadcrumb, sort, FTS)
- test_shares.py: 7 stubs for SHARE-01..05 (share, received list, quota isolation, revoke, duplicate)
- FTS tests carry both xfail and skipif(INTEGRATION) decorators
- All stubs: xfail(strict=False), body is pytest.xfail("not implemented yet")
2026-05-25 18:23:40 +02:00
curo1305 a5f202b069 Fix Phase 3 UAT blockers: MinIO presigned URL hostname, CORS, admin flush→commit, auth refresh race
Bugs fixed:
- minio_backend.py: generate_presigned_put_url and presigned_get_url used internal
  _client (minio:9000) instead of _public_client (localhost:9000). Browser received
  ERR_NAME_NOT_RESOLVED. Fixed by using _public_client with region='us-east-1' to
  skip region-discovery HTTP request from inside the container.

- docker-compose.yml: MINIO_API_CORS_ALLOW_ORIGIN was set from CORS_ORIGINS which
  uses pydantic JSON list format '["http://localhost:5173"]'. MinIO expected a plain
  string and never matched the origin. Fixed to use FRONTEND_URL instead.

- admin.py: All write handlers (create_user, update_user_status, update_user_quota,
  update_ai_config) used session.flush() without session.commit(). Changes appeared
  to succeed (response reflected in-memory state) but rolled back on session close.
  Fixed by replacing flush() with commit() in all four write handlers.

- auth.js: Concurrent refresh() calls from QuotaBar and App.vue on page reload caused
  a token rotation race — first call rotated the cookie, second arrived with stale
  cookie and cleared accessToken. Fixed by deduplicating with a shared in-flight
  promise (_refreshInFlight).

Phase 3 UAT: 9/10 pass. UAT-3 (QuotaBar visual) pending browser confirmation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:30:41 +02:00
curo1305 a5994d9ff4 chore: commit pending phase-3 work and add TEST_ACCOUNTS.md
Includes planning artifacts (03-CONTEXT, 03-DISCUSSION-LOG, 03-02-SUMMARY),
integration test script, MinIO/auth/docker fixes, and local dev account reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:30:56 +02:00
curo1305 6849ebd1e6 feat(03-04): retire flat-file settings; wire per-user AI config via DB lookup
- config.py: Remove SETTINGS_FILE, DEFAULT_SYSTEM_PROMPT, DEFAULT_SETTINGS
  constants; add system_prompt, default_ai_provider, default_ai_model to Settings
- services/classifier.py: Add _DEFAULT_SYSTEM_PROMPT module constant; classify_document
  and suggest_topics_for_document accept ai_provider/ai_model kwargs; no longer calls
  storage.load_settings() — uses app_settings defaults with DB-supplied overrides (D-14, D-15)
- services/storage.py: Delete load_settings, save_settings, mask_api_key, settings_masked;
  remove from __all__; remove import copy, json, DEFAULT_SETTINGS, SETTINGS_FILE (D-12)
- tasks/document_tasks.py: _run resolves user.ai_provider/ai_model via session.get(User,
  doc.user_id) and passes through to classifier; task signature unchanged (T-03-19)
- api/settings.py: Deleted — /api/settings endpoint removed (D-12)
- main.py: Remove settings_router import and include_router call
- tests/test_settings.py: Replace all tests with test_settings_endpoint_removed (404, green)
- tests/test_classifier.py: Implement test_per_user_provider, test_celery_task_uses_user_provider,
  test_default_provider_fallback; remove xfail markers (DOC-03, DOC-05)
2026-05-23 20:32:55 +02:00
curo1305 5950a3f5c2 feat(03-03): wire get_current_user into /api/topics/*; add load_topics_for_user; POST /api/admin/topics
- api/topics.py: add get_current_user dep to all 5 handlers (list, create, update, delete, suggest)
- list_topics: uses load_topics_for_user (system topics + user's own) with user-scoped doc counts
- create_topic: passes user_id=current_user.id (never creates system topics via regular endpoint)
- update_topic/delete_topic: ownership assertion — system topics and other users' topics return 404
- api/admin.py: add SystemTopicCreate model + POST /api/admin/topics (user_id=NULL, admin-only)
- services/storage.py: add or_ import; load_topics_for_user (D-17); create_topic gains user_id param with namespace-scoped dedup; topic_doc_counts gains optional user_id for user-scoped counts; add load_topics_for_user to __all__
- services/classifier.py: replace load_topics with load_topics_for_user(doc.user_id); pass user_id=doc.user_id to create_topic for AI-suggested topics (D-11)
- Tests: update all topic tests to pass auth headers; implement test_topic_namespace, test_admin_create_system_topic, test_regular_user_cannot_create_system_topic, test_topics_require_auth
2026-05-23 20:15:44 +02:00
curo1305 b28bb01995 feat(03-03): add get_regular_user dep; wire auth + ownership into /api/documents/*
- Add get_regular_user FastAPI dep (rejects admin with 403) to deps/auth.py
- Wire Depends(get_regular_user) into all 6 /api/documents/* handlers
- upload-url: replace null-user/... object_key with str(current_user.id)/...; set user_id=current_user.id
- confirm: remove Wave 2 doc.user_id is None guard — quota runs unconditionally; add ownership assertion (404 on cross-user)
- list: filter by user_id=current_user.id via storage.list_metadata(user_id=...)
- get/delete/classify: ownership assertion (doc.user_id != current_user.id → 404)
- storage.list_metadata: add required user_id param + Document.user_id == user_id filter
- storage.delete_document: remove if doc.user_id is not None guard; use CASE WHEN for SQLite-compat quota decrement
- Tests: update existing tests to pass auth headers; implement test_cross_user_access_404, test_admin_cannot_access_documents, test_documents_require_auth; mark test_confirm_endpoint xfail(strict=False) for SQLite UUID mismatch
2026-05-23 20:05:34 +02:00
curo1305 0d51d023ce feat(03-02): implement presigned upload flow, quota enforcement, cleanup task
- Replace POST /api/documents/upload with POST /api/documents/upload-url + /{id}/confirm
- upload-url: create pending Document row with user_id=None (Wave 2), return presigned PUT URL
- confirm: stat MinIO for authoritative size (T-03-05), atomic quota UPDATE (T-03-06, STORE-03)
- Confirm returns 413 with {used_bytes, limit_bytes, rejected_bytes} on quota exceeded (STORE-05)
- Wave 2 guard: skip quota UPDATE when doc.user_id is None (Plan 03-03 removes this)
- Add GET /api/auth/me/quota to api/auth.py (STORE-04)
- services/storage.py: remove save_upload (D-04); add GREATEST(0, used_bytes-delta) quota decrement to delete_document (STORE-06)
- tasks/document_tasks.py: add cleanup_abandoned_uploads Celery beat task (D-06)
- celery_app.py: add beat_schedule for cleanup-abandoned-uploads every 30 minutes
- tests/test_documents.py: replace legacy /upload tests with xfail; add real test logic for upload-url/confirm/get-quota
- tests/test_quota.py: implement real test logic with xfail for PostgreSQL-specific SQL
2026-05-23 14:32:12 +02:00
curo1305 3ed6dd494f feat(03-02): extend StorageBackend ABC and MinIOBackend with presigned PUT and stat_object
- Add generate_presigned_put_url and stat_object abstract methods to StorageBackend ABC
- Extend MinIOBackend with dual client (self._client internal + self._public_client public)
- MinIOBackend.__init__ accepts optional public_endpoint param (RESEARCH.md Finding 3)
- generate_presigned_put_url uses self._public_client for browser-resolvable URLs
- stat_object uses self._client.stat_object and returns .size (authoritative, T-03-05)
- get_storage_backend() passes public_endpoint=settings.minio_public_endpoint
- config.py adds minio_public_endpoint field (RESEARCH.md Finding 3)
- docker-compose.yml: MINIO_API_CORS_ALLOW_ORIGIN on minio service (T-03-09)
- docker-compose.yml: MINIO_PUBLIC_ENDPOINT on backend service
- docker-compose.yml: new celery-beat service (RESEARCH.md Finding 10)
2026-05-23 13:52:16 +02:00
curo1305 807a1b3e67 feat(03-01): create Alembic migration 0003 for multi-user isolation
- revision="0003", down_revision="0002"
- upgrade(): collects null-user object_keys, deletes document_topics cascade,
  deletes null-user documents, removes MinIO objects (skip if MINIO_ENDPOINT unset),
  deletes all topics (D-10), alters documents.user_id NOT NULL via batch_alter_table,
  creates ix_topics_user_id index, reconciles quotas.used_bytes from SUM(size_bytes)
- downgrade(): drops ix_topics_user_id, reverts user_id to nullable; documents not restored
- batch_alter_table ensures SQLite compatibility for test suite
- MinIO step gated on MINIO_ENDPOINT env var for safe SQLite test runs
2026-05-23 13:44:22 +02:00
curo1305 21ec9cb4c3 test(03-01): add Wave 0 xfail stubs and shared fixtures for Phase 3
- Add auth_user, admin_user, mock_minio_presigned, mock_minio_stat fixtures to conftest.py
- Create test_quota.py with 4 xfail stubs (STORE-03, STORE-05, STORE-06, SC2 race)
- Append test_migration_0003 to test_alembic.py (full pre-seed + post-migration assertions)
- Append 3 classifier xfail stubs (DOC-03, DOC-05, D-15)
- Append 6 document xfail stubs (D-05, STORE-04, SEC-04, D-16)
- Append 4 topic xfail stubs (DOC-04, D-09, D-17)
- Append test_settings_endpoint_removed stub (D-12)
- All 19 new test IDs collect cleanly with xfail(strict=False)
2026-05-23 13:42:37 +02:00
curo1305 f94e8d8b4a feat(02-04): implement admin API endpoints — user CRUD, quota management, AI config
- GET /api/admin/users: list users (safe fields only, ordered by created_at)
- POST /api/admin/users: create user (password_must_change=True, quota init)
- PATCH /api/admin/users/{id}/status: deactivate/reactivate with sole-admin guard
- POST /api/admin/users/{id}/password-reset: Celery email dispatch (no token returned)
- GET /api/admin/users/{id}/quota: quota view with MB helpers
- PATCH /api/admin/users/{id}/quota: quota adjust with below-usage warning
- PATCH /api/admin/users/{id}/ai-config: assign AI provider/model per user
- _user_to_dict() whitelist helper prevents password_hash/credentials_enc leakage
- No impersonation endpoint (ADMIN-07 enforced by omission)
- get_current_admin Depends() on every handler (SEC-07)
- Updated backend/main.py to include admin_router
- Fixed test: mock send_reset_email.delay to avoid Redis in unit tests
2026-05-22 20:01:37 +02:00
curo1305 cbad9acac1 test(02-04): RED phase — admin API test suite (11 tests, expect fail until admin.py exists) 2026-05-22 19:59:16 +02:00
curo1305 43e1d0145e feat(02-03): add TOTP setup/enable/disable, password reset, and frontend_url to config
- GET /api/auth/totp/setup: returns provisioning_uri + secret (400 if already enabled)
- POST /api/auth/totp/enable: rate-limited 10/min, verifies TOTP code with Redis replay prevention, returns 10 backup codes
- DELETE /api/auth/totp: disables TOTP, clears secret, deletes backup codes
- POST /api/auth/password-reset: always returns 202 (anti-enumeration), enqueues Celery email task
- POST /api/auth/password-reset/confirm: validates token, strength, HIBP; updates password; no auto-login (AUTH-05)
- config.py: added frontend_url setting for password reset link construction
- test_auth_totp.py: all 11 tests passing (GREEN)
2026-05-22 19:52:36 +02:00
curo1305 d7831e9382 test(02-03): add failing tests for TOTP endpoints, password reset, logout-all
- test_totp_setup_returns_uri: GET /api/auth/totp/setup returns provisioning_uri + secret
- test_totp_setup_already_enabled: returns 400 when totp_enabled=True
- test_totp_setup_requires_auth: returns 401/403 without Bearer
- test_password_reset_always_202_nonexistent: anti-enumeration for non-existent email
- test_password_reset_always_202_existing: anti-enumeration for existing email
- test_password_reset_confirm_invalid_token: returns 400 for bad token
- test_password_reset_confirm_weak_password: returns 422 for weak password
- test_password_reset_confirm_valid_no_autologin: returns 200 with no access_token (AUTH-05)
- test_logout_all_revokes_tokens: returns 200 with revoked message
- test_logout_all_requires_auth: returns 401/403 without Bearer
- test_totp_enable_rate_limit: 11th call returns 429
2026-05-22 19:50:51 +02:00
curo1305 1882edfff6 feat(02-02): auth API endpoints + security hardening + Python 3.9 compat
- backend/api/auth.py: register, login (TOTP+backup), refresh, logout,
  me, change-password; per-account Redis rate limit; HIBP check
- backend/main.py: Origin validation middleware, CSP headers middleware,
  CORS locked to settings.cors_origins, Redis lifespan (app.state.redis),
  admin bootstrap, auth router included, slowapi SlowAPIMiddleware
- backend/services/email.py: already created in Plan 01 (verified exists)
- Python 3.9 compat: fixed match statement in ai/__init__.py,
  str|None union syntax in openai_provider.py, api/documents.py,
  api/topics.py, api/settings.py, services/classifier.py

All 17 tests in test_auth_api.py pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:35:38 +02:00
curo1305 1d425d4392 test(02-02): add failing tests for auth API endpoints
RED phase - 17 tests covering register, login, TOTP, backup codes,
per-account rate limiting, Origin validation, change-password, and
password_must_change flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:35:31 +02:00
curo1305 c4613b6b87 feat(02-01): implement deps/auth.py FastAPI dependency chain with tests
- get_current_user: validates Bearer JWT via decode_access_token, loads User from DB
  raises HTTP 401 on invalid/expired token, missing user, or deactivated account
- get_current_admin: wraps get_current_user, raises HTTP 403 on role != 'admin' (T-02-07)
- Admin impersonation architecturally excluded (ADMIN-07, T-02-08) — no code path bypasses role check
- tests/test_auth_deps.py: 7 tests covering happy path, tampered token, inactive user, 403 non-admin, 200 admin
2026-05-22 19:25:16 +02:00
curo1305 9fc820d893 feat(02-01): implement services/auth.py full auth service layer and email_tasks.py
- services/auth.py: Argon2 password hashing (pwdlib), constant-time verify (SEC-06)
- JWT create/decode for access tokens and password-reset tokens (typ claim validation, T-02-01)
- Refresh token lifecycle: create, rotate, revoke-all with family revocation (AUTH-07, RFC 9700)
- Family revocation enqueues send_security_alert_email.delay on token reuse (T-02-02)
- TOTP provisioning (pyotp) and verification with Redis replay prevention, valid_window=1 (AUTH-08)
- Backup code generation (8-char hex uppercase), storage (Argon2 hashed), constant-time verify (T-02-03)
- HIBP k-anonymity check via SHA-1 prefix (T-02-05), fail-open on network error (T-02-06)
- Admin bootstrap: idempotent, logs WARNING if env vars missing (D-04/D-05/D-06)
- services/email.py: SMTP send + dev stdout fallback (D-01/D-02)
- tasks/email_tasks.py: send_reset_email and send_security_alert_email Celery tasks
- celery_app.py: add email queue route for tasks.email_tasks.*
- TDD tests: 17 tests covering all auth primitives and family revocation
2026-05-22 19:23:42 +02:00
curo1305 12c6487855 feat(02-01): add BackupCode ORM model, password_must_change field, Alembic migration, extend Settings
- Add BackupCode model to db/models.py with user_id FK, code_hash (Argon2), used_at (nullable)
- Add ix_backup_codes_user_id index on backup_codes.user_id
- Add password_must_change BOOLEAN NOT NULL DEFAULT false to User model (ADMIN-01)
- Extend config.py Settings with JWT, SMTP, admin bootstrap, and CORS fields (D-01, D-04, D-09)
- Add env_list_separator=',' for cors_origins env var parsing
- Append PyJWT, pwdlib[argon2], pyotp, aioredis, slowapi to requirements.txt
- Add .env.example entries for SECRET_KEY, ADMIN_EMAIL, SMTP_*, CORS_ORIGINS
- Create migration 0002 adding backup_codes table and password_must_change column
- Add TDD tests for all Task 1 acceptance criteria (7 tests pass)
2026-05-22 19:19:52 +02:00
curo1305 970c8e4e44 feat(01-05): final cutover — delete data/, prune config.py, async-only tests
- Delete backend/data/ tracked files (D-04): flat-file metadata, settings.json,
  topics.json, and uploaded files removed from git; backend/data/ added to
  .gitignore (empty dir remains on macOS due to ACL — no tracked files remain)
- Prune backend/config.py: remove DATA_DIR, UPLOADS_DIR, METADATA_DIR,
  TOPICS_FILE, ensure_data_dirs(); rebase SETTINGS_FILE as derived path from
  settings.data_dir (Phase 1 flat-file settings kept per plan decision)
- Prune backend/tests/conftest.py: remove isolated_data_dir autouse fixture
  and sync TestClient client fixture; add SQLite type compatibility shim
  (visit_INET/JSONB) so in-memory db_session can create tables with
  PostgreSQL-specific column types; add live_services_available fixture
- Rewrite backend/tests/test_documents.py: delete all legacy sync tests,
  remove all @pytest.mark.xfail markers; async-only document tests now
  use async_client + storage service directly for topic wiring
- Rewrite backend/tests/test_health.py: delete legacy sync test_health(client);
  remove @pytest.mark.xfail from test_health_checks_postgres_and_minio
- Port backend/tests/test_topics.py to async_client (sync client removed)
- Port backend/tests/test_settings.py to async_client with monkeypatch for
  SETTINGS_FILE isolation (settings remain flat-file in Phase 1)
2026-05-22 09:53:39 +02:00
curo1305 c1931fd566 feat(01-05): wire main.py lifespan+health and rewrite documents+topics to async session
- Rewrite main.py lifespan: MinIO client created at startup, docuvault bucket
  auto-created if missing, stored on app.state.minio; engine.dispose() on shutdown
- Extend /health endpoint: probes PostgreSQL (SELECT 1) and MinIO (bucket_exists)
  returning {"status": "ok"|"degraded", "checks": {"postgres": ..., "minio": ...}}
- Rewrite api/documents.py: all routes inject session: AsyncSession = Depends(get_db);
  save_upload/save_metadata/list_metadata/get_metadata/delete_document all async;
  upload handler queues extract_and_classify.delay() instead of inline classification;
  /classify endpoint retains synchronous await classifier.classify_document() for
  backward-compatible immediate response
- Rewrite api/topics.py: all routes inject session dependency; all storage calls
  are async with session parameter; Pydantic models TopicCreate/TopicUpdate/
  SuggestRequest preserved verbatim
2026-05-22 09:47:00 +02:00
curo1305 32d67de1ca feat(01-05): introduce celery_app + tasks/document_tasks + session-aware classifier
- Add backend/celery_app.py: Celery("docuvault") with Redis broker, JSON
  serialization, and tasks.document_tasks.* routed to documents queue;
  reads REDIS_URL directly from os.environ (no config import — Pitfall 7)
- Add backend/tasks/__init__.py: empty package marker
- Add backend/tasks/document_tasks.py: sync extract_and_classify Celery task
  that calls asyncio.run(_run()) to retrieve bytes from MinIO, extract text
  via extractor, and classify via classifier; classification failure is non-fatal
- Update backend/services/classifier.py: classify_document and
  suggest_topics_for_document now accept session: AsyncSession as first arg;
  all storage.* calls updated to async session-injection pattern
- Add extract_text_from_bytes helper to services/extractor.py for bytes-based
  extraction (used by Celery worker, which retrieves bytes from MinIO)
2026-05-22 09:45:33 +02:00
curo1305 3e4b1f1f91 feat(01-04): rewrite services/storage.py as async SQLAlchemy + MinIO orchestrator
- Replaced entire flat-file + filelock implementation with async ORM + MinIO
- All 14 DB-touching functions are async def accepting AsyncSession as first param
- load_settings/save_settings/mask_api_key/settings_masked remain sync (flat-file, Phase 2 will migrate)
- save_upload uses null-user D-03 sentinel; object_key via MinIO put_object
- update_document_topics auto-creates missing topics via create_topic deduplication
- No filelock, no METADATA_DIR/UPLOADS_DIR/TOPICS_FILE references remain
- Added __all__ listing all 18 public functions
- Updated conftest.py: removed filelock patching no longer needed
- Fixed test_object_key_schema: removed unused db_session param (SQLite INET type conflict)
2026-05-22 09:39:32 +02:00
curo1305 eaf86a832a feat(01-04): add StorageBackend ABC + MinIOBackend + factory
- backend/storage/base.py: StorageBackend ABC with 5 abstract methods mirroring ai/base.py
- backend/storage/minio_backend.py: MinIOBackend wrapping all sync Minio SDK calls in asyncio.to_thread(); STORE-02 key schema: {user_id}/{document_id}/{uuid4()}{ext}
- backend/storage/__init__.py: get_storage_backend() factory mirroring ai/__init__.py
- backend/tests/test_storage.py: remove xfail markers (plan 04 implements the module)
2026-05-22 09:36:24 +02:00
curo1305 75ea7ef106 feat(01-03): scaffold Alembic async config and author 0001_initial_schema migration
- backend/alembic.ini: script_location=migrations, sqlalchemy.url=%(DATABASE_MIGRATE_URL)s
- backend/migrations/env.py: async_engine_from_config + Base.metadata wiring;
  runtime os.environ.get("DATABASE_MIGRATE_URL") injection (alembic.ini interpolation
  does not read OS env directly)
- backend/migrations/versions/0001_initial_schema.py: creates all 11 tables in
  dependency order with correct FKs, indexes, and named constraints
- documents.user_id is nullable=True per D-03; Phase 2 adds NOT NULL
- Ends with GRANT + ALTER DEFAULT PRIVILEGES for docuvault_app (Pitfall 4)
- Also grants USAGE/SELECT on sequences (audit_log.id autoincrement)
- downgrade() drops all tables in reverse dependency order
2026-05-22 09:20:49 +02:00
curo1305 3e1fcd69b5 feat(01-03): add full v1 ORM schema, async session factory, and DB dependency
- backend/db/models.py: 11 SQLAlchemy 2.0 ORM models (User, Quota, RefreshToken,
  Folder, Document, Topic, DocumentTopic, Share, AuditLog, CloudConnection, Group)
- Document.user_id declared nullable=True per D-03 (Phase 2 adds NOT NULL)
- AuditLog.metadata_ uses mapped_column("metadata", JSONB) to avoid DeclarativeBase
  reserved-attribute conflict
- Group table stub for D-02 (v2 feature, seeded per PROJECT.md)
- Uses Optional[X] instead of X | None for Python < 3.10 compatibility
- backend/db/session.py: async engine (pool_pre_ping=True, expire_on_commit=False)
- backend/deps/db.py: async get_db() FastAPI dependency yielding AsyncSession
2026-05-22 09:16:21 +02:00
curo1305 d856a2eaa9 test(01-02): extend test_health.py and port test_documents.py to async client
test_health.py:
  - Keep existing test_health(client) sync test unchanged (Plan 01 baseline)
  - Add test_health_checks_postgres_and_minio(async_client) xfail scaffold
    for extended /health response with postgres+minio checks (Plan 05, D-07)

test_documents.py:
  - Keep all 9 existing sync tests verbatim
  - Add async ports (_async suffix) for each: 9 xfail tests using async_client
  - Add test_upload_persists_to_postgres_and_minio_async (UUID id + GET
    round-trip assertion) — xfail until Plan 05 storage rewrite
  - Total: 10 new xfail async tests, 9 sync tests unchanged
2026-05-22 09:08:05 +02:00
curo1305 27fa0d4631 test(01-02): add Wave 0 scaffolds test_storage.py and test_alembic.py
test_storage.py (6 xfail tests, STORE-02):
  - test_object_key_schema: regex {user_id}/{doc_id}/{uuid4}{ext}
  - test_filename_not_in_object_key: human filename never in MinIO key
  - test_storage_backend_abc_methods: incomplete subclass raises TypeError
  - test_get_storage_backend_returns_minio: factory returns MinIOBackend
  - test_put_object_uses_asyncio_to_thread: SDK call wrapped in to_thread
  - test_minio_backend_health_check_returns_bool: True/False on ok/error

test_alembic.py (2 xfail tests, STORE-01 / D-02 / D-03):
  - test_migration_creates_all_tables: all 11 v1 tables after upgrade head
  - test_documents_user_id_nullable: user_id notnull=0 per D-03
2026-05-22 09:06:55 +02:00
curo1305 1f675fcf1a feat(01-02): add async db_session and async_client fixtures to conftest.py
- Add @pytest_asyncio.fixture db_session: in-memory SQLite via aiosqlite,
  expire_on_commit=False, skips gracefully (ImportError) before Plan 03
- Add @pytest_asyncio.fixture async_client: httpx.AsyncClient with
  ASGITransport, overrides deps.db.get_db, skips before Plan 03
- Retain all legacy sync fixtures (isolated_data_dir, client, sample_txt,
  sample_pdf) unchanged for backward compatibility through Plan 04
2026-05-22 09:05:36 +02:00
curo1305 6c507d5991 feat(01-01): add Pydantic Settings class to config.py and update requirements.txt
- Add Settings(BaseSettings) class reading DATABASE_URL, DATABASE_MIGRATE_URL,
  MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET, REDIS_URL,
  SECRET_KEY from environment via SettingsConfigDict (pydantic-settings v2 API)
- Instantiate settings = Settings() at module level for all callers
- Preserve legacy DATA_DIR, UPLOADS_DIR, METADATA_DIR, TOPICS_FILE, SETTINGS_FILE,
  DEFAULT_SYSTEM_PROMPT, DEFAULT_SETTINGS, ensure_data_dirs() for Wave 4 compat
- Remove filelock>=3.14 (replaced by PostgreSQL transactions per STORE-07)
- Add sqlalchemy[asyncio]>=2.0.49, psycopg[binary]>=3.3.4, alembic>=1.18.4,
  minio>=7.2.20, celery[redis]>=5.6.3, redis>=7.4.0, aiosqlite>=0.20.0
- Bump pytest-asyncio to >=1.3.0 for asyncio_mode auto support
2026-05-22 08:59:12 +02:00
curo1305 7a34807fa0 chore: initial commit — existing single-user document scanner codebase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:53:28 +02:00