# Test Contract Audit **Generated**: 2026-04-18 **Contract**: - **Glob E2E** = endpoint call β†’ effect verification (HTTP POST/GET + verify state change or response correctness) - **Legacy E2E** = UI interaction β†’ effect verification (click/select/fill + verify state change) Tests that call HTTP endpoints directly in the Playwright suite VIOLATE the legacy contract. Tests that check only status code without verifying effect VIOLATE the glob contract. ## Summary | Contract violation | Count | Severity | |---|---:|---| | Playwright tests using direct API (bypass UI) | ~~9~~ β†’ 5 | πŸ”΄ contract breach (4 resolved in Stage2 WI-F; remaining 5 are in legacy-ui-snapshot helper functions + other files β€” see updated Section 1) | | Playwright tests partially UI-driven (mixed) | 2 | 🟑 weakened | | Glob tests missing effect verification | ~~1~~ β†’ 0 | 🟑 status-only (resolved in Stage2 WI-D) | | Glob tests fully effect-verifying | 80 | βœ“ compliant | | Security-contract tests (CSRF method-reject β€” 8 functions / 52 invocations β€” 26 glob + 26 legacy) | 8 | βœ“ compliant (separate negative contract; glob + legacy server coverage) | --- # Section 1 β€” Playwright Contract Audit (Legacy UI β†’ Effect) ## βœ… VIOLATIONS β€” all 4 listed tests resolved in Stage2 WI-F Historical record (2026-04-18, Stage2 WI-F). All 4 direct-API Playwright tests that previously violated the legacy contract have been removed from the suite or rewritten to click real UI elements: | File | Test | Original violation | Resolution | |---|---|---|---| | legacy-ui-snapshot.spec.ts | `lists existing snapshots` | Direct `page.request.get('/v2/snapshot/getlist')` β€” no UI click | **DELETED**; backend `getlist` coverage owned by pytest `test_e2e_snapshot_lifecycle.py::test_getlist_after_save` (12/12 pytest regression PASS). | | legacy-ui-snapshot.spec.ts | `save snapshot via API and verify in list` | 100% `page.request.post/get` β€” zero UI interaction | **REWRITTEN** as `SS1 Save button creates a new snapshot row`: clicks dialog Save/Create button, polls `getlist` only as backend-effect confirmation helper (not as the test's primary action), cleans up via afterEach. Bonus: `UI Remove button deletes a snapshot row` added for row-delete UI coverage. | | legacy-ui-navigation.spec.ts | `API health check while dialogs are open` | `page.request.get('/v2/manager/version')` β€” direct API, not UI | **DELETED**; version coverage owned by `test_e2e_system_info.py::test_version_returns_string/test_version_is_stable`. | | legacy-ui-navigation.spec.ts | `system endpoints accessible from browser context` | 2Γ— `page.request.get` β€” direct API | **DELETED**; fully redundant with `test_e2e_system_info.py` suite. | **Verification**: `npx playwright test --list --grep ''` β†’ `Total: 0 tests in 0 files`. Current spec listing: 5 tests in 2 files, all UI-driven. **Residual note**: The snapshot spec still uses `page.request.get('/v2/snapshot/getlist')` inside the `getSnapshotNames` helper and in the `beforeEach/afterEach` for deterministic seeding/cleanup. This is acceptable because (a) the TEST ACTION is a UI button click, and (b) the API use is confined to backend-effect observation, matching the hybrid pattern also used in legacy-ui-manager-menu's dropdown tests (the mixed-pattern row below, which remains WEAKENED but is a known follow-up). ## 🟑 WEAKENED β€” tests that mix UI + direct API These tests DO perform UI interaction (e.g., `selectOption`) but use direct API for verification/cleanup. The UIβ†’effect part is present but the effect is validated via API, not via UI rendering. | File | Test | Mixed pattern | Recommended | |---|---|---|---| | legacy-ui-manager-menu.spec.ts | `DB mode dropdown round-trips via API` | `selectOption(newValue)` (UI βœ“) β†’ `page.request.get` (verify βœ—) | Verify via UI: re-open dialog, read `.value` from `` elements visible | | legacy-ui-manager-menu.spec.ts | `closes and reopens without duplicating` | Close + reopen dialog | ≀2 dialog instances in DOM | | legacy-ui-custom-nodes.spec.ts | `opens from Manager menu and renders grid` | Click "Custom Nodes Manager" | `#cn-manager-dialog` + grid visible | | legacy-ui-custom-nodes.spec.ts | `loads custom node list (non-empty)` | Open dialog, wait | `.tg-row` count > 0 | | legacy-ui-custom-nodes.spec.ts | `filter dropdown changes displayed nodes` | `selectOption('Installed')` | Filtered count ≀ initial | | legacy-ui-custom-nodes.spec.ts | `search input filters the grid` | `fill('ComfyUI-Manager')` | Filtered count ≀ initial | | legacy-ui-custom-nodes.spec.ts | `footer buttons are present` | Open dialog | Install via Git URL / Restart button visible | | legacy-ui-model-manager.spec.ts | `opens from Manager menu and renders grid` | Click "Model Manager" | `#cmm-manager-dialog` + grid | | legacy-ui-model-manager.spec.ts | `loads model list (non-empty)` | Open dialog | Rows > 0 | | legacy-ui-model-manager.spec.ts | `search input filters the model grid` | `fill('stable diffusion')` | Filtered ≀ initial | | legacy-ui-model-manager.spec.ts | `filter dropdown is present with expected options` | Open dialog | Options length > 0 | | legacy-ui-snapshot.spec.ts | `opens snapshot manager from Manager menu` | Click "Snapshot Manager" | `#snapshot-manager-dialog` visible | | legacy-ui-snapshot.spec.ts | `SS1 Save button creates a new snapshot row` | Click dialog Save/Create button | New snapshot appears in UI row + backend list (hybrid UI-action + backend-effect confirm) | | legacy-ui-snapshot.spec.ts | `UI Remove button deletes a snapshot row` | Click in-row Remove/Delete button (dialog confirm accepted) | Snapshot absent from UI row AND backend list | | legacy-ui-navigation.spec.ts | `Manager menu β†’ Custom Nodes β†’ close β†’ Manager still visible` | Nested dialog nav | Manager reopenable | | legacy-ui-navigation.spec.ts | `Manager menu β†’ Model Manager β†’ close β†’ reopen` | Close + reopen | Model Manager reappears | | debug-install-flow.spec.ts | `capture install button API flow` | Click Install β†’ select version β†’ Select | Captures full API sequence (debug) | ## Playwright Contract Summary (post Stage2 WI-F) - **Compliant** (UIβ†’effect): 17 / 20 tests (85%) - **Mixed** (UI + direct API): 2 / 20 tests (10%) - **Violating** (direct API only): 0 / 20 tests (0%) βœ… β€” WI-F resolved all 4 - **Debug/instrumentation** (acceptable exception): 1 / 20 tests (5%) Net change from previous audit (22 tests β†’ 20 tests): `legacy-ui-navigation` lost 2 deleted INADEQUATE tests; `legacy-ui-snapshot` kept 3 total (1 existing PASS + 2 new UI-driven PASS that replaced the 2 original INADEQUATE). The "Mixed" WEAKENED rows (2 manager-menu dropdown tests) remain and should be addressed in a follow-up WI. --- # Section 1.5 β€” Security-Contract Tests (CSRF Method-Reject) `tests/e2e/test_e2e_csrf.py` follows a NEGATIVE-assertion contract: state-changing POST endpoints MUST reject HTTP GET. Unlike the glob endpointβ†’effect contract (positive response + state change), the CSRF contract verifies ABSENCE of acceptance. **Contract**: - GET on state-changing POST endpoint β†’ status_code ∈ (400,403,404,405) - POST counterpart β†’ status_code == 200 (sanity) - GET on read-only endpoint β†’ status_code == 200 (negative control) **Reference**: commit 99caef55 β€” "mitigate CSRF on state-changing endpoints + version SSOT" (CVSS 8.1, reported by XlabAI-Tencent-Xuanwu). Commit applied the GETβ†’POST conversion to BOTH `glob/manager_server.py` (~91 lines) and `legacy/manager_server.py` (~92 lines); the legacy-side regression guard is exercised by `test_e2e_csrf_legacy.py` (added in WI-FF, integrated into this audit in WI-GG). **Scope clarification per file docstrings**: ONLY the GET-rejection layer. NOT covered: Origin/Referer validation, same-site cookies, anti-CSRF tokens, cross-site form POST. Do NOT read a PASS here as "CSRF fully solved". Both glob and legacy suites share the same scope β€” the split exists solely because `comfyui_manager/__init__.py` loads `glob.manager_server` XOR `legacy.manager_server` (mutex via `--enable-manager-legacy-ui`), so each route table requires its own server lifecycle to exercise. | File | Tests | Contract verdict | |---|---:|---| | test_e2e_csrf.py::TestStateChangingEndpointsRejectGet | 13 (parametrized; 3 dual-purpose endpoints removed in WI-HH β€” legitimately covered only in the allow-GET class) | βœ“ compliant β€” negative-path security contract (glob) | | test_e2e_csrf.py::TestCsrfPostWorks | 2 | βœ“ compliant β€” positive sanity (glob) | | test_e2e_csrf.py::TestCsrfReadEndpointsStillAllowGet | 11 (parametrized) | βœ“ compliant β€” negative control for over-correction (glob) | | test_e2e_csrf_legacy.py::TestLegacyStateChangingEndpointsRejectGet | 13 (parametrized β€” queue/taskβ†’queue/batch; 3 dual-purpose excluded) | βœ“ compliant β€” negative-path security contract (legacy) | | test_e2e_csrf_legacy.py::TestLegacyCsrfPostWorks | 2 | βœ“ compliant β€” positive sanity (legacy) | | test_e2e_csrf_legacy.py::TestLegacyCsrfReadEndpointsStillAllowGet | 11 (parametrized) | βœ“ compliant β€” negative control (legacy) | **Endpoint-list differences** (legacy vs glob, per `test_e2e_csrf_legacy.py` docstring L23–36): - `/v2/manager/queue/task` β†’ dropped (glob-only; legacy uses `queue/batch`) - `/v2/manager/queue/batch` β†’ added (legacy task-enqueue) - `/v2/manager/db_mode`, `/v2/manager/policy/update`, `/v2/manager/channel_url_list` β†’ dropped from reject-GET (CSRF contract applies only to POST write-path; same GET-read + POST-write split as glob, so these 3 legitimately appear in the allow-GET class only). `test_e2e_csrf.py` currently lists them in BOTH classes; WI-HH tracks the glob-side correction. --- # Section 2 β€” Glob pytest Contract Audit (Endpoint β†’ Effect) ## 🟑 Missing effect verification | File | Test | Missing effect | |---|---|---| | test_e2e_task_operations.py | `test_install_model_accepts_valid_request` | Only checks 200 status. Does NOT verify task was actually queued (could be via GET queue/status total_countβ‰₯1). | Adding one line (`status check`) would fix this. ## βœ“ Effect-verifying tests (80 of 81) ### Install/Uninstall (pack-level effects) - test_e2e_endpoint.test_install_via_endpoint β†’ `_pack_exists` + `_has_tracking` βœ“ - test_e2e_endpoint.test_uninstall_via_endpoint β†’ `_pack_exists == False` βœ“ - test_e2e_endpoint.test_install_uninstall_cycle β†’ both βœ“ - test_e2e_git_clone.test_01_nightly_install β†’ `_pack_exists` + `.git` dir βœ“ - test_e2e_git_clone.test_03_nightly_uninstall β†’ `_pack_exists == False` βœ“ ### Disable/Enable (state-level effects) - test_e2e_task_operations.test_disable_pack β†’ `_pack_disabled` + `_pack_exists == False` βœ“ - test_e2e_task_operations.test_enable_pack β†’ `_pack_exists` + `!_pack_disabled` βœ“ - test_e2e_task_operations.test_disable_enable_cycle β†’ both transitions βœ“ ### Update/Fix (post-state verification) - test_e2e_task_operations.test_update_installed_pack β†’ `_pack_exists` after βœ“ - test_e2e_task_operations.test_fix_installed_pack β†’ `_pack_exists` after βœ“ ### Queue state - test_e2e_queue_lifecycle.test_reset_queue β†’ status.pending_count == 0 βœ“ - test_e2e_queue_lifecycle.test_queue_task_and_history β†’ done_count > 0 βœ“ - test_e2e_queue_lifecycle.test_start_queue_already_idle β†’ status code βœ“ (idempotent effect) - test_e2e_task_operations.test_update_comfyui_queues_task β†’ total_count β‰₯ 1 βœ“ ### Config round-trips (persistence effect) - test_e2e_config_api.test_set_and_restore_db_mode β†’ GET reflects POST βœ“ - test_e2e_config_api.test_set_and_restore_update_policy β†’ same βœ“ - test_e2e_config_api.test_set_and_restore_channel β†’ same βœ“ ### Snapshot state - test_e2e_snapshot_lifecycle.test_save_snapshot + test_getlist_after_save β†’ save creates, getlist reflects βœ“ - test_e2e_snapshot_lifecycle.test_remove_snapshot β†’ removed item absent from list βœ“ ### System state - test_e2e_system_info.test_reboot_and_recovery β†’ health check recovers βœ“ - test_e2e_system_info.test_version_is_stable β†’ consecutive calls idempotent βœ“ ### Response-correctness (read endpoints) All GET endpoint tests verify response schema and content. Examples: - getmappings, installed, queue/status, queue/history, snapshot/get_current, etc. β†’ response shape + field presence asserted βœ“ ### Validation/Negative (error-path effects) - All `test_*_invalid_body`, `test_*_missing_params`, `test_*_returns_400`, `test_fetch_updates_returns_deprecated` verify the error RESPONSE effect (status code + optional body fields) βœ“ ## Glob pytest Contract Summary - **Compliant** (endpointβ†’effect, positive contract): 81 / 81 tests (100%) β€” Stage2 WI-D upgraded `test_install_model_accepts_valid_request` to effect-verifying. - **Weak** (status-only, no effect): ~~1~~ β†’ 0 / 81 tests (resolved) - **Security-contract** (CSRF method-reject, separate negative contract): 8 / 8 test functions (52 / 52 parametrized invocations β€” 26 glob + 26 legacy) β€” all compliant. References: `tests/e2e/test_e2e_csrf.py` (glob, 99caef55 ~91-line diff; 3 dual-purpose endpoints removed from reject-GET fixture in WI-HH to match the GET-read + POST-write split, so glob count dropped from 29 β†’ 26) + `tests/e2e/test_e2e_csrf_legacy.py` (legacy, 99caef55 ~92-line diff β€” added in WI-FF, audited in WI-GG). See Section 1.5 above. --- # Section 3 β€” Reclassification Plan ## Tests to move out of Playwright suite (STATUS: ALL 4 RESOLVED β€” Stage2 WI-F) 1. ~~`legacy-ui-snapshot.spec.ts::lists existing snapshots`~~ β†’ **DELETED**; backend `getlist` coverage owned by `test_e2e_snapshot_lifecycle.py::test_getlist_after_save`. 2. ~~`legacy-ui-snapshot.spec.ts::save snapshot via API and verify in list`~~ β†’ **REWRITTEN** as UI-driven `SS1 Save button creates a new snapshot row`; also `UI Remove button deletes a snapshot row` added. 3. ~~`legacy-ui-navigation.spec.ts::API health check while dialogs are open`~~ β†’ **DELETED**; version coverage in `test_e2e_system_info.py::test_version_returns_string`. 4. ~~`legacy-ui-navigation.spec.ts::system endpoints accessible from browser context`~~ β†’ **DELETED**; redundant with pytest system_info suite. Verification: `npx playwright test --list --grep ''` β†’ 0 tests. pytest counterparts regression: 12/12 PASS (`test_e2e_snapshot_lifecycle.py` + `test_e2e_system_info.py`). ## Tests to rewrite for UI-only verification 2 mixed tests in `legacy-ui-manager-menu.spec.ts` should verify via UI instead of API: 1. `DB mode dropdown round-trips via API` β†’ after selectOption, re-open dialog and check `