diff --git a/CHANGELOG.md b/CHANGELOG.md index d56647a1f..a3d31f7aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Annual-planning Playwright coverage now boots isolated local frontend/backend servers on `127.0.0.1:3100` and `127.0.0.1:8010`, so the review-flow slice no longer depends on whatever developer servers are already running on `3000/8000`. - Annual shock preview now groups the full repair-block blast radius around the direct absence hits, so coordinators can see which other scheduled assignments sit in the same repair block before generating or publishing repair work. - Annual-planning readiness now evaluates effective program policy snapshots, recurring program calendar anchors, and policy-layered institutional events together, so coordinators see the full DB-backed policy stack instead of only institutional-event coverage before annual optimization or repair work. - The annual-planning hub's policy rollover action now refreshes program policy snapshots and recurring calendar anchors alongside institutional events, keeping academic-year policy carry-forward aligned across all current DB-backed policy sources. @@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added current mocked browser coverage for the annual-planning review surface, so Playwright now verifies effective program policy, recurring calendar anchors, institutional events, plan provenance, repair preview, and plan history together instead of the older policy-anchor-only copy. - Added admin rollover endpoints for `ProgramPolicySnapshot` and `ProgramCalendarAnchor`, plus annual-planning hub views for the effective program-policy snapshot and effective recurring calendar anchors for a selected academic year. - Added a recent proving-pass report panel to the annual-planning hub so schedulers can review baseline/shock/repair drill outcomes for the selected academic year without leaving the coordinator surface. - Added a scheduler-only annual-planner proving-pass report feed sourced from the native `docs/reports/automation/annual_proving_pass_*.json` artifacts, so recent baseline/repair drill outcomes can be surfaced in the app instead of staying trapped in local files. @@ -38,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added annual-planning hub clone/delete controls for unpublished scenarios, so coordinators can branch year-level what-if plans and clean up stale drafts without dropping to raw API calls. - Added annual-plan provenance and repair-scope details to the annual-planning hub, so coordinators can see who created, approved, or published a plan and what exact repair slice a draft is carrying before acting on it. - Added annual-plan history timelines to the planning hub, so coordinators can review lifecycle and repair-scope events on a selected plan before approving, publishing, or repairing from it. -- Added a mocked Playwright annual-planning review-flow test that verifies policy anchors, plan provenance, and plan history render together in the coordinator hub. +- Added a mocked Playwright annual-planning review-flow test that verifies the current policy-layer review surface, plan provenance, and plan history render together in the coordinator hub. - Annual shock preview now returns and displays the exact resident/block assignments that a selected set of approved or confirmed absences would unlock, so coordinators can review the real blast radius before creating a repair draft. - Annual plan proving-pass coverage now includes exact resident/block repair scope, repair-plan and repair-draft publish flow, and a full scripted proving pass that validates the localized shock-repair path before coordinators use it manually. - Added a `verify_hybrid_runtime.py` report tool plus `HybridRuntimeVerificationService` so operators can compare published hybrid outpatient template policy against actual generated HDAs, including weekly patterns, scope-adjusted targets, source breakdown, and slot-grid output for real pilot blocks. diff --git a/TODO.md b/TODO.md index 4f819c904..202ddd9dd 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO — Actionable Items -> **Updated:** 2026-03-21 (policy-layer readiness/review/rollover actualization in progress) +> **Updated:** 2026-03-21 (policy-layer and annual-planning browser-confidence actualization in progress) > **Source:** Extracted from architecture docs, planning docs, code TODOs, and Explore agent audit. > **Companion:** `docs/planning/ROADMAP.md` (macro vision), `docs/planning/TECHNICAL_DEBT.md` (debt tracker) > **Cutover tracker:** `docs/planning/ROTATION_SHAPE_CUTOVER_STATUS_20260320.md` (merged vs open PR vs untouched rotation-shape/constraint cutover work) @@ -26,6 +26,7 @@ - [x] **Faculty weekly shapes** — Primary operational blocker from `docs/reviews/FULL_STACK_AUDIT_20260320.md`. Replaced heuristic-first faculty scheduling with baseline weekly shapes, role drivers, person deltas, and week-specific overrides across merged PRs [#1412](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1412) through [#1419](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1419). - [ ] **Program / regulatory / institution policy tables** — `ProgramPolicySnapshot`, `ProgramCalendarAnchor`, and policy-layered `InstitutionalEvent` tables/admin surfaces now exist, and the annual-planning hub can now assess, review, and roll all three sources together. Remaining: move the surviving higher-order policy still trapped in Python into these DB-backed layers without making ad hoc scheduling-logic changes. - [ ] **13-block AY 26-27 draft / validate / publish workflow** — Core lifecycle is merged on `main` via [#1425](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1425) through [#1428](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1428). Current hardening stack is [#1452](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1452) through [#1457](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1457), plus [#1468](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1468), [#1484](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1484), and [#1485](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1485): publish aliases, naive UTC timestamps, idempotent block-draft regeneration, annual optimization leave pressure, resilience-helper proving-pass fixes, and exact shock-impact assignment review. Native proving passes now reach baseline publish, repair publish, and repair-draft publish. Remaining: merge the open stack and keep tightening annual diff/review ergonomics. +- [x] **Annual-planning browser confidence** — The mocked coordinator review-flow slice now runs on dedicated Playwright frontend/backend ports (`127.0.0.1:3100` / `127.0.0.1:8010`) instead of ambient `3000/8000` dev servers, and the browser spec is aligned with the current policy-layer review surface (effective program policy, calendar anchors, institutional events, provenance, repair preview, and plan history). Remaining GUI coverage work should build on this isolated path instead of reusing whatever local servers happen to be running. - [ ] **Shock-event model + targeted regeneration** — Initial `Absence`-driven shock preview/draft slice is merged on `main`. Current extensions are [#1457](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1457), [#1468](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1468), and [#1485](https://github.com/Euda1mon1a/Autonomous-Assignment-Program-Manager/pull/1485), which feed approved/confirmed `Absence` rows into annual optimization as leave requests, persist shock repair scope even when the annual rotation diff is unchanged, keep repair generation from aborting on archived combo templates, legacy encrypted absence fields, or fully absent repair residents, and expose the exact resident/block assignments affected by a shock before draft creation. This branch extends that review surface with block-scoped blast-radius context showing the full repair block alongside the direct absence hits. Remaining: finish the operator-facing repair publish flow and any downstream review/reporting gaps. ### Annual Rotation Optimizer (ARO) — Frontend UI @@ -37,7 +38,7 @@ - [x] **ARO service layer** — create_plan → import_leave → optimize → approve → publish (PR #1276) - [x] **ARO API routes** — REST endpoints under `/api/v1/annual-planner/plans/...` (PR #1276) - [x] **ARO rotation mapping** — `publish_plan()` resolves rotation_template_id + conflict handling (PR #1276) -- [x] **ARO Frontend UI** — `/hub/annual-planning` exists on `main` and now covers create / optimize / validate / approve / publish, shock preview / draft, diff review, and annual workbook export. Remaining improvements live under the north-star workflow items above. +- [x] **ARO Frontend UI** — `/hub/annual-planning` exists on `main` and now covers create / optimize / validate / approve / publish, shock preview / draft, diff review, annual workbook export, policy-layer review, and browser-verified review/history surfaces. Remaining improvements live under the north-star workflow items above. - **Doc:** `docs/architecture/ANNUAL_ROTATION_OPTIMIZER.md` ### Excel Pipeline — Stateful Roundtrip diff --git a/docs/planning/ANNUAL_PLANNING_BROWSER_CONFIDENCE_20260321.md b/docs/planning/ANNUAL_PLANNING_BROWSER_CONFIDENCE_20260321.md new file mode 100644 index 000000000..3abfe4597 --- /dev/null +++ b/docs/planning/ANNUAL_PLANNING_BROWSER_CONFIDENCE_20260321.md @@ -0,0 +1,56 @@ +# Annual Planning Browser Confidence — 2026-03-21 + +## Objective + +Make the annual-planning review-flow browser proof reliable on a real local machine without depending on ambient frontend/backend dev servers. + +## Problem + +The mocked annual-planning Playwright slice was reusing whatever happened to be running on `localhost:3000` and `localhost:8000`. + +That was brittle for two reasons: + +1. the browser flow could silently talk to a developer's unrelated Next.js session +2. admin bootstrap could fail if the reused backend was started from the wrong working directory and never loaded `backend/.env` + +The concrete symptom on this machine was `POST /api/v1/auth/initialize-admin -> 500` while the database itself was already at Alembic head. + +## Implementation + +1. Added a shared Playwright local-dev env helper that primes dedicated defaults: + - `PLAYWRIGHT_BASE_URL=http://127.0.0.1:3100` + - `PLAYWRIGHT_API_URL=http://127.0.0.1:8010` + - `BACKEND_URL` follows the Playwright API URL unless explicitly provided +2. Updated both `frontend/playwright.config.ts` and `frontend/e2e/playwright.config.ts` to: + - launch Next.js on `3100` + - launch FastAPI on `8010` + - pass `BACKEND_URL` into the frontend server command +3. Updated `frontend/e2e/utils/local-dev-auth.ts` to use the shared Playwright API default and to spawn any fallback backend on the API URL's port instead of hardcoding `8000`. +4. Updated the annual-planning browser spec so it matches the current review surface: + - effective program policy + - effective calendar anchors + - effective institutional events + - plan provenance + - repair preview + - plan history + +## Result + +- The annual-planning Playwright slice no longer depends on ambient `3000/8000` developer servers. +- The old admin-bootstrap `500` is gone in the targeted browser run. +- The mocked review-flow browser proof now validates the current policy-layer UI rather than stale "policy anchors" copy. + +## Validation + +- `npx playwright test e2e/tests/annual-planning.spec.ts --project=chromium --grep 'Annual Planning review surfaces'` +- `npm run type-check` +- `npx eslint playwright.config.ts e2e/playwright.config.ts e2e/utils/local-dev-env.ts e2e/utils/local-dev-auth.ts e2e/tests/annual-planning.spec.ts` +- `npm test -- --watchAll=false src/app/hub/annual-planning/__tests__/AnnualPlanningHubClient.test.tsx` + +## Files + +- `frontend/playwright.config.ts` +- `frontend/e2e/playwright.config.ts` +- `frontend/e2e/utils/local-dev-env.ts` +- `frontend/e2e/utils/local-dev-auth.ts` +- `frontend/e2e/tests/annual-planning.spec.ts` diff --git a/docs/planning/TECHNICAL_DEBT.md b/docs/planning/TECHNICAL_DEBT.md index 5db4c4424..7d392ef57 100644 --- a/docs/planning/TECHNICAL_DEBT.md +++ b/docs/planning/TECHNICAL_DEBT.md @@ -338,7 +338,7 @@ command: celery -A app.core.celery_app worker -Q default,resilience,notification **Found:** 2026-03-04 (Codex GPT-5 full-stack assessment) **Status:** ✅ RESOLVED (2026-03-06, branch `fix/playwright-port-conflict`) -**Resolution:** Unique ports assigned: root 3001, CI 3002, E2E 3003. Fixed hardcoded localhost:3000 in test files. +**Resolution:** Playwright now uses dedicated local frontend/backend ports for the annual-planning review slice (`127.0.0.1:3100` and `127.0.0.1:8010`) instead of ambient `3000/8000` dev servers, and the review-flow spec no longer relies on whichever backend a developer already has running. --- diff --git a/frontend/e2e/playwright.config.ts b/frontend/e2e/playwright.config.ts index f3b906c99..4fa961fee 100644 --- a/frontend/e2e/playwright.config.ts +++ b/frontend/e2e/playwright.config.ts @@ -1,5 +1,11 @@ import { defineConfig, devices } from '@playwright/test'; +import { primePlaywrightLocalDevEnv } from './utils/local-dev-env'; + +const { apiUrl, baseUrl } = primePlaywrightLocalDevEnv(); +const frontendPort = new URL(baseUrl).port || '3100'; +const backendPort = new URL(apiUrl).port || '8010'; + /** * Playwright E2E Test Configuration * @@ -33,11 +39,11 @@ export default defineConfig({ /* Shared settings for all the projects below */ use: { /* Base URL to use in actions like `await page.goto('/')` */ - baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000', + baseURL: baseUrl, /* API URL for backend */ extraHTTPHeaders: { - 'X-API-Base-URL': process.env.PLAYWRIGHT_API_URL || 'http://localhost:8000', + 'X-API-Base-URL': apiUrl, }, /* Collect trace when retrying the failed test */ @@ -156,16 +162,16 @@ export default defineConfig({ /* Run local dev server before starting tests */ webServer: process.env.CI ? undefined : [ { - command: 'npm run dev', - url: 'http://localhost:3000', + command: `BACKEND_URL=${apiUrl} npm run dev -- --hostname 127.0.0.1 --port ${frontendPort}`, + url: baseUrl, reuseExistingServer: !process.env.CI, timeout: 120_000, stdout: 'ignore', stderr: 'pipe', }, { - command: 'cd ../backend && uvicorn app.main:app --host 0.0.0.0 --port 8000', - url: 'http://localhost:8000/health', + command: `cd ../backend && .venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port ${backendPort}`, + url: `${apiUrl}/health`, reuseExistingServer: !process.env.CI, timeout: 120_000, stdout: 'ignore', diff --git a/frontend/e2e/tests/annual-planning.spec.ts b/frontend/e2e/tests/annual-planning.spec.ts index b2387682a..7e38aeb26 100644 --- a/frontend/e2e/tests/annual-planning.spec.ts +++ b/frontend/e2e/tests/annual-planning.spec.ts @@ -35,7 +35,14 @@ async function mockAnnualPlanningHubShell(adminPage: Page) { }, policyAnchors: { status: 'ready', + hasEffectiveProgramPolicy: true, + effectiveProgramPolicySource: 'academic_year', activeEventCount: 1, + activeCalendarAnchorCount: 1, + activeAnchorCount: 2, + programAnchorCount: 1, + regulatoryAnchorCount: 0, + institutionAnchorCount: 1, programEventCount: 0, regulatoryEventCount: 0, institutionEventCount: 1, @@ -108,7 +115,7 @@ test.describe('Annual Planning Hub', () => { }); test.describe('Annual Planning review surfaces', () => { - test('renders policy anchors, provenance, and history from planner APIs', async ({ + test('renders policy layers, provenance, and history from planner APIs', async ({ adminPage, }) => { const repairPlanId = '11111111-1111-4111-8111-111111111111'; @@ -208,6 +215,63 @@ test.describe('Annual Planning review surfaces', () => { }, }); + await mockAPI(adminPage, '**/api/v1/program-policies/effective*', { + body: { + id: '99999999-9999-4999-8999-999999999999', + name: 'AY 2026-2027 Core Policy', + academicYear: 2026, + isDefault: false, + isActive: true, + pgy1InpatientClinicDayOfWeek: 2, + pgy1InpatientClinicTimeOfDay: 'AM', + pgy2InpatientClinicDayOfWeek: 1, + pgy2InpatientClinicTimeOfDay: 'PM', + pgy3InpatientClinicDayOfWeek: 0, + pgy3InpatientClinicTimeOfDay: 'PM', + smAcademicDayOfWeek: 2, + smAcademicTimeOfDay: 'AM', + facultyDidacticDayOfWeek: 2, + facultyDidacticTimeOfDay: 'PM', + skipFinalWeekForFacultyDidactic: true, + notes: 'Graduation month stays protected for didactics and handoffs.', + requestedAcademicYear: 2026, + source: 'academic_year', + }, + }); + + await mockAPI(adminPage, '**/api/v1/program-calendar/effective*', { + body: { + items: [ + { + id: '88888888-8888-4888-8888-888888888888', + name: 'Graduation Week', + policyLayer: 'institution', + anchorKind: 'protected_time', + anchorRule: 'fixed_date', + academicYear: 2026, + federalHoliday: null, + month: 6, + day: 20, + dayOffset: 0, + durationDays: 11, + appliesTo: 'all', + appliesToInpatient: true, + activityId: null, + timeOfDay: null, + notes: 'Protect end-of-year outprocessing', + isActive: true, + anchorDate: '2027-06-20', + effectiveStartDate: '2027-06-20', + effectiveEndDate: '2027-06-30', + affectsSchedule: true, + }, + ], + total: 1, + page: 1, + pageSize: 200, + }, + }); + await mockAPI(adminPage, '**/api/v1/institutional-events/effective*', { body: { items: [ @@ -263,19 +327,30 @@ test.describe('Annual Planning review surfaces', () => { await expect( adminPage.getByRole('heading', { name: 'Annual Planning & Roll-Over' }), ).toBeVisible(); - await expect(adminPage.getByText('Policy Anchors for AY 2026-2027')).toBeVisible(); - await expect(adminPage.getByText('Graduation Week')).toBeVisible(); + await expect(adminPage.getByText('Policy Layers for AY 2026-2027')).toBeVisible(); + await expect( + adminPage.getByText('Effective Program Policy', { exact: true }), + ).toBeVisible(); + await expect(adminPage.getByText('AY 2026-2027 Core Policy')).toBeVisible(); + await expect(adminPage.getByText('AY 2026-2027 override')).toBeVisible(); + await expect(adminPage.getByText('Graduation Week').first()).toBeVisible(); await expect(adminPage.getByText('Created by: plancreator')).toBeVisible(); await expect( adminPage.getByText('Source: Published AY 26-27 Baseline (published)'), ).toBeVisible(); await expect(adminPage.getByText('Repair blocks: 5')).toBeVisible(); await expect( - adminPage.getByRole('link', { name: 'Open Events Admin' }), + adminPage.getByRole('link', { name: 'Open Events Admin' }).first(), ).toHaveAttribute( 'href', /\/admin\/institutional-events\?active=active&policyLayer=all&academicYear=2026/, ); + await expect( + adminPage.getByRole('link', { name: 'Open Policy Admin' }), + ).toHaveAttribute( + 'href', + /\/admin\/program-policy\?active=active&academicYear=2026/, + ); await expect( adminPage.getByRole('link', { name: 'Manage AY Roster' }), ).toHaveAttribute( diff --git a/frontend/e2e/utils/local-dev-auth.ts b/frontend/e2e/utils/local-dev-auth.ts index df76e202f..7d9ca6f96 100644 --- a/frontend/e2e/utils/local-dev-auth.ts +++ b/frontend/e2e/utils/local-dev-auth.ts @@ -4,6 +4,8 @@ import path from 'node:path'; import { APIRequestContext, Page, expect } from '@playwright/test'; +import { getPlaywrightApiUrl } from './local-dev-env'; + type LocalAdminCredentials = { username: string; password: string; @@ -23,7 +25,7 @@ const BACKEND_LOG_FILE = path.join( ); function getApiBaseUrl(): string { - return process.env.PLAYWRIGHT_API_URL || 'http://localhost:8000'; + return getPlaywrightApiUrl(); } function isLocalApiBaseUrl(apiBaseUrl: string): boolean { @@ -86,6 +88,7 @@ async function waitForBackendHealth( async function ensureLoopbackBackendRunning(apiBaseUrl: string): Promise { const normalizedApiBaseUrl = normalizeLoopbackApiBaseUrl(apiBaseUrl); + const apiPort = new URL(normalizedApiBaseUrl).port || '8010'; const healthUrl = `${normalizedApiBaseUrl}/health`; if (await isBackendHealthy(healthUrl)) { @@ -105,7 +108,7 @@ async function ensureLoopbackBackendRunning(apiBaseUrl: string): Promise { const logFd = fs.openSync(BACKEND_LOG_FILE, 'a'); const child = spawn( BACKEND_PYTHON, - ['-m', 'uvicorn', 'app.main:app', '--host', '127.0.0.1', '--port', '8000'], + ['-m', 'uvicorn', 'app.main:app', '--host', '127.0.0.1', '--port', apiPort], { cwd: BACKEND_DIR, env: process.env, diff --git a/frontend/e2e/utils/local-dev-env.ts b/frontend/e2e/utils/local-dev-env.ts new file mode 100644 index 000000000..cf4cdce80 --- /dev/null +++ b/frontend/e2e/utils/local-dev-env.ts @@ -0,0 +1,36 @@ +const DEFAULT_PLAYWRIGHT_BASE_URL = 'http://127.0.0.1:3100'; +const DEFAULT_PLAYWRIGHT_API_URL = 'http://127.0.0.1:8010'; + +function normalizeUrl(url: string): string { + return url.replace(/\/$/, ''); +} + +function getDefaultApiUrl(): string { + return normalizeUrl( + process.env.PLAYWRIGHT_API_URL || + process.env.BACKEND_URL || + DEFAULT_PLAYWRIGHT_API_URL, + ); +} + +export function getPlaywrightBaseUrl(): string { + return normalizeUrl(process.env.PLAYWRIGHT_BASE_URL || DEFAULT_PLAYWRIGHT_BASE_URL); +} + +export function getPlaywrightApiUrl(): string { + return getDefaultApiUrl(); +} + +export function primePlaywrightLocalDevEnv(): { + apiUrl: string; + baseUrl: string; +} { + const baseUrl = getPlaywrightBaseUrl(); + const apiUrl = getDefaultApiUrl(); + + process.env.PLAYWRIGHT_BASE_URL ??= baseUrl; + process.env.PLAYWRIGHT_API_URL ??= apiUrl; + process.env.BACKEND_URL ??= apiUrl; + + return { apiUrl, baseUrl }; +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index cae1f7d56..518a5860d 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,5 +1,11 @@ import { defineConfig, devices } from '@playwright/test'; +import { primePlaywrightLocalDevEnv } from './e2e/utils/local-dev-env'; + +const { apiUrl, baseUrl } = primePlaywrightLocalDevEnv(); +const frontendPort = new URL(baseUrl).port || '3100'; +const backendPort = new URL(apiUrl).port || '8010'; + /** * Playwright configuration for E2E testing * @@ -17,7 +23,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:3000', + baseURL: baseUrl, trace: 'on-first-retry', screenshot: 'only-on-failure', }, @@ -27,10 +33,22 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, ], - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - }, + webServer: process.env.CI + ? undefined + : [ + { + command: `BACKEND_URL=${apiUrl} npm run dev -- --hostname 127.0.0.1 --port ${frontendPort}`, + url: baseUrl, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, + { + command: `cd ../backend && .venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port ${backendPort}`, + url: `${apiUrl}/health`, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + stdout: 'ignore', + stderr: 'pipe', + }, + ], });