# Report B — E2E Test Inventory + Coverage Mapping **Generated**: 2026-04-18 **Source directories**: - `tests/e2e/*.py` (pytest — HTTP + CLI E2E) - `tests/playwright/*.spec.ts` (Playwright — legacy UI E2E) ## Summary | Category | Files | Test Functions | |---|---:|---:| | pytest E2E (HTTP API) | 13 | 92 | | pytest E2E (CLI — uv-compile) | 1 | 12 | | Playwright (legacy UI) | 6 | 21 | | Playwright (debug) | 1 | 1 | | **TOTAL** | **21** | **126** | > **Note**: +1 file / +4 functions vs prior counts reflect inclusion of `tests/e2e/test_e2e_csrf.py` (CSRF-mitigation contract suite, commit 99caef55). That suite's 4 test functions parametrize to 26 pytest invocations (13+2+11) after WI-HH removed 3 dual-purpose endpoints from the reject-GET fixture (they legitimately answer GET on the read-path and are covered only in the allow-GET class). > > **WI-Z Y2 sync (2026-04-19)**: Playwright legacy UI count 21 → 19 reflected Stage2 WI-F deletion of two `legacy-ui-navigation.spec.ts` tests (`API health check while dialogs are open`, `system endpoints accessible from browser context`) that were rewritten as direct-API violators; their coverage is now owned by `test_e2e_system_info.py`. TOTAL 119 → 117 was a downstream correction. > > **WI-AA sync (2026-04-19)**: Playwright legacy UI count 19 → 21 reflects addition of `tests/playwright/legacy-ui-install.spec.ts` (2 tests: LB1 Install button + LB2 Uninstall button) previously implemented but not inventoried. TOTAL 117 → 119. See dedicated subsection below. > > **WI-GG sync (2026-04-20)**: pytest E2E (HTTP API) file count 10 → 11 and function count 85 → 89 reflects addition of `tests/e2e/test_e2e_csrf_legacy.py` (4 new test functions / 26 parametrized invocations: 13 reject-GET + 2 POST-works + 11 allow-GET) from WI-FF. TOTAL 119 → 123 test functions. The legacy suite is the counterpart to `test_e2e_csrf.py` for the `--enable-manager-legacy-ui` server variant — required because `comfyui_manager/__init__.py` loads `glob.manager_server` XOR `legacy.manager_server`. See dedicated subsection below. (Accounting note: the higher-level audit `reports/e2e_verification_audit.md` Summary Matrix renders the 26 legacy invocations per-row, reaching TOTAL 143; both counts refer to the same underlying tests at different granularities — function-level here, invocation-level there.) > > **WI-LL sync (2026-04-20)**: pytest E2E (HTTP API) file count 11 → 13 and function count 89 → 92 reflects addition of two new SECGATE-coverage files (WI-KK deliverables, audit-integrated by WI-LL): `tests/e2e/test_e2e_secgate_strict.py` (strict-mode harness + SR4 PoC — 2 functions: `test_remove_returns_403` PASS + `test_post_works_at_default_after_restore` pytest.skip'd positive counterpart stub) + `tests/e2e/test_e2e_secgate_default.py` (default-mode demo + CV4 — 1 function: `test_switch_version_returns_403_at_default` PASS). TOTAL 123 → 126 test functions. These close 2 of the original 8 T2 SECGATE-PENDING Goals (SR4, CV4) and establish the strict-mode harness pattern (`start_comfyui_strict.sh` + config.ini backup/restore) for the remaining T2-pending-harness-ready Goals (SR6, V5, UA2). See the Classification policy block in `e2e_verification_audit.md` for the reclassification and propagation plan. **Unique endpoints exercised**: 27 (glob v2) + 4 (legacy-only: queue/batch indirectly via UI) --- # Section 1 — pytest E2E HTTP Tests ## tests/e2e/test_e2e_endpoint.py (7 tests) Covers the main install/uninstall flow via `/v2/manager/queue/task` and `/v2/customnode/installed`. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestEndpointInstallUninstall::test_install_via_endpoint` | POST queue/task, POST queue/start | Install CNR pack (success) | pack dir exists + .tracking file present | | `TestEndpointInstallUninstall::test_installed_list_shows_pack` | GET customnode/installed | Pack appears in installed list | cnr_id match in dict values | | `TestEndpointInstallUninstall::test_uninstall_via_endpoint` | POST queue/task kind=uninstall | Uninstall success | pack dir removed from disk | | `TestEndpointInstallUninstall::test_installed_list_after_uninstall` | GET customnode/installed | Post-uninstall state | cnr_id absent from installed list | | `TestEndpointInstallUninstall::test_install_uninstall_cycle` | queue/task x2 | Full install→verify→uninstall cycle | All above assertions in one test | | `TestEndpointStartup::test_comfyui_started` | GET /system_stats | Server health | 200 response | | `TestEndpointStartup::test_startup_resolver_ran` | (log file) | UnifiedDepResolver ran at startup | log contains `[UnifiedDepResolver]` + "startup batch resolution succeeded" | ## tests/e2e/test_e2e_git_clone.py (3 tests) Covers nightly (URL-based) install via `/v2/manager/queue/task` which triggers git_helper.py subprocess. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestNightlyInstallCycle::test_01_nightly_install` | POST queue/task selected_version=nightly | git clone via Manager API | pack dir exists + .git directory present | | `TestNightlyInstallCycle::test_02_no_module_error` | (log file) | No ModuleNotFoundError regression | log does not contain "ModuleNotFoundError" | | `TestNightlyInstallCycle::test_03_nightly_uninstall` | POST queue/task kind=uninstall | Uninstall nightly pack | pack dir removed | ## tests/cli/test_uv_compile.py — RELOCATED (WI-PP) Previously tracked here as `tests/e2e/test_e2e_uv_compile.py`. Moved to `tests/cli/` in WI-PP because every test in the suite drives cm-cli as a subprocess; none of them exercise HTTP endpoints. The 8 tests (post WI-MM/NN/OO consolidation) continue to cover install / reinstall (xfail-marked for purge_node_state) / verbs-with-uv-compile (parametrized ×5) / uv-sync no-packs-exits-zero / no-packs-emits-signal / with-packs / conflict attribution with specs. CI runner updated in `.github/workflows/e2e.yml` to point at the new path. ## tests/e2e/test_e2e_config_api.py (10 tests) Covers GET/POST round-trip on configuration endpoints. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestConfigDbMode::test_read_db_mode` | GET db_mode | Read current value | text in {cache, channel, local, remote} | | `TestConfigDbMode::test_set_and_restore_db_mode` | GET/POST db_mode | Set→read-back→restore | POST 200 + verify echo + restore verified | | `TestConfigDbMode::test_set_db_mode_invalid_body` | POST db_mode | Malformed JSON | 400 | | `TestConfigUpdatePolicy::test_read_update_policy` | GET policy/update | Read current policy | text in {stable, stable-comfyui, nightly, nightly-comfyui} | | `TestConfigUpdatePolicy::test_set_and_restore_update_policy` | GET/POST policy/update | Set→read-back→restore | Round-trip verification | | `TestConfigUpdatePolicy::test_set_policy_invalid_body` | POST policy/update | Malformed JSON | 400 | | `TestConfigChannelUrlList::test_read_channel_url_list` | GET channel_url_list | Response shape | has `selected` (str) + `list` (array) | | `TestConfigChannelUrlList::test_channel_list_entries_are_name_url_strings` | GET channel_url_list | Entry format | each entry is "name::url" string | | `TestConfigChannelUrlList::test_set_and_restore_channel` | GET/POST channel_url_list | Switch channel + restore | Verify `selected` matches set value | | `TestConfigChannelUrlList::test_set_channel_invalid_body` | POST channel_url_list | Malformed JSON | 400 | ## tests/e2e/test_e2e_customnode_info.py (11 tests) Covers custom node info/mapping endpoints. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestCustomNodeMappings::test_getmappings_returns_dict` | GET customnode/getmappings?mode=local | Success response | 200 + dict | | `TestCustomNodeMappings::test_getmappings_entries_have_node_lists` | GET getmappings | Entry structure | each value is `[node_list, metadata]` | | `TestFetchUpdates::test_fetch_updates_returns_deprecated` | GET customnode/fetch_updates | Deprecated endpoint | 410 + `deprecated: true` | | `TestInstalledPacks::test_installed_returns_dict` | GET customnode/installed | Success | 200 + dict | | `TestInstalledPacks::test_installed_imported_mode` | GET installed?mode=imported | Startup snapshot | 200 + dict | | `TestImportFailInfo::test_unknown_cnr_id_returns_400` | POST import_fail_info | Unknown pack | 400 | | `TestImportFailInfo::test_missing_fields_returns_400` | POST import_fail_info | Missing cnr_id+url | 400 | | `TestImportFailInfo::test_invalid_body_returns_error` | POST import_fail_info | Non-dict body | 400 | | `TestImportFailInfoBulk::test_bulk_with_cnr_ids_returns_dict` | POST import_fail_info_bulk | cnr_ids list | 200 + null for unknown | | `TestImportFailInfoBulk::test_bulk_empty_lists_returns_400` | POST import_fail_info_bulk | Empty cnr_ids+urls | 400 | | `TestImportFailInfoBulk::test_bulk_with_urls_returns_dict` | POST import_fail_info_bulk | urls list | 200 + dict | ## tests/e2e/test_e2e_queue_lifecycle.py (9 tests) Covers the queue management lifecycle. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestQueueLifecycle::test_reset_queue` | POST queue/reset | Empty the queue | 200 | | `TestQueueLifecycle::test_status_after_reset` | GET queue/status | Post-reset state | all counts 0, is_processing bool | | `TestQueueLifecycle::test_status_with_client_id_filter` | GET queue/status?client_id=X | Client filter | response echoes client_id | | `TestQueueLifecycle::test_start_queue_already_idle` | POST queue/start | Idle worker start | status in {200, 201} | | `TestQueueLifecycle::test_queue_task_and_history` | POST queue/task + queue/start + GET queue/status + GET queue/history | Full lifecycle | done_count>0 polled, history 200 or 400 | | `TestQueueLifecycle::test_history_with_ui_id_filter` | GET queue/history?ui_id=X | Filter history | 200 or 400 (serialization-limit) | | `TestQueueLifecycle::test_history_with_pagination` | GET queue/history?max_items=1&offset=0 | Pagination | 200 or 400 | | `TestQueueLifecycle::test_history_list` | GET queue/history_list | List batch IDs | 200 + `ids` list | | `TestQueueLifecycle::test_final_reset_and_clean_state` | POST queue/reset + GET queue/status | Cleanup | pending_count==0 | ## tests/e2e/test_e2e_snapshot_lifecycle.py (7 tests) Covers snapshot save/list/remove cycle. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestSnapshotLifecycle::test_get_current_snapshot` | GET snapshot/get_current | Current state dict | 200 + dict | | `TestSnapshotLifecycle::test_save_snapshot` | POST snapshot/save | Save new snapshot | 200 | | `TestSnapshotLifecycle::test_getlist_after_save` | GET snapshot/getlist | List contains new snapshot | items.length > 0 | | `TestSnapshotLifecycle::test_remove_snapshot` | POST snapshot/remove?target=X + GET getlist | Remove + verify | target absent + count decremented | | `TestSnapshotLifecycle::test_remove_nonexistent_snapshot` | POST snapshot/remove | Nonexistent target | 200 (no-op) | | `TestSnapshotLifecycle::test_remove_path_traversal_rejected` | POST snapshot/remove?target=../... | Path-traversal targets must be rejected | 400 + sentinel file outside snapshot dir preserved (SR3) | | `TestSnapshotGetCurrentSchema::test_getlist_items_are_strings` | GET snapshot/getlist | Items shape | each item is string | > Note: `POST /v2/snapshot/restore` intentionally NOT tested (destructive). ## tests/e2e/test_e2e_system_info.py (4 tests) Covers system-level endpoints (version, legacy UI flag, reboot). | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestManagerVersion::test_version_returns_string` | GET manager/version | Non-empty string | 200 + len>0 | | `TestManagerVersion::test_version_is_stable` | GET manager/version x2 | Idempotency | consecutive calls return same value | | `TestIsLegacyManagerUI::test_returns_boolean_field` | GET is_legacy_manager_ui | Response shape | `{is_legacy_manager_ui: bool}` | | `TestReboot::test_reboot_and_recovery` | POST manager/reboot + GET version | Restart + recovery | 200 or 403 (security); server polls healthy; version unchanged | ## tests/e2e/test_e2e_task_operations.py (16 tests) Covers queue/task operations for kinds NOT tested in test_e2e_endpoint.py. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestDisableEnable::test_disable_pack` | POST queue/task kind=disable | Disable moves pack to .disabled/ | pack dir gone + .disabled/ entry present | | `TestDisableEnable::test_enable_pack` | POST queue/task kind=enable | Enable restores pack | pack dir present + .disabled/ entry gone | | `TestDisableEnable::test_disable_enable_cycle` | queue/task x2 | Full disable→enable | Both transitions verified | | `TestUpdatePack::test_update_installed_pack` | POST queue/task kind=update | Update pack | pack still exists after update | | `TestUpdatePack::test_update_history_recorded` | GET queue/history?ui_id=X | History has update entry | 200 or 400 (serialization-limit) | | `TestFixPack::test_fix_installed_pack` | POST queue/task kind=fix | Fix pack | pack still exists | | `TestFixPack::test_fix_history_recorded` | GET queue/history?ui_id=X | History has fix entry | 200 or 400 | | `TestInstallModel::test_install_model_accepts_valid_request` | POST queue/install_model | Valid model request | 200 (reset queue after) | | `TestInstallModel::test_install_model_missing_client_id` | POST queue/install_model | Missing client_id | 400 | | `TestInstallModel::test_install_model_missing_ui_id` | POST queue/install_model | Missing ui_id | 400 | | `TestInstallModel::test_install_model_invalid_body` | POST queue/install_model | Invalid metadata | 400 | | `TestUpdateAll::test_update_all_queues_tasks` | POST queue/update_all | Queue all update tasks | 200/403 or tolerated ReadTimeout | | `TestUpdateAll::test_update_all_missing_params` | POST queue/update_all | Missing params | 400 ValidationError | | `TestUpdateComfyUI::test_update_comfyui_queues_task` | POST queue/update_comfyui | Queue task | 200 + total_count>=1 after | | `TestUpdateComfyUI::test_update_comfyui_missing_params` | POST queue/update_comfyui | Missing params | 400 | | `TestUpdateComfyUI::test_update_comfyui_with_stable_flag` | POST queue/update_comfyui?stable=true | Explicit stable flag | 200 | ## tests/e2e/test_e2e_version_mgmt.py (7 tests) Covers comfyui_versions + comfyui_switch_version endpoints. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestComfyUIVersions::test_versions_endpoint` | GET comfyui_versions | Response shape | `{versions: list, current: str}` | | `TestComfyUIVersions::test_versions_list_not_empty` | GET comfyui_versions | Non-empty list | len>0 | | `TestComfyUIVersions::test_versions_items_are_strings` | GET comfyui_versions | Item type | each version is string | | `TestComfyUIVersions::test_current_is_in_versions` | GET comfyui_versions | Current in list | current appears in versions | | `TestSwitchVersionNegative::test_switch_version_missing_all_params` | POST comfyui_switch_version | No params | 400 or 403 | | `TestSwitchVersionNegative::test_switch_version_missing_client_id` | POST comfyui_switch_version?ver=X | Partial params | 400 or 403 | | `TestSwitchVersionNegative::test_switch_version_validation_error_body` | POST comfyui_switch_version | Error body shape | `error` field present (when 400 JSON) | > Note: Actual version switching (destructive) intentionally NOT tested. --- ## tests/e2e/test_e2e_csrf.py (4 test functions / 26 parametrized invocations — post-WI-HH) Covers the CSRF-mitigation contract from commit 99caef55 — state-changing endpoints must reject HTTP GET so that `` / link-click / redirect-based cross-origin triggers cannot mutate server state. **Scope (per docstring)**: ONLY the GET-rejection contract. NOT covered here: Origin/Referer validation (separate middleware), same-site cookies, anti-CSRF tokens, cross-site form POST. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestStateChangingEndpointsRejectGet::test_get_is_rejected[path]` | 13 POST endpoints (queue/start, queue/reset, queue/update_all, queue/update_comfyui, queue/install_model, queue/task, snapshot/save, snapshot/remove, snapshot/restore, manager/reboot, comfyui_switch_version, import_fail_info, import_fail_info_bulk — WI-HH removed db_mode, policy/update, channel_url_list from this list since they legitimately answer GET on the read-path) | GET must reject | status_code in (400,403,404,405); explicit `not in 200-399` guard | | `TestCsrfPostWorks::test_queue_reset_post_works` | POST queue/reset | POST counterpart works | status_code == 200 | | `TestCsrfPostWorks::test_snapshot_save_post_works` | POST snapshot/save + cleanup via getlist+remove | POST counterpart works | status_code == 200; cleanup | | `TestCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds[path]` | 11 GET endpoints (version, db_mode, policy/update, channel_url_list, queue/status, queue/history_list, is_legacy_manager_ui, customnode/installed, snapshot/getlist, snapshot/get_current, comfyui_versions) | Negative control: read-only still works | status_code == 200 | > Note: Three endpoints (`db_mode`, `policy/update`, `channel_url_list`) appear in BOTH reject-GET (POST path, write) and allow-GET (read path) lists — commit 99caef55 split each into a GET-read + POST-write pair; the POST path must reject GET while the GET path must continue to succeed. --- ## tests/e2e/test_e2e_csrf_legacy.py (4 test functions / 26 parametrized invocations) Legacy-mode counterpart to `test_e2e_csrf.py`. Verifies the same CSRF method-rejection contract but against the legacy server module loaded via `--enable-manager-legacy-ui`. Added in WI-FF (commit following 99caef55) to close the legacy-side regression-guard gap. Audit-integrated in WI-GG. **Why a separate file** (per docstring L7–13): `comfyui_manager/__init__.py` loads `glob.manager_server` XOR `legacy.manager_server` via mutex on the `--enable-manager-legacy-ui` flag. One ComfyUI process exposes either the glob or the legacy route table, never both — so verifying the legacy CSRF contract requires its own module-scoped server lifecycle with the legacy flag set (via `start_comfyui_legacy.sh`). **Scope (per docstring L44–48)**: Same as `test_e2e_csrf.py` — ONLY the method-reject layer. Origin/Referer, same-site cookies, anti-CSRF tokens, and cross-site form POST are out of scope. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestLegacyStateChangingEndpointsRejectGet::test_get_is_rejected[path]` | 13 POST endpoints (queue/start, queue/reset, queue/update_all, queue/update_comfyui, queue/install_model, **queue/batch** (legacy; replaces queue/task), snapshot/save, snapshot/remove, snapshot/restore, manager/reboot, comfyui_switch_version, import_fail_info, import_fail_info_bulk) | GET must reject under legacy server | status_code in (400,403,404,405); explicit `not in 200-399` guard | | `TestLegacyCsrfPostWorks::test_queue_reset_post_works` | POST queue/reset (legacy) | POST counterpart works under legacy server | status_code == 200 | | `TestLegacyCsrfPostWorks::test_snapshot_save_post_works` | POST snapshot/save + cleanup via getlist+remove (legacy) | POST counterpart works + cleanup | status_code == 200; cleanup | | `TestLegacyCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds[path]` | 11 GET endpoints (version, db_mode, policy/update, channel_url_list, queue/status, queue/history_list, is_legacy_manager_ui, customnode/installed, snapshot/getlist, snapshot/get_current, comfyui_versions) | Negative control: legacy read-only still works | status_code == 200 | > **Endpoint-list deltas vs glob** (per docstring L23–36): > - `queue/task` → dropped (glob-only); `queue/batch` → added (legacy task-enqueue equivalent) > - `db_mode`, `policy/update`, `channel_url_list` → dropped from reject-GET (CSRF contract applies only to the POST write-path; legacy splits these into `@routes.get` read + `@routes.post` write, identical to glob). These 3 remain in the ALLOW-GET class above. (The glob `test_e2e_csrf.py` lists them in BOTH classes; WI-HH tracks the glob-side correction.) --- ## tests/e2e/test_e2e_secgate_strict.py (1 test active + 1 skipped; WI-KK PoC, WI-LL audit-integrated) Strict-mode security-gate PoC. Covers the middle/middle+ gate 403 contract for Goals that require elevating `security_level=strong`. Launches via `start_comfyui_strict.sh` (which patches `user/__manager/config.ini` to `security_level=strong`, leaves a `.before-strict` backup, and starts the server on the E2E port) and restores the original config in the fixture teardown. **Scope (per docstring L3–9)**: strict-mode 403 path for the middle/middle+ gates. The default E2E config (`security_level=normal`, `is_local_mode=True`) puts NORMAL inside the allowed set for both gates per `comfyui_manager/glob/utils/security_utils.py` L32–38, so this harness is the only way to exercise the 403 side. This is the first of 4 planned Goals (SR4 ← here; SR6, V5, UA2 ← mechanical additions using the same fixture). | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestSecurityGate403_SR4::test_remove_returns_403` | POST `/v2/snapshot/remove?target=…` (under security_level=strong) | Goal SR4 — snapshot/remove below `middle` | (a) `status_code == 403`; (b) the seeded snapshot file on disk is NOT deleted (negative-check per `verification_design.md` §7.3 Security Boundary Template). | | `TestSecurityGate403_SR4::test_post_works_at_default_after_restore` | (none — pytest.skip) | Positive counterpart of SR4 at default config | pytest.skip'd: deferred to `test_e2e_secgate_default.py` follow-up to avoid double-startup cost. Documents both halves of the gate contract. | **Harness notes**: - **Teardown ordering** is contract-critical: stop server FIRST, then restore config (the server holds the config-file lock; restoring before stopping causes a re-snapshot race). Documented in the fixture's `finally` block. - Subsequent test modules continue to see `security_level=normal` because the backup restore happens deterministically in teardown. ## tests/e2e/test_e2e_secgate_default.py (1 test; WI-KK demo, WI-LL audit-integrated) Default-mode security-gate demonstration. Covers the CV4 Goal (comfyui_switch_version `high+` gate 403 contract) without any harness, leveraging the WI-KK research finding that default `security_level=normal` + `is_local_mode=True` already triggers 403 for high+ operations at the HTTP handler. This is the cleanest of the 4 originally-classified-T2 high+ Goals to demonstrate the no-harness-needed insight. **Scope (per docstring L1–18)**: only the CV4 Goal. The other 3 originally-T2 high+ Goals are deferred with reclassification notes: - **IM4** → **T2-TASKLEVEL**: non-safetensors check lives deep in the install pipeline (worker + `get_risky_level`), not at the HTTP handler. POST `/v2/manager/queue/install_model` accepts the request and queues a task; rejection only surfaces at task execution. Requires a queue-observation pattern, not a simple HTTP 403 check. - **LGU2**, **LPP2** → **NORMAL-legacy**: registered ONLY in `legacy/manager_server.py` (L1502, L1522). Testing needs `start_comfyui_legacy.sh` fixture — follow-up `test_e2e_secgate_legacy_default.py` is the natural home. | Test | Endpoint(s) | Scenario | Assertion semantics | |---|---|---|---| | `TestSecurityGate403_CV4::test_switch_version_returns_403_at_default` | POST `/v2/comfyui_manager/comfyui_switch_version` with `ver`, `client_id`, `ui_id` (at default security_level) | Goal CV4 — comfyui_switch_version below `high+` | `status_code == 403`. The `ver` query is syntactically valid so the request WOULD reach the Pydantic validation step IF the gate were broken; since the gate is the FIRST check in the handler, 403 must precede any 400-from-validation outcome. | --- # Section 2 — Playwright UI Tests All Playwright tests require ComfyUI running with `--enable-manager-legacy-ui` on PORT (default 8199). ## tests/playwright/legacy-ui-manager-menu.spec.ts (5 tests) Covers the Manager Menu dialog and its settings dropdowns. | Test | Endpoint(s) exercised | Scenario | Assertion semantics | |---|---|---|---| | `Manager Menu Dialog > opens via Manager button and shows 3-column layout` | (indirect: initial page + legacy UI detection) | Menu dialog opens | `#cm-manager-dialog` visible; "Custom Nodes Manager", "Model Manager", "Restart" buttons present | | `> shows settings dropdowns (DB, Channel, Policy)` | (UI) | DB + Policy combos render | Both `