ComfyUI-Manager/reports/scenario_effects.md
Dr.Lt.Data 4410ebc6a6
Some checks are pending
Publish to PyPI / build-and-publish (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted
simple-form Content-Types (x-www-form-urlencoded, multipart/form-data,
text/plain) on 16 no-body POST handlers (glob + legacy) to block
<form method=POST> CSRF that bypasses method-only gating. Move
comfyui_switch_version to a JSON body so the preflight requirement applies.
Split db_mode/policy/update/channel_url_list into GET(read) + POST(write).
Tighten do_fix (high → high+) and gate three previously-ungated config
setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations,
ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness
variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage,
and a CHANGELOG.

Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/
reinstall/abort_current) are removed; callers already use queue/batch.
Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained.

Reported-by: XlabAI Team of Tencent Xuanwu Lab
CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
2026-04-22 05:04:30 +09:00

22 KiB
Raw Permalink Blame History

Scenario × Functional Effect Mapping

Generated: 2026-04-18 Definition of "effect": The actual functional purpose of the feature — not just any side effect. A scenario is verified only when the intended outcome is observably achieved.

Pattern Effect definition
Success scenario The feature's PURPOSE is fulfilled and observable
Validation/security error The purpose is NOT fulfilled + correct rejection signal
State edge case The purpose is correctly short-circuited or no-op

Unless specified, status code alone is NOT sufficient evidence of effect.


Section 1 — Glob v2 Endpoints

1.1 Queue Management (Install/Uninstall/Update/Fix/Disable/Enable/Model)

POST /v2/manager/queue/task (kind=install)

Purpose: install a custom node pack so it becomes loadable by ComfyUI.

Scenario Functional effect to verify
Success (CNR pack) (a) pack directory exists under custom_nodes/, (b) .tracking file present (CNR marker), (c) pack appears in GET customnode/installed with correct cnr_id + version, (d) worker task_worker_lock released after completion
Success (nightly/URL) (a) pack directory exists, (b) .git subdir present (git clone), (c) repo remote matches requested URL, (d) appears in installed list
Success (skip_post_install + already disabled) Pack moved from .disabled/ back to active (enable shortcut), NOT a fresh install
Validation error (bad kind value) Task NOT queued (queue/status unchanged), queue/history does not contain this ui_id, pack NOT installed
Validation error (missing ui_id/client_id) Same: no queued task, no installation side-effect
Worker auto-start After task queued, queue/status.is_processing=true and eventually done_count increments

POST /v2/manager/queue/task (kind=uninstall)

Purpose: remove an installed pack so it is no longer loaded.

Scenario Effect to verify
Success Pack directory no longer exists under custom_nodes/, pack absent from customnode/installed, no import error on next ComfyUI reload
Target not installed No-op or error — purpose already satisfied; no state change
Unknown pack No filesystem change

POST /v2/manager/queue/task (kind=update)

Purpose: update an installed pack to a newer version.

Scenario Effect to verify
Success (a) pack directory still exists, (b) version actually changed (check .tracking content or pyproject version), (c) dependencies refreshed, (d) still loadable by ComfyUI
Already up-to-date No-op or confirmatory response; no downgrade
Unknown pack / Update fails No partial state (pack not removed nor corrupted)

POST /v2/manager/queue/task (kind=fix)

Purpose: re-install dependencies of an existing pack without changing source.

Scenario Effect to verify
Success (a) pack directory unchanged (same HEAD/version), (b) dependencies present in venv after fix, (c) pack import succeeds on reload
Missing dependencies pre-fix After fix, imports succeed

POST /v2/manager/queue/task (kind=disable)

Purpose: stop loading a pack without removing it, reversibly.

Scenario Effect to verify
Success (a) pack moved from custom_nodes/<name>/ to custom_nodes/.disabled/<name>/, (b) on next ComfyUI reload, pack nodes NOT registered, (c) pack absent from customnode/installed (active)
Already disabled No-op; still in .disabled/

POST /v2/manager/queue/task (kind=enable)

Purpose: restore a disabled pack to active, loadable state.

Scenario Effect to verify
Success (a) pack restored from .disabled/ to active custom_nodes/ (may be case-normalized CNR name), (b) on reload, nodes registered again, (c) appears in customnode/installed
Not disabled (already active) No-op; no regression

POST /v2/manager/queue/install_model

Purpose: download a model file to the appropriate models/ subdirectory.

Scenario Effect to verify
Success (a) task queued (queue/status reflects), (b) eventually file downloaded to models/<type>/<filename>, (c) file size > 0, (d) visible via externalmodel/getlist with installed=True (legacy)
Missing client_id/ui_id Task NOT queued; no download attempted
Invalid metadata Task NOT queued
Not in whitelist (legacy check) Download rejected; no file written
Non-safetensors + security<high+ Rejected; no file written

POST /v2/manager/queue/update_all

Purpose: queue update tasks for ALL currently active packs.

Scenario Effect to verify
Success queue/status.pending_count == N where N = (active_nodes + unknown_active_nodes - manager-skip). Each queued task has correct kind=update + correct node_name
Security denied (<middle+) 403; NO tasks queued; queue/status unchanged
Missing params 400; NO tasks queued
mode=local No remote fetch; uses local channel data
Desktop build comfyui-manager pack NOT in queued tasks

POST /v2/manager/queue/update_comfyui

Purpose: queue a self-update task for ComfyUI core.

Scenario Effect to verify
Success (a) queue/status.total_count increased by 1, (b) the queued task has kind=update-comfyui with params.is_stable matching request/config
Missing params 400; no task queued
stable=true overrides config Task params.is_stable==True regardless of config policy

POST /v2/manager/queue/reset

Purpose: clear all queued/running/history tasks.

Scenario Effect to verify
Success queue/status: total_count=0, done_count=0, pending_count=0, in_progress_count=0, is_processing=false
Already empty Same; idempotent

POST /v2/manager/queue/start

Purpose: start the worker thread to process queued tasks.

Scenario Effect to verify
Worker not running queue/status.is_processing becomes true (may be momentary if queue empty); tasks transition pending → running → done
Already running 201; is_processing remains true; no duplicate worker spawned
Empty queue Worker starts and idles; no errors

GET /v2/manager/queue/status

Purpose: accurately reflect the current queue state.

Scenario Effect to verify
No filter Counts match actual internal queue state (cross-check via known queued tasks)
With client_id filter client_id echo + filtered counts correspond to only that client's tasks
Fields shape total/done/in_progress/pending/is_processing all present + correct types

GET /v2/manager/queue/history

Purpose: retrieve completed task records for introspection.

Scenario Effect to verify
id=<batch_id> query Returns JSON content of that batch file (not another's)
Path traversal file read DOES NOT occur; returns 400
ui_id filter Returns the matching single task record
client_id filter Returns only that client's history
Pagination Result size ≤ max_items
Serialization limitation If 400 returned, server didn't crash; no corrupted state

GET /v2/manager/queue/history_list

Purpose: list available batch history file IDs.

Scenario Effect to verify
Success Returned ids ⊆ files in manager_batch_history_path (mtime-desc sorted)
Empty ids=[] reflects empty dir

1.2 Custom Node Info

GET /v2/customnode/getmappings

Purpose: provide node→pack mapping for the UI to resolve missing nodes.

Scenario Effect to verify
Success mode=local/cache/remote Returned dict: values are [node_list, metadata], all currently-loaded NODE_CLASS_MAPPINGS either present in a node_list OR matched by nodename_pattern regex
mode=nickname Nicknames filter applied (each entry has nickname field)
Missing mode query 500/KeyError; no partial data returned

GET /v2/customnode/fetch_updates (deprecated)

Purpose: (deprecated) was previously used to fetch git updates.

Scenario Effect to verify
Always 410 + {deprecated: true}. No git fetch performed (no disk I/O on .git dirs)

GET /v2/customnode/installed

Purpose: list currently-installed packs with metadata for the UI.

Scenario Effect to verify
mode=default Dict reflects real filesystem scan of custom_nodes/: every dir with proper marker appears
mode=imported Returns snapshot frozen at startup (unchanged after runtime installs) — proves stability
Newly installed pack After install, default mode reflects it; imported mode does NOT

POST /v2/customnode/import_fail_info

Purpose: return detailed traceback/message for a pack that failed to import at startup.

Scenario Effect to verify
Known failed pack via cnr_id 200 + body has msg + traceback matching cm_global.error_dict[module]
Known failed via url Same
Unknown pack 400 (no info); error_dict NOT mutated
Missing fields / non-dict 400 with appropriate text

POST /v2/customnode/import_fail_info_bulk

Purpose: same as above but for multiple packs in one call.

Scenario Effect to verify
cnr_ids list Each key maps to either {error, traceback} (if failed) or null (if no failure). Unknown cnr_ids → null
urls list Same semantics
Empty lists 400
Mixed types inside list 400 or skip with per-item error

1.3 Snapshots

GET /v2/snapshot/get_current

Purpose: capture and return the current system state (not persist it).

Scenario Effect to verify
Success Returned dict contains comfyui (hash/tag), git_custom_nodes (list), cnr_custom_nodes (list), pips. Consistent with actual installed state

POST /v2/snapshot/save

Purpose: persist current system state so it can be restored later.

Scenario Effect to verify
Success (a) new file created in manager_snapshot_path with timestamped name, (b) file content == get_current() at save time, (c) appears in snapshot/getlist.items

GET /v2/snapshot/getlist

Purpose: list saved snapshots for UI selection.

Scenario Effect to verify
Success items list matches .json files in snapshot dir (without extension), sorted desc
After save New snapshot name appears at top
After remove Removed name absent

POST /v2/snapshot/remove

Purpose: delete a saved snapshot permanently.

Scenario Effect to verify
Success File removed from disk; absent from getlist
Nonexistent target No change; 200 (no-op)
Path traversal File NOT removed; 400; any other files untouched
Security denied File NOT removed; 403

POST /v2/snapshot/restore

Purpose: schedule a snapshot to be applied on next server restart (the actual restore happens at startup).

Scenario Effect to verify
Success restore-snapshot.json copied to manager_startup_script_path. Next reboot → actual state reverts to snapshot (verifiable by reboot + get_current comparison)
Nonexistent target No marker file created; 400
Path traversal No file operations; 400
Security denied No marker file; 403

1.4 Configuration

GET /v2/manager/db_mode

Purpose: return current DB source mode config.

Scenario Effect to verify
Success Returned text == core.get_config()["db_mode"] value in config.ini

POST /v2/manager/db_mode

Purpose: persist new DB mode to config.ini.

Scenario Effect to verify
Valid value (a) config.ini written to disk with new value, (b) GET returns new value, (c) survives process restart
Malformed JSON / missing value 400; config.ini UNCHANGED

GET/POST /v2/manager/policy/update

Purpose: read/persist update policy (stable vs nightly).

Same verification pattern as db_mode but for update_policy key.

GET /v2/manager/channel_url_list

Purpose: return available channels + currently selected.

Scenario Effect to verify
Success selected matches channel whose URL == config.channel_url (else "custom"); list is all known channels as "name::url"

POST /v2/manager/channel_url_list

Purpose: switch active channel by name.

Scenario Effect to verify
Known name config.channel_url written with new URL; GET.selected matches new name
Unknown name Silent no-op; 200; channel_url UNCHANGED (verify)
Malformed 400; channel_url UNCHANGED

1.5 System

GET /v2/manager/is_legacy_manager_ui

Purpose: let UI know which Manager UI (legacy vs current) to load.

Scenario Effect to verify
Success is_legacy_manager_ui matches the CLI flag --enable-manager-legacy-ui that was passed

GET /v2/manager/version

Purpose: report the Manager package version.

Scenario Effect to verify
Success Text == core.version_str (non-empty, semver-ish)
Idempotent Consecutive calls return identical value

POST /v2/manager/reboot

Purpose: restart the server process.

Scenario Effect to verify
Success (a) server process actually exits, (b) new process binds same port, (c) new process serves requests, (d) pre-reboot state preserved (version, config)
CLI session mode .reboot marker file created before exit(0); process-manager restarts
Security denied 403; process continues (no restart)

GET /v2/comfyui_manager/comfyui_versions

Purpose: enumerate available ComfyUI versions + current.

Scenario Effect to verify
Success current is a git tag/hash present in .git log; versions array non-empty; current ∈ versions
Git failure 400; no partial response

POST /v2/comfyui_manager/comfyui_switch_version

Purpose: queue a task to switch ComfyUI to a target version (actual switch happens via worker).

Scenario Effect to verify
Success (a) task queued with params.target_version=<ver>, (b) queue/status reflects, (c) eventually .git HEAD points at target commit/tag after worker runs
Missing params 400; no task queued
Security denied (<high+) 403; no task queued

Section 2 — Legacy-only Endpoints (UI → effect)

For these, the functional purpose is triggered by UI interaction. The effect MUST be observable both through the UI (state transitions, renders) AND/OR through the backend (filesystem, queue state).

POST /v2/manager/queue/batch (legacy)

Purpose: accept one aggregated request to enqueue multiple operations, then start the worker.

Scenario Effect to verify
install item(s) Each pack installed (filesystem effect); failed list only contains actually-failed ids
uninstall item(s) Each pack removed
update item(s) Packs updated (version change verifiable)
reinstall item(s) Pack removed then re-installed (dir exists, .tracking present)
disable Pack in .disabled/
install_model Model file downloaded
fix Dependencies re-resolved
update_comfyui ComfyUI update task queued
update_all All active pack updates queued
Mixed kinds Each kind's effect achieved; failed contains only real failures

GET /v2/customnode/getlist (legacy)

Purpose: feed the Custom Nodes Manager dialog with the list of available + installed packs.

Scenario Effect to verify
Success Response has channel + node_packs; each pack includes install state (installed/disabled), stars (github-stats), update availability (if skip_update=false)
skip_update=true No git fetch performed (check timing / no remote calls)
Channel resolution Maps URL back to name (default/custom/etc.)

GET /customnode/alternatives (legacy)

Purpose: show alternative pack recommendations for a given pack.

Scenario Effect to verify
Success Response dict keyed by unified pack id; values from alter-list.json with markdown processed

GET /v2/externalmodel/getlist (legacy)

Purpose: list available external models with install state for Model Manager dialog.

Scenario Effect to verify
Success Each model entry has installed ∈ {'True','False'}; True ⟺ file actually exists under appropriate models subdir
HuggingFace sentinel Filename resolved from URL basename; installed flag correct
Custom save_path Path resolved correctly

GET /v2/customnode/versions/{node_name} (legacy)

Purpose: list all versions of a CNR pack for the user to pick.

Scenario Effect to verify
Known CNR pack Response array lists all available versions (latest first, typically) matches CNR registry
Unknown pack 400; no partial data

GET /v2/customnode/disabled_versions/{node_name} (legacy)

Purpose: list versions of a pack currently in the disabled state for possible re-enable.

Scenario Effect to verify
Has disabled versions Response array matches actual cnr_inactive_nodes[node] keys + "nightly" if in nightly_inactive_nodes
None disabled 400

POST /v2/customnode/install/git_url (legacy)

Purpose: clone a pack from arbitrary git URL (dangerous; requires high+).

Scenario Effect to verify
Success Repo cloned into custom_nodes/, .git dir present, repo remote matches URL
Already installed 200 skip; no duplicate; no overwrite
Clone failure 400; no partial dir left behind
Security denied (<high+) 403; no filesystem change

POST /v2/customnode/install/pip (legacy)

Purpose: run pip install <packages> in the venv.

Scenario Effect to verify
Success Packages are importable from the venv Python afterwards (or pip list shows them)
Security denied (<high+) 403; no pip invocation

GET /v2/manager/notice (legacy)

Purpose: fetch the News wiki content and augment with version footer.

Scenario Effect to verify
GitHub reachable HTML returned; contains markdown-body content + ComfyUI/Manager version footer appended
GitHub unreachable "Unable to retrieve Notice"; no crash
Non-git ComfyUI Response starts with "Your ComfyUI isn't git repo" warning
Outdated ComfyUI Response starts with "too OUTDATED!!!" warning
Desktop variant Footer uses __COMFYUI_DESKTOP_VERSION__ instead of commit hash

Section 3 — UI→effect Mapping (Legacy)

For Playwright tests, the "UI→effect" contract requires:

UI action Target endpoint Effect to verify
Click Manager menu button (none — UI only) #cm-manager-dialog visible
Click "Custom Nodes Manager" menu item GET customnode/getlist + getmappings #cn-manager-dialog + grid populated (rows > 0)
Click "Model Manager" menu item GET externalmodel/getlist #cmm-manager-dialog + grid populated
Click "Snapshot Manager" menu item GET snapshot/getlist #snapshot-manager-dialog + list populated
Click "Install" on a pack row GET customnode/versions/{id} → POST queue/batch (install) → WebSocket cm-queue-status Pack dir exists on disk + row shows "Installed" state in UI + WebSocket all-done received
Click "Uninstall" on installed row POST queue/batch (uninstall) Pack dir removed + row state updates to "Not Installed"
Click "Disable" on row POST queue/batch (disable) Pack in .disabled/ + row state "Disabled"
Click "Update" on outdated row POST queue/batch (update) Pack version changes + row state update
Click "Fix" on row POST queue/batch (fix) Dependencies restored
Click "Try alternatives" GET /customnode/alternatives Alternatives list rendered
Open "Versions" dropdown on row GET customnode/versions/{id} Version list rendered in UI
Open "Disabled Versions" on row GET customnode/disabled_versions/{id} Disabled versions rendered
Click "Install via Git URL" button + enter URL + confirm POST customnode/install/git_url Pack cloned; dir visible in UI
Click "Install via pip" POST customnode/install/pip Package installed; no UI crash
Click "Install" on Model Manager row POST queue/install_model Model file downloaded; row state "Installed"
Change DB mode dropdown POST db_mode Config persisted; dropdown value persists after dialog reopen
Change Update Policy dropdown POST policy/update Same
Change Channel dropdown POST channel_url_list Same
Click "Update All" button POST queue/update_all Multiple tasks queued; progress indicator shows count
Click "Update ComfyUI" button POST queue/update_comfyui Task queued; status indicator
Click "Save Snapshot" in Snapshot Manager POST snapshot/save New row in dialog list with timestamp
Click "Remove" on snapshot row POST snapshot/remove?target=X Row disappears from list
Click "Restore" on snapshot row POST snapshot/restore?target=X Marker file created; next reboot applies
Click "Restart" button POST manager/reboot Server restarts; UI reconnects
Open Manager menu with pending News GET manager/notice News panel visible with HTML content
Filter/search in grid (client-side) Row count ≤ initial count
Close dialog (X button / Esc) (none) Dialog hidden; no leaked DOM

Section 4 — Effects Not Easily Observable

Some purposes can only be proven via side-channel observation:

Endpoint Purpose Why hard to verify
POST snapshot/restore Apply snapshot at next reboot Must actually reboot + compare post-state; destructive
POST switch_version (positive) Change ComfyUI version Destructive; needs rollback
POST manager/reboot Restart process Hard to assert "new process" vs "same process" cleanly; proxy: pid change or connection drop+rebind
POST queue/start → worker runs Tasks execute Timing-dependent; must poll done_count
GET manager/notice Content from GitHub External dependency; flaky
POST install (network) Actually installs Depends on CNR/GitHub availability
POST install_model (download) File downloaded Slow; large files; fake whitelist URL returns quick 404

For these, tests either (a) accept destructive as out-of-scope, (b) use timing/polling, or (c) mock at minimum granularity.


End of Scenario × Effect Mapping