Merge remote dev branch

This commit is contained in:
Alex Blank
2026-05-29 12:55:03 +02:00
6 changed files with 690 additions and 24 deletions
@@ -0,0 +1,156 @@
# PR-2 Apply Report: Backend Integration for Notification Center
## Status: COMPLETE
All 5 tasks for PR-2 (NC-PR2-001 through NC-PR2-005) have been implemented, tested, and validated.
## What Was Implemented
### NC-PR2-001: Wire lifecycle_hooks.py to NotificationService
**File:** `apps/api/src/services/lifecycle_hooks.py`
- Imported `notification_service` singleton from `src.services.notification_service`
- Added `_derive_title(event_type)` helper mapping lifecycle events to human-readable titles:
- `instance.created` → "Container created"
- `instance.started` → "Container started"
- `instance.stopped` → "Container stopped"
- `instance.restarted` → "Container restarted"
- `instance.deleted` → "Container deleted"
- `instance.error` → "Container error"
- After `event_bus.publish(...)`, calls `notification_service.create_notification(...)` with:
- `user_id = instance.owner_id`
- `category = "instance"`
- `severity = "error"` for `instance.error`, `"info"` for all others
- `source_type = "tool_instances"`, `source_id = instance.id`
- Wrapped in `try/except`; logs failure with `correlation_id` and continues
- Event bus publish and audit row insert are unaffected by notification failure
### NC-PR2-002: Wire health_monitor.py to NotificationService
**File:** `apps/api/src/services/health_monitor.py`
- Imported `notification_service` singleton
- After `self._event_bus.publish(event_type, payload)`, calls `notification_service.create_notification(...)` with:
- `user_id = instance.owner_id`
- `category = "instance"` for `new_status == "error"`
- `category = "health"` for `instance.health_changed`
- `severity` mapped:
- `"error"` for crash
- `"warning"` for unhealthy
- `"info"` for recovery (running)
- `title` mapped:
- "Container failed" for error
- "Container unhealthy" for unhealthy
- "Container recovered" for running
- Wrapped in `try/except`; logs failure with `correlation_id` and continues
- Original event bus publish and health check insert are unaffected
### NC-PR2-003: Extend UserConfig schema for notification preferences
**File:** `apps/api/src/api/user_config.py`
- Added `notification_mute_categories: list[str] | None = None` to `UserConfigResponse`
- Added `notification_toast_level: str | None = None` to `UserConfigResponse`
- Added the same fields to `UserConfigUpdate`
- Existing config keys are unaffected; new fields are optional with `None` defaults
### NC-PR2-004: Event producer integration tests (RED)
**File:** `apps/api/tests/integration/test_notifications_lifecycle.py` *(new)*
6 integration tests covering:
1. `test_lifecycle_event_creates_notification` — lifecycle hook `instance.started` creates `severity="info"` notification for owner
2. `test_health_monitor_error_creates_notification` — simulated crash creates `severity="error"` notification for owner
3. `test_notification_failure_does_not_block_event_pipeline` — mocked `create_notification` raising `RuntimeError`; event still published, no exception escapes
4. `test_notification_ownership_matches_instance_owner` — notification `user_id` equals `instance.owner_id`, not the API caller
5. `test_lifecycle_error_creates_error_notification``instance.error` maps to `severity="error"`, title="Container error"
6. `test_health_monitor_unhealthy_creates_warning_notification` — tunnel failure creating `severity="warning"`, category="health"
### NC-PR2-005: Verify producer tests and clean up (GREEN / REFACTOR)
- All 6 new integration tests pass
- 13 unit tests for `NotificationService` pass (no regressions)
- 10 integration tests for notifications API pass (no regressions)
- 6 existing health monitor unit tests pass (no regressions)
- 6 existing event integration tests pass (no regressions)
- `ruff check` passes on all modified files
## TDD Cycle Evidence
| Cycle | Task | Test File | RED | GREEN | Evidence |
|-------|------|-----------|-----|-------|----------|
| 1 | NC-PR2-004 (producer flow) | `tests/integration/test_notifications_lifecycle.py` | 4 tests written against unwired producers | Wired `lifecycle_hooks.py` and `health_monitor.py` | 4 passed |
| 2 | NC-PR2-004 (severity mapping) | `tests/integration/test_notifications_lifecycle.py` | Added error + warning severity tests | Already green from implementation | 6 passed |
| 3 | NC-PR2-003 (UserConfig schema) | `src/api/user_config.py` | Schema extended with new optional fields | PATCH/GET endpoints validate correctly | Verified manually |
| 4 | NC-PR2-005 (REFACTOR) | All files | — | ruff clean, no regressions across 41 related tests | All pass |
## Changed Files
1. `apps/api/src/services/lifecycle_hooks.py` — Wired `NotificationService` after event bus publish
2. `apps/api/src/services/health_monitor.py` — Wired `NotificationService` after state change event publish
3. `apps/api/src/api/user_config.py` — Added `notification_mute_categories` and `notification_toast_level` to Pydantic schemas
4. `apps/api/tests/integration/test_notifications_lifecycle.py` *(new)* — 6 integration tests for event-to-notification flow
## Test Commands & Exit Codes
```bash
# New integration tests for event producers (6 tests)
cd apps/api && python -m pytest tests/integration/test_notifications_lifecycle.py -v
# Exit: 0 — 6 passed
# NotificationService unit tests (no regressions)
cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
# Exit: 0 — 13 passed
# Notifications API integration tests (no regressions)
cd apps/api && python -m pytest tests/integration/test_notifications_api.py -v
# Exit: 0 — 10 passed
# Health monitor unit tests (no regressions)
cd apps/api && python -m pytest tests/unit/test_health_monitor.py -v
# Exit: 0 — 6 passed
# Event integration tests (no regressions)
cd apps/api && python -m pytest tests/integration/test_events.py -v
# Exit: 0 — 6 passed
# Combined relevant test suite
cd apps/api && python -m pytest \
tests/unit/test_notification_service.py \
tests/integration/test_notifications_api.py \
tests/integration/test_notifications_lifecycle.py \
tests/unit/test_health_monitor.py \
tests/integration/test_events.py \
-v
# Exit: 0 — 41 passed
# Ruff linting on all modified files
cd apps/api && python -m ruff check \
src/services/lifecycle_hooks.py \
src/services/health_monitor.py \
src/api/user_config.py \
tests/integration/test_notifications_lifecycle.py
# Exit: 0 — All checks passed
```
## Deviations from Design
None. All mappings and behaviors match the design spec (section 1.3) and task requirements exactly.
## Surprises / Decisions
1. **Health monitor `test_health_monitor_unhealthy_creates_warning_notification` required `public_url`:** The health monitor only checks tunnel health when `instance.public_url` is truthy. Without setting it on the test fixture instance, `_derive_status` returned `"running"` instead of `"unhealthy"`, which created an `"info"` notification. Fixed by setting `test_instance.public_url` in the test before calling `_check_instance`.
2. **Patch target for failure test:** The `test_notification_failure_does_not_block_event_pipeline` patches `src.services.lifecycle_hooks.notification_service.create_notification`. This only works because `lifecycle_hooks.py` imports `notification_service` at module level, making the attribute resolvable by `unittest.mock.patch`.
3. **No schema migration needed for UserConfig:** Preferences are stored in the existing JSON `config` blob, consistent with the existing pattern (theme, editor, git identity). No Alembic migration required.
## Risks
- **None:** All changes are additive. Event producers use `try/except` so notification failures cannot block the event pipeline. No existing test regressions introduced.
## PR Boundary
This PR covers PR-2 only (NC-PR2-001 through NC-PR2-005). PR-3 (frontend core) and PR-4 (toast coordination) are out of scope.
@@ -1,6 +1,6 @@
# Apply Progress: PR-1 Backend Core for Notification Center
# Apply Progress: Notification Center
## TDD Cycle Evidence
## TDD Cycle Evidence (PR-1)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|-------|------|-----------|-----|-------|----------|
@@ -10,8 +10,18 @@
| 4 | NC-PR1-008 (API edge cases) | `tests/integration/test_notifications_api.py` | Already included in cycle 3 | Pagination, 404 ownership, mute categories at API layer | Same 10 tests pass |
| 5 | NC-PR1-010 (REFACTOR) | All files | — | ruff clean, no regressions | `ruff check` passes on all new files; existing unit tests 223 passed (4 pre-existing failures unrelated) |
## TDD Cycle Evidence (PR-2)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|-------|------|-----------|-----|-------|----------|
| 1 | NC-PR2-004 (producer flow) | `tests/integration/test_notifications_lifecycle.py` | 4 tests written against unwired producers | Wired `lifecycle_hooks.py` and `health_monitor.py` | 4 passed |
| 2 | NC-PR2-004 (severity mapping) | `tests/integration/test_notifications_lifecycle.py` | Added error + warning severity tests | Already green from implementation | 6 passed |
| 3 | NC-PR2-003 (UserConfig schema) | `src/api/user_config.py` | Schema extended with new optional fields | PATCH/GET endpoints validate correctly | Verified manually |
| 4 | NC-PR2-005 (REFACTOR) | All files | — | ruff clean, no regressions across 41 related tests | All pass |
## Completed Tasks
### PR-1: Backend Core
- [x] NC-PR1-001: Alembic migration for `notifications` table
- [x] NC-PR1-002: SQLAlchemy `Notification` model (`apps/api/src/models/notification.py`)
- [x] NC-PR1-003: Export `Notification` in `models/__init__.py`
@@ -24,8 +34,16 @@
- [x] NC-PR1-010: Register router in `main.py` + import `Notification` for Alembic
- [x] NC-PR1-011: Code quality pass — ruff, test regressions, smoke tests (REFACTOR)
### PR-2: Backend Integration
- [x] NC-PR2-001: Wire `lifecycle_hooks.py` to `NotificationService`
- [x] NC-PR2-002: Wire `health_monitor.py` to `NotificationService`
- [x] NC-PR2-003: Extend `UserConfig` schema for notification preferences
- [x] NC-PR2-004: Event producer integration tests (RED)
- [x] NC-PR2-005: Verify producer tests pass and clean up (GREEN / REFACTOR)
## Files Changed
### PR-1 Files
1. `apps/api/alembic/versions/2026_05_29_add_notifications_table.py` *(new)* — Alembic migration
2. `apps/api/src/models/notification.py` *(new)* — SQLAlchemy model
3. `apps/api/src/models/__init__.py` — Export `Notification`
@@ -37,8 +55,15 @@
9. `apps/api/tests/integration/test_notifications_api.py` *(new)* — 10 integration tests
10. `apps/api/tests/integration/test_models.py` — Updated expected tables list
### PR-2 Files
11. `apps/api/src/services/lifecycle_hooks.py` — Wired `NotificationService` after event bus publish
12. `apps/api/src/services/health_monitor.py` — Wired `NotificationService` after state change event publish
13. `apps/api/src/api/user_config.py` — Added `notification_mute_categories` and `notification_toast_level` to Pydantic schemas
14. `apps/api/tests/integration/test_notifications_lifecycle.py` *(new)* — 6 integration tests for event-to-notification flow
## Test Commands & Exit Codes
### PR-1
```bash
# Unit tests for NotificationService (13 tests)
cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
@@ -48,48 +73,78 @@ cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
cd apps/api && python -m pytest tests/integration/test_notifications_api.py -v
# Exit: 0 — 10 passed
# Combined new tests
cd apps/api && python -m pytest tests/unit/test_notification_service.py tests/integration/test_notifications_api.py -v
# Exit: 0 — 23 passed
# Existing unit tests (no regressions in our code)
cd apps/api && python -m pytest tests/unit/ -v
# Exit: 1 — 223 passed, 4 failed (pre-existing failures in test_config.py and test_git_repository_clone_preflight.py)
```
# Ruff linting on all new/modified files
cd apps/api && python -m ruff check \
src/models/notification.py \
src/models/__init__.py \
src/services/notification_service.py \
src/api/notifications.py \
src/api/__init__.py \
src/main.py \
alembic/versions/2026_05_29_add_notifications_table.py \
### PR-2
```bash
# New integration tests for event producers (6 tests)
cd apps/api && python -m pytest tests/integration/test_notifications_lifecycle.py -v
# Exit: 0 — 6 passed
# NotificationService unit tests (no regressions)
cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
# Exit: 0 — 13 passed
# Notifications API integration tests (no regressions)
cd apps/api && python -m pytest tests/integration/test_notifications_api.py -v
# Exit: 0 — 10 passed
# Health monitor unit tests (no regressions)
cd apps/api && python -m pytest tests/unit/test_health_monitor.py -v
# Exit: 0 — 6 passed
# Event integration tests (no regressions)
cd apps/api && python -m pytest tests/integration/test_events.py -v
# Exit: 0 — 6 passed
# Combined relevant test suite (41 tests)
cd apps/api && python -m pytest \
tests/unit/test_notification_service.py \
tests/integration/test_notifications_api.py \
tests/integration/test_models.py
# Exit: 0 — All checks passed
tests/integration/test_notifications_lifecycle.py \
tests/unit/test_health_monitor.py \
tests/integration/test_events.py \
-v
# Exit: 0 — 41 passed
# Smoke tests
health: 200
notifications unauth: 401
# Ruff linting on all PR-2 modified files
cd apps/api && python -m ruff check \
src/services/lifecycle_hooks.py \
src/services/health_monitor.py \
src/api/user_config.py \
tests/integration/test_notifications_lifecycle.py
# Exit: 0 — All checks passed
```
## Deviations from Design
### PR-1
- **SQLAlchemy `metadata` column name conflict:** `Base.metadata` is reserved by SQLAlchemy DeclarativeBase. Used `notification_metadata` as the Python attribute name with DB column name `"metadata"`. In the Pydantic response model, used `Field(serialization_alias="metadata")` so the JSON API still exposes `metadata` as specified.
- **`created_at` type in Pydantic:** Used `datetime` instead of `str` to leverage FastAPI's automatic ISO serialization.
### PR-2
- None. All mappings and behaviors match the design spec (section 1.3) and task requirements exactly.
## Surprises / Decisions
### PR-1
1. **SQLite `func.now()` resolution:** `test_list_notifications_orders_by_created_at_desc` failed because multiple rapid INSERTs got identical timestamps. Fixed by explicitly setting `created_at` offsets in the test after creation.
2. **Pre-existing integration test failures:** ~40 integration tests fail due to missing `asyncpg` module and direct PostgreSQL connection attempts in their custom setup code. These are unrelated to our changes.
3. **Pre-existing `test_models.py` outdated:** The `test_expected_tables_are_registered` assertion had a hardcoded set missing many newer tables. Updated it to include all current tables (including `notifications`).
### PR-2
1. **Health monitor `test_health_monitor_unhealthy_creates_warning_notification` required `public_url`:** The health monitor only checks tunnel health when `instance.public_url` is truthy. Without setting it on the test fixture instance, `_derive_status` returned `"running"` instead of `"unhealthy"`, which created an `"info"` notification. Fixed by setting `test_instance.public_url` in the test before calling `_check_instance`.
2. **Patch target for failure test:** The `test_notification_failure_does_not_block_event_pipeline` patches `src.services.lifecycle_hooks.notification_service.create_notification`. This only works because `lifecycle_hooks.py` imports `notification_service` at module level, making the attribute resolvable by `unittest.mock.patch`.
3. **No schema migration needed for UserConfig:** Preferences are stored in the existing JSON `config` blob, consistent with the existing pattern (theme, editor, git identity). No Alembic migration required.
## Remaining Tasks
None — PR-1 is complete.
- [ ] PR-3: Frontend Core (NC-PR3-001 through NC-PR3-012)
- [ ] PR-4: Toast Coordination (NC-PR4-001 through NC-PR4-006)
## PR Boundary
This PR covers PR-1 only (NC-PR1-001 through NC-PR1-011). PR-2 (backend integration) and PR-3/PR-4 (frontend) are out of scope.
This progress covers PR-1 and PR-2. PR-3 (frontend core — NotificationProvider, useNotifications, NotificationCenter, NotificationItem, styles, AppShell integration) and PR-4 (toast coordination — EventToastBridge preferences, settings UI) are out of scope.