Files
rookiestar28-ComfyUI-Doctor/tests/test_host_compatibility_script.py

1316 lines
50 KiB
Python

import importlib.util
import sys
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "check_host_compatibility.py"
SPEC = importlib.util.spec_from_file_location("check_host_compatibility", SCRIPT_PATH)
host_compat = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = host_compat
SPEC.loader.exec_module(host_compat)
def _write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def _create_minimal_reference(root: Path) -> None:
_write(
root / "ComfyUI" / "main.py",
"file_log_outputs = get_file_log_outputs(args.verbose)\n"
"setup_logger(log_level=console_log_level, file_outputs=file_log_outputs, use_stdout=args.log_stdout)\n"
"def execute_prestartup_script():\n return 'prestartup_script.py'\n",
)
_write(root / "ComfyUI" / "nodes.py", "EXTENSION_WEB_DIRS = {}\nWEB_DIRECTORY = './web'\n")
_write(root / "ComfyUI" / "requirements.txt", "comfyui-frontend-package==1.48.7\n")
_write(
root / "ComfyUI" / "README.md",
"torch 2.7 is minimally supported but using a newer version is extremely recommended.\n",
)
_write(
root / "ComfyUI" / "comfy" / "cli_args.py",
"LOG_LEVELS = ('DEBUG', 'DETAIL', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')\n"
"class VerboseAction(argparse.Action):\n pass\n"
"def get_console_log_level(outputs):\n return 'INFO'\n"
"def get_file_log_outputs(outputs):\n return []\n"
'parser.add_argument("--models-directory", type=is_valid_directory, '
'help="Set the ComfyUI models directory. Overrides the models folder in --base-directory.")\n',
)
_write(
root / "ComfyUI" / "server.py",
"class PromptServer:\n def __init__(self):\n self.routes = []\n"
'@routes.get("/extensions")\n'
'@routes.get("/features")\n'
'@routes.get("/system_stats")\n'
"def get_extensions():\n return EXTENSION_WEB_DIRS\n"
"def queue_prompt(request, json_data):\n"
" extra_data = json_data['extra_data']\n"
' if "comfy_usage_source" not in extra_data:\n'
' usage_source = request.headers.get("Comfy-Usage-Source")\n'
" if usage_source:\n"
' extra_data["comfy_usage_source"] = usage_source\n'
"def system_stats():\n"
" torch_devices = comfy.model_management.get_all_torch_devices()\n"
" device_entries = []\n"
" comfy_package_versions = FrontendManager.get_comfy_package_versions()\n"
' return {"devices": device_entries, "comfy_package_versions": comfy_package_versions, '
'"deploy_environment": get_deploy_environment()}\n'
"def _cancel_job_by_id(job_id):\n"
" return self.prompt_queue.interrupt_if_running(job_id)\n"
" classification = cancel_job(job_id, running, queued, history, interrupt, dequeue)\n"
" return classification in (CANCEL_RUNNING, CANCEL_PENDING)\n"
'@routes.post("/api/jobs/{job_id}/cancel")\n'
"async def cancel_job_by_id(request):\n pass\n"
'@routes.post("/api/jobs/cancel")\n'
"async def cancel_jobs_batch(request):\n pass\n"
'if first_message and data.get("type") == "feature_flags":\n'
' self.sockets_metadata[sid]["feature_flags"] = client_flags\n'
" feature_flags.get_server_features()\n"
"# Prefix every route with /api for easier matching for delegation.\n"
'api_routes.route(route.method, "/api" + route.path)(route.handler, **route.kwargs)\n'
"self.app.add_routes(api_routes)\n"
"self.app.add_routes(self.routes)\n"
"web.static('/extensions/' + name, dir)\n",
)
_write(
root / "ComfyUI" / "execution.py",
'"execution_error" "node_id" "node_type" "traceback" "current_inputs" "current_outputs"\n'
'if io.Hidden.comfy_usage_source.name in hidden:\n'
' hidden_inputs_v3[io.Hidden.comfy_usage_source] = extra_data.get("comfy_usage_source", None)\n'
'input_data_all[x] = [extra_data.get("comfy_usage_source", None)]\n'
"output_ui = enrich_output_with_assets(output_ui)\n"
'"executed" "output": output_ui\n'
"def interrupt_if_running(self, prompt_id):\n"
" if self.currently_running == prompt_id:\n"
" comfy.model_management.interrupt_processing()\n",
)
_write(
root / "ComfyUI" / "comfy_execution" / "jobs.py",
"CANCEL_RUNNING = 'running'\n"
"CANCEL_PENDING = 'pending'\n"
"CANCEL_TERMINAL = 'terminal'\n"
"CANCEL_UNKNOWN = 'unknown'\n"
"def classify_job_for_cancel(job_id, running, queued, history):\n"
" return CANCEL_PENDING\n"
"def cancel_job(job_id, running, queued, history, interrupt, dequeue):\n"
" return CANCEL_RUNNING\n",
)
_write(
root / "ComfyUI" / "comfy_execution" / "progress.py",
'"progress_state" "display_node_id" "parent_node_id" "real_node_id" supports_preview_metadata\n',
)
_write(
root / "ComfyUI" / "folder_paths.py",
'folder_names_and_paths["diffusion_models"] = []\n'
'folder_names_and_paths["text_encoders"] = []\n'
'folder_names_and_paths["clip_vision"] = []\n'
'folder_names_and_paths["style_models"] = []\n'
'folder_names_and_paths["photomaker"] = []\n'
'folder_names_and_paths["classifiers"] = []\n'
'folder_names_and_paths["model_patches"] = []\n'
'folder_names_and_paths["audio_encoders"] = []\n'
'folder_names_and_paths["background_removal"] = []\n'
'folder_names_and_paths["frame_interpolation"] = []\n'
'folder_names_and_paths["geometry_estimation"] = []\n'
'folder_names_and_paths["optical_flow"] = []\n'
'folder_names_and_paths["detection"] = []\n'
'folder_names_and_paths["datasets"] = ([os.path.join(base_path, "datasets")], set())\n'
"def get_system_user_directory(name='system'):\n return name\n"
"def get_public_user_directory(user_id):\n return user_id\n",
)
_write(
root / "ComfyUI" / "comfy_extras" / "nodes_dataset.py",
'node_id="LoadImageDataSetFromFolder"\n'
'node_id="LoadImageTextDataSetFromFolder"\n'
'node_id="LoadVideoDataSetFromFolder"\n'
'node_id="LoadVideoTextDataSetFromFolder"\n'
'node_id="LoadTrainingDataset"\n',
)
_write(
root / "ComfyUI" / "comfy_api" / "feature_flags.py",
"_CORE_FEATURE_FLAGS = {\n"
'"supports_preview_metadata": True\n'
'"supports_model_type_tags": True\n'
'"extension": {"manager": {"supports_v4": True}}\n'
'"node_replacements": True\n'
'"enable_telemetry": {"default": False}\n'
"}\n"
"def get_server_features():\n return {}\n",
)
_write(
root / "ComfyUI_frontend" / "package.json",
'{"name": "@comfyorg/comfyui-frontend", "version": "1.50.3"}\n',
)
_write(
root / "ComfyUI_frontend" / "src" / "types" / "extensionTypes.ts",
"registerSidebarTab(tab)\nsetting: {\nget: <T = unknown>(id: string) => undefined\n"
"set: <T = unknown>(id: string, value: T) => void\n}\n"
"lastNodeErrors: Record<NodeId, NodeError> | null\n"
"lastExecutionError: ExecutionErrorWsMessage | null\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "stores" / "workspaceStore.ts",
"const sidebarTab = computed(() => useSidebarTabStore())\n"
"Use `sidebarTab.registerSidebarTab` instead\n"
"sidebarTab.value.registerSidebarTab(tab)\n"
"Use `sidebarTab.unregisterSidebarTab` instead\n"
"sidebarTab.value.unregisterSidebarTab(id)\n"
"Use `sidebarTab.sidebarTabs` instead\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "stores" / "workspace" / "sidebarTabStore.ts",
"defineStore('sidebarTab'\n"
"const sidebarTabs = ref<SidebarTabExtension[]>([])\n"
"const activeSidebarTabId = ref<string | null>(null)\n"
"const registerSidebarTab = () => {}\n"
"const unregisterSidebarTab = () => {}\n"
"toggleSidebarTab\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "schemas" / "apiSchema.ts",
"const zExecutionErrorWsMessage = z.object({ node_id: z.string(), node_type: z.string(), "
"traceback: z.array(z.string()), current_inputs: z.any(), current_outputs: z.any() })\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "scripts" / "app.ts",
"rootGraph\n"
"lastExecutionError\n"
"const hasPromptNodeErrors = Object.keys(response.node_errors).length > 0\n"
"if (error.status === 403 && !hasPromptNodeErrors) {\n"
" showError(error)\n"
"}\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "utils" / "executionErrorUtil.ts",
"export const NODE_LEVEL_VALIDATION_ERROR_TYPES = new Set([\n"
" 'PARTNER_NODE_DISABLED',\n"
" 'exception_during_validation',\n"
" 'dependency_cycle'\n"
"])\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "scripts" / "api.ts",
"async queuePrompt(number, { output, workflow }) {\n"
" const body = {\n"
" extra_data: {\n"
" client_id: this.clientId,\n"
" comfy_usage_source: 'comfyui-frontend',\n"
" }\n"
" }\n"
" return body\n"
"}\n"
"async cancelJob(jobId: string) {\n"
" return this.fetchApi(`/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST' })\n"
"}\n"
"async cancelJobs(jobIds: string[]) {\n"
" return this.fetchApi('/jobs/cancel', { method: 'POST', body: JSON.stringify({ job_ids: jobIds }) })\n"
"}\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "platform" / "remote" / "comfyui" / "jobs" / "fetchJobs.ts",
"async function fetchJobsRaw(fetchApi, statuses, maxItems = 200, offset = 0) {\n"
" const statusParam = statuses.join(',')\n"
" const url = `/jobs?status=${statusParam}&limit=${maxItems}&offset=${offset}`\n"
" return fetchApi(url)\n"
"}\n"
"export async function fetchHistory(fetchApi, maxItems = 200, offset = 0) {\n"
" return fetchHistoryPage(fetchApi, maxItems, offset)\n"
"}\n"
"export async function fetchHistoryPage(fetchApi, maxItems = 200, offset = 0) {\n"
" return fetchJobsRaw(fetchApi, ['completed', 'failed', 'cancelled'], maxItems, offset)\n"
"}\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "platform" / "settings" / "settingStore.ts",
"const isVisible = setting.type !== 'hidden'\n"
"const trackChanges = telemetry?.trackChanges ?? isVisible\n"
"if (!trackChanges) return undefined\n"
"const includeValues = telemetry?.includeValues ?? isVisible\n"
"const event = settingChangedEvent(settingsById.value[key], key, applied)\n"
"if (event) useTelemetry()?.trackSettingChanged(event)\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "platform" / "settings" / "types.ts",
"type SettingTelemetryOptions =\n"
" | { trackChanges: false; includeValues?: never }\n"
" | { trackChanges?: true; includeValues?: boolean }\n"
"export interface SettingParams { telemetry?: SettingTelemetryOptions }\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "platform" / "telemetry" / "initTelemetry.ts",
"const IS_CLOUD_BUILD = __DISTRIBUTION__ === 'cloud'\n"
"export async function initTelemetry() {\n"
" if (!IS_CLOUD_BUILD) return\n"
" setTelemetryRegistry(registry)\n"
"}\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "platform" / "telemetry" / "initHostTelemetry.ts",
"const ENABLE_TELEMETRY_FEATURE = 'enable_telemetry'\n"
"return remoteConfig.value.enable_telemetry === true\n"
"if (!isHostTelemetryEnabled()) return\n"
"if (!window.__comfyDesktop2?.Telemetry) return\n"
"setTelemetryRegistry(registry)\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "lib"
/ "litegraph"
/ "src"
/ "subgraph"
/ "SubgraphNode.ts",
"export class SubgraphNode extends LGraphNode implements BaseLGraph {\n"
" override readonly isVirtualNode = true as const\n"
" readonly subgraph: Subgraph\n"
" override isSubgraphNode(): this is SubgraphNode { return true }\n"
" override getInnerNodes(executableNodes) { return executableNodes }\n"
"}\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "lib" / "litegraph" / "src" / "LGraphNode.ts",
"const namedValues = getNamedValues()\n"
"if (namedValues && LiteGraph.namedValuesRestore) {\n"
" restoreNamedValues(namedValues)\n"
"} else if (info.widgets_values) {\n"
" restorePositionalValues(info.widgets_values)\n"
"}\n"
"if (widgets?.length && this.serialize_widgets) {\n"
" o.widgets_values = []\n"
" o.widgets_values_named = {}\n"
" for (const [i, widget] of widgets.entries()) {\n"
" o.widgets_values[i] = serialisedVal\n"
" o.widgets_values_named[widget.name] = serialisedVal\n"
" }\n"
"}\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "lib"
/ "litegraph"
/ "src"
/ "LiteGraphGlobal.ts",
"namedValuesRestore: boolean = false\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "platform"
/ "settings"
/ "constants"
/ "coreSettings.ts",
"id: 'Comfy.Workflow.NamedValuesRestore',\n"
" name: 'Restore widget values by name',\n"
" type: 'boolean',\n"
" defaultValue: false,\n"
" experimental: true\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "core"
/ "graph"
/ "subgraph"
/ "resolveConcretePromotedWidget.ts",
"export function resolveConcretePromotedWidget(\n"
" hostNode: LGraphNode, rawNodeId: SerializedNodeId, widgetName: string\n"
") {\n"
" const sourceWidget = sourceNode.widgets.find((entry) => entry.name === widgetName)\n"
" return { status: 'resolved', resolved: { node: sourceNode, nodePath, widget: sourceWidget } }\n"
"}\n"
"export function buildPromotedSourceExecutionId(hostExecutionId, nodePath) {\n"
" return createNodeExecutionId([...hostNodeIds, ...nodePath])\n"
"}\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "platform"
/ "missingMedia"
/ "missingMediaScan.ts",
"function mediaTypeFromSpec(spec: InputSpecV2 | undefined) {\n"
" if (!spec || !isComboInputSpec(spec)) return undefined\n"
" if (spec.video_upload) return 'video'\n"
" if (spec.image_upload || spec.animated_image_upload) return 'image'\n"
" if (spec.audio_upload) return 'audio'\n"
" return undefined\n"
"}\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "core"
/ "graph"
/ "subgraph"
/ "liftNodeErrorsToBoundary.ts",
"const liftedExtraInfo = {\n"
" input_name: surface.hostInputName,\n"
" source_execution_id: executionId,\n"
" source_input_name: inputName\n"
"}\n"
"export function liftNodeErrorsToBoundary(rootGraph, nodeErrors) { return nodeErrors }\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "stores" / "executionErrorStore.ts",
"const surfacedNodeErrors = computed(() =>\n"
" lastNodeErrors.value && app.isGraphReady\n"
" ? liftNodeErrorsToBoundary(app.rootGraph, lastNodeErrors.value)\n"
" : lastNodeErrors.value\n"
")\n",
)
_write(
root
/ "ComfyUI_frontend"
/ "browser_tests"
/ "assets"
/ "missing"
/ "missing_model_nested_promoted_widget.json",
'{"definitions":{"subgraphs":[{"id":"synthetic-subgraph","inputs":'
'[{"name":"ckpt_name","type":"COMBO"}],"nodes":[{"id":2,'
'"widgets_values":["synthetic_model.safetensors"]}]}]}}\n',
)
_write(
root / "desktop" / "package.json",
'{\n "version": "0.9.4",\n "config": {'
'"frontend": {"version": "1.43.18"}, '
'"comfyUI": {"version": "0.22.3"}}\n}\n',
)
_write(
root / "desktop" / "todesktop.json",
'{"appFiles":["assets/ComfyUI/**"],"extraResources":[{"from":"./assets"}],'
'"filesForDistribution":["!assets/**"],"updateUrlBase":"https://updater.comfy.org"}\n',
)
_write(
root / "desktop" / "src" / "main-process" / "comfyServer.ts",
"userDirectoryPath\n"
"inputDirectoryPath\n"
"outputDirectoryPath\n"
"path.join(this.basePath, 'user')\n"
"'user-directory': this.userDirectoryPath\n"
"'input-directory': this.inputDirectoryPath\n"
"'output-directory': this.outputDirectoryPath\n"
"'front-end-root': this.webRootPath\n"
"'base-directory': this.basePath\n"
"'database-url': this.databaseUrl\n"
"'extra-model-paths-config': ComfyServerConfig.configPath\n",
)
_write(
root / "desktop" / "src" / "virtualEnvironment.ts",
"this.venvPath = path.join(basePath, '.venv')\nScripts', 'python.exe'\nbin', 'python'\n",
)
_write(
root / "desktop" / "src" / "config" / "comfyConfigManager.ts",
"DEFAULT_DIRECTORIES\n"
"'custom_nodes'\n"
"'input'\n"
"'output'\n"
"['user', ['default']]\n"
"'models'\n"
"const requiredSubdirs = ['models', 'input', 'user', 'output', 'custom_nodes']\n",
)
_write(
root / "desktop" / "src" / "config" / "comfySettings.ts",
"path.join(this.#basePath, 'user', 'default', 'comfy.settings.json')\n",
)
_write(
root / "desktop" / "src" / "config" / "comfyServerConfig.ts",
"parsedConfig.comfyui_desktop.base_path = basePath\n"
"const customNodesPath = path.join(getAppResourcesPath(), 'ComfyUI', 'custom_nodes')\n"
"parsedConfig.desktop_extensions = { custom_nodes: customNodesPath }\n",
)
_write(
root / "desktop" / "src" / "services" / "cmCli.ts",
"@trackEvent('migrate_flow:migrate_custom_nodes')\n"
"path.join(this.virtualEnvironment.basePath, 'custom_nodes')\n"
"path.join(this.virtualEnvironment.basePath, 'custom_nodes', 'ComfyUI-Manager')\n",
)
def test_host_compatibility_smoke_passes_for_expected_surfaces(tmp_path):
_create_minimal_reference(tmp_path)
results = host_compat.run_checks(tmp_path)
assert all(result.ok for result in results)
def test_host_compatibility_smoke_reports_missing_patterns(tmp_path):
_create_minimal_reference(tmp_path)
(tmp_path / "ComfyUI_frontend" / "src" / "types" / "extensionTypes.ts").write_text(
"registerSidebarTab(tab)\n"
"lastNodeErrors: Record<NodeId, NodeError> | null\n"
"lastExecutionError: ExecutionErrorWsMessage | null\n",
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "extensionManager settings/sidebar API"
assert "setting:" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_current_sidebar_store(tmp_path):
_create_minimal_reference(tmp_path)
(tmp_path / "ComfyUI_frontend" / "src" / "stores" / "workspace" / "sidebarTabStore.ts").write_text(
"defineStore('sidebarTab'\n",
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "current sidebarTab store API"
assert "const registerSidebarTab =" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_last_node_errors(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "types" / "extensionTypes.ts"
path.write_text(
path.read_text(encoding="utf-8").replace("lastNodeErrors", "legacyNodeErrors"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "extensionManager execution error state"
assert "lastNodeErrors" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_system_stats_package_versions(tmp_path):
_create_minimal_reference(tmp_path)
server_path = tmp_path / "ComfyUI" / "server.py"
server_path.write_text(
server_path.read_text(encoding="utf-8").replace("comfy_package_versions", "frontend_package_versions"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "system_stats current multi-device package versions"
assert "comfy_package_versions" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_current_model_folder_anchor(tmp_path):
_create_minimal_reference(tmp_path)
folder_paths_path = tmp_path / "ComfyUI" / "folder_paths.py"
folder_paths_path.write_text(
folder_paths_path.read_text(encoding="utf-8").replace('"geometry_estimation"', '"depth_estimation"'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "current model folder anchors"
assert '"geometry_estimation"' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_desktop_current_baseline(tmp_path):
_create_minimal_reference(tmp_path)
package_path = tmp_path / "desktop" / "package.json"
package_path.write_text(
package_path.read_text(encoding="utf-8").replace('"version": "0.22.3"', '"version": "0.21.0"'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "Desktop current bundled host baseline"
assert '"version": "0.22.3"' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_deprecated_sidebar_fallback(tmp_path):
_create_minimal_reference(tmp_path)
(tmp_path / "ComfyUI_frontend" / "src" / "stores" / "workspaceStore.ts").write_text(
"const sidebarTab = computed(() => useSidebarTabStore())\n",
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "deprecated sidebar wrapper fallback"
assert "sidebarTab.value.registerSidebarTab(tab)" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_api_route_duplication(tmp_path):
_create_minimal_reference(tmp_path)
server_path = tmp_path / "ComfyUI" / "server.py"
server_path.write_text(
server_path.read_text(encoding="utf-8").replace('"/api" + route.path', '"/v2" + route.path'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "host /api route duplication"
assert '"/api" + route.path' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_server_usage_source_pass_through(tmp_path):
_create_minimal_reference(tmp_path)
server_path = tmp_path / "ComfyUI" / "server.py"
server_path.write_text(
server_path.read_text(encoding="utf-8").replace('"Comfy-Usage-Source"', '"Legacy-Source"'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "prompt usage source pass-through"
assert 'request.headers.get("Comfy-Usage-Source")' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_execution_usage_source_hidden_input(tmp_path):
_create_minimal_reference(tmp_path)
execution_path = tmp_path / "ComfyUI" / "execution.py"
execution_path.write_text(
execution_path.read_text(encoding="utf-8").replace("io.Hidden.comfy_usage_source", "io.Hidden.legacy_source"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "execution usage source hidden input"
assert "io.Hidden.comfy_usage_source.name in hidden" in failed[0].missing_patterns
assert "hidden_inputs_v3[io.Hidden.comfy_usage_source]" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_frontend_queue_usage_source(tmp_path):
_create_minimal_reference(tmp_path)
api_path = tmp_path / "ComfyUI_frontend" / "src" / "scripts" / "api.ts"
api_path.write_text(
api_path.read_text(encoding="utf-8").replace("comfy_usage_source: 'comfyui-frontend'", "usage_source: 'legacy'"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "frontend queue prompt usage source"
assert "comfy_usage_source: 'comfyui-frontend'" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_expanded_model_folder_anchor(tmp_path):
_create_minimal_reference(tmp_path)
folder_paths_path = tmp_path / "ComfyUI" / "folder_paths.py"
folder_paths_path.write_text(
folder_paths_path.read_text(encoding="utf-8").replace('"background_removal"', '"legacy_background"'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "current expanded model folder anchors"
assert '"background_removal"' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_executed_asset_enrichment_anchor(tmp_path):
_create_minimal_reference(tmp_path)
execution_path = tmp_path / "ComfyUI" / "execution.py"
execution_path.write_text(
execution_path.read_text(encoding="utf-8").replace("enrich_output_with_assets(output_ui)", "output_ui"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "executed output asset enrichment tolerance"
assert "enrich_output_with_assets(output_ui)" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_system_stats_deploy_environment(tmp_path):
_create_minimal_reference(tmp_path)
server_path = tmp_path / "ComfyUI" / "server.py"
server_path.write_text(
server_path.read_text(encoding="utf-8").replace("get_deploy_environment", "get_legacy_environment"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "system_stats deploy environment"
assert "get_deploy_environment" in failed[0].missing_patterns
assert '"deploy_environment": get_deploy_environment()' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_jobs_cancel_endpoint(tmp_path):
_create_minimal_reference(tmp_path)
server_path = tmp_path / "ComfyUI" / "server.py"
server_path.write_text(
server_path.read_text(encoding="utf-8").replace("/api/jobs/{job_id}/cancel", "/api/jobs/{job_id}/stop"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "jobs namespace cancel endpoints"
assert '@routes.post("/api/jobs/{job_id}/cancel")' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_prompt_queue_interrupt_hook(tmp_path):
_create_minimal_reference(tmp_path)
execution_path = tmp_path / "ComfyUI" / "execution.py"
execution_path.write_text(
execution_path.read_text(encoding="utf-8").replace("def interrupt_if_running", "def legacy_interrupt"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "prompt queue running job interrupt hook"
assert "def interrupt_if_running" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_jobs_cancel_classification_helper(tmp_path):
_create_minimal_reference(tmp_path)
jobs_path = tmp_path / "ComfyUI" / "comfy_execution" / "jobs.py"
jobs_path.write_text(
jobs_path.read_text(encoding="utf-8").replace("CANCEL_TERMINAL", "CANCEL_DONE"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "jobs cancel classification helpers"
assert "CANCEL_TERMINAL" in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_enable_telemetry_feature_flag(tmp_path):
_create_minimal_reference(tmp_path)
feature_flags_path = tmp_path / "ComfyUI" / "comfy_api" / "feature_flags.py"
feature_flags_path.write_text(
feature_flags_path.read_text(encoding="utf-8").replace('"enable_telemetry":', '"legacy_telemetry":'),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "server feature flags"
assert '"enable_telemetry":' in failed[0].missing_patterns
def test_host_compatibility_smoke_reports_missing_frontend_jobs_cancel_api(tmp_path):
_create_minimal_reference(tmp_path)
api_path = tmp_path / "ComfyUI_frontend" / "src" / "scripts" / "api.ts"
api_path.write_text(
api_path.read_text(encoding="utf-8").replace("async cancelJobs", "async stopJobs"),
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
failed = [result for result in results if not result.ok]
assert len(failed) == 1
assert failed[0].check.label == "frontend jobs cancel API"
assert "async cancelJobs(" in failed[0].missing_patterns
PRE_T28_CHECK_LABELS = {
"custom node prestartup loading",
"WEB_DIRECTORY registration",
"extension static routes",
"PromptServer route registry",
"host /api route duplication",
"feature-flag websocket exchange",
"system_stats current multi-device package versions",
"system_stats deploy environment",
"jobs namespace cancel endpoints",
"prompt usage source pass-through",
"execution_error websocket payload",
"execution usage source hidden input",
"executed output asset enrichment tolerance",
"prompt queue running job interrupt hook",
"jobs cancel classification helpers",
"progress_state lineage payload",
"current model folder anchors",
"current expanded model folder anchors",
"server feature flags",
"extensionManager execution error state",
"frontend queue prompt usage source",
"frontend jobs cancel API",
"extensionManager settings/sidebar API",
"deprecated sidebar wrapper fallback",
"current sidebarTab store API",
"frontend execution_error schema",
"frontend rootGraph API",
"Desktop current bundled host baseline",
"Desktop base/user/input/output directories",
"Desktop managed .venv layout",
"Desktop valid basePath shape",
"Desktop settings path",
}
def _failed_results(root: Path):
return [result for result in host_compat.run_checks(root) if not result.ok]
def test_t28_preserves_all_preexisting_surface_checks():
labels = {check.label for check in host_compat.CHECKS}
assert len(PRE_T28_CHECK_LABELS) == 32
assert PRE_T28_CHECK_LABELS <= labels
def test_t28_records_three_distinct_frontend_runtime_lanes():
lanes = getattr(host_compat, "FRONTEND_RUNTIME_LANES", ())
assert [
(lane.id, lane.version, lane.setting_change_telemetry)
for lane in lanes
] == [
("desktop-0.9.4", "1.43.18", False),
("core-pin-1.48.7", "1.48.7", True),
("standalone-1.50.3+", "1.50.3+", True),
]
def test_t28_runtime_lane_source_versions_fail_in_isolation(tmp_path):
cases = (
(
"desktop",
Path("desktop/package.json"),
'"version": "1.43.18"',
'"version": "1.43.17"',
"frontend runtime lane: Desktop bundle",
),
(
"core",
Path("ComfyUI/requirements.txt"),
"comfyui-frontend-package==1.48.7",
"comfyui-frontend-package==1.48.6",
"frontend runtime lane: ComfyUI package pin",
),
(
"standalone",
Path("ComfyUI_frontend/package.json"),
'"version": "1.50.3"',
'"version": "1.50.2"',
"frontend runtime lane: standalone source",
),
)
for case_name, relative_path, current, mutated, expected_label in cases:
root = tmp_path / case_name
_create_minimal_reference(root)
path = root / relative_path
path.write_text(
path.read_text(encoding="utf-8").replace(current, mutated),
encoding="utf-8",
)
failed = _failed_results(root)
assert len(failed) == 1
assert failed[0].check.label == expected_label
assert current in failed[0].missing_patterns
def test_t28_reports_missing_model_type_tag_feature(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI" / "comfy_api" / "feature_flags.py"
path.write_text(
path.read_text(encoding="utf-8").replace(
'"supports_model_type_tags": True',
'"supports_legacy_model_tags": True',
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "model type tag feature flag"
assert '"supports_model_type_tags": True' in failed[0].missing_patterns
def test_t28_reports_missing_models_directory_cli_override(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI" / "comfy" / "cli_args.py"
path.write_text(
path.read_text(encoding="utf-8").replace("--models-directory", "--legacy-models-directory"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "models directory CLI override"
assert 'parser.add_argument("--models-directory"' in failed[0].missing_patterns
def test_t28_rejects_client_id_in_cached_history_query_contract(tmp_path):
_create_minimal_reference(tmp_path)
path = (
tmp_path
/ "ComfyUI_frontend"
/ "src"
/ "platform"
/ "remote"
/ "comfyui"
/ "jobs"
/ "fetchJobs.ts"
)
path.write_text(
path.read_text(encoding="utf-8").replace(
"&offset=${offset}`",
"&offset=${offset}&client_id=synthetic`",
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "cached history query without client id"
assert "client_id" in failed[0].present_forbidden_patterns
def test_t28_reports_missing_setting_telemetry_defaults(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "platform" / "settings" / "settingStore.ts"
path.write_text(
path.read_text(encoding="utf-8").replace(
"const trackChanges = telemetry?.trackChanges ?? isVisible",
"const trackChanges = false",
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "setting telemetry defaults"
assert "const trackChanges = telemetry?.trackChanges ?? isVisible" in failed[0].missing_patterns
def test_t28_reports_missing_setting_telemetry_opt_out_type(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "platform" / "settings" / "types.ts"
path.write_text(
path.read_text(encoding="utf-8").replace("trackChanges: false", "trackChanges: true"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "setting telemetry opt-out type"
assert "trackChanges: false" in failed[0].missing_patterns
def test_t28_reports_missing_cloud_telemetry_initialization_gate(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "platform" / "telemetry" / "initTelemetry.ts"
path.write_text(
path.read_text(encoding="utf-8").replace("if (!IS_CLOUD_BUILD) return", "if (false) return"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "cloud telemetry initialization gate"
assert "if (!IS_CLOUD_BUILD) return" in failed[0].missing_patterns
def test_t28_reports_missing_host_telemetry_initialization_gate(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "platform" / "telemetry" / "initHostTelemetry.ts"
path.write_text(
path.read_text(encoding="utf-8").replace(
"remoteConfig.value.enable_telemetry === true",
"remoteConfig.value.enable_telemetry !== false",
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "host telemetry initialization gate"
assert "remoteConfig.value.enable_telemetry === true" in failed[0].missing_patterns
def test_t28_reports_missing_real_subgraph_node_shape(tmp_path):
_create_minimal_reference(tmp_path)
path = (
tmp_path
/ "ComfyUI_frontend"
/ "src"
/ "lib"
/ "litegraph"
/ "src"
/ "subgraph"
/ "SubgraphNode.ts"
)
path.write_text(
path.read_text(encoding="utf-8").replace("override isSubgraphNode()", "override isLegacyNode()"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "real SubgraphNode public shape"
assert "override isSubgraphNode()" in failed[0].missing_patterns
def test_t28_reports_missing_boundary_error_provenance(tmp_path):
_create_minimal_reference(tmp_path)
path = (
tmp_path
/ "ComfyUI_frontend"
/ "src"
/ "core"
/ "graph"
/ "subgraph"
/ "liftNodeErrorsToBoundary.ts"
)
path.write_text(
path.read_text(encoding="utf-8").replace("source_execution_id", "legacy_execution_id"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "boundary error source provenance"
assert "source_execution_id: executionId" in failed[0].missing_patterns
def test_t28_reports_missing_surfaced_error_derivation(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI_frontend" / "src" / "stores" / "executionErrorStore.ts"
path.write_text(
path.read_text(encoding="utf-8").replace(
"liftNodeErrorsToBoundary(app.rootGraph, lastNodeErrors.value)",
"lastNodeErrors.value",
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "surfaced error derivation"
assert "liftNodeErrorsToBoundary(app.rootGraph, lastNodeErrors.value)" in failed[0].missing_patterns
def test_t28_reports_missing_nested_promoted_model_serialization(tmp_path):
_create_minimal_reference(tmp_path)
path = (
tmp_path
/ "ComfyUI_frontend"
/ "browser_tests"
/ "assets"
/ "missing"
/ "missing_model_nested_promoted_widget.json"
)
path.write_text(
path.read_text(encoding="utf-8").replace('"subgraphs"', '"legacy_subgraphs"'),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "nested promoted missing-model serialization"
assert '"subgraphs"' in failed[0].missing_patterns
def test_t28_reports_missing_expanded_desktop_topology(tmp_path):
cases = (
(
"package",
Path("desktop/todesktop.json"),
'"extraResources"',
'"legacyResources"',
"Desktop packaged resource topology",
),
(
"bundled",
Path("desktop/src/config/comfyServerConfig.ts"),
"parsedConfig.desktop_extensions = { custom_nodes: customNodesPath }",
"parsedConfig.legacy_extensions = { custom_nodes: customNodesPath }",
"Desktop bundled extension topology",
),
(
"user",
Path("desktop/src/services/cmCli.ts"),
"path.join(this.virtualEnvironment.basePath, 'custom_nodes')",
"path.join(this.virtualEnvironment.basePath, 'legacy_nodes')",
"Desktop user custom-node restore topology",
),
)
for case_name, relative_path, current, mutated, expected_label in cases:
root = tmp_path / case_name
_create_minimal_reference(root)
path = root / relative_path
path.write_text(
path.read_text(encoding="utf-8").replace(current, mutated),
encoding="utf-8",
)
failed = _failed_results(root)
assert len(failed) == 1
assert failed[0].check.label == expected_label
assert current in failed[0].missing_patterns
def test_t28_new_checks_report_source_revision_and_applicable_lanes(tmp_path):
_create_minimal_reference(tmp_path)
new_checks = [check for check in host_compat.CHECKS if check.label not in PRE_T28_CHECK_LABELS]
assert len(new_checks) >= 16
assert all(check.source_revision for check in new_checks)
assert all(check.applicable_lanes for check in new_checks)
formatted = host_compat.format_results(host_compat.run_checks(tmp_path))
assert "Frontend runtime matrix:" in formatted
assert "desktop-0.9.4: frontend 1.43.18" in formatted
assert "core-pin-1.48.7: frontend 1.48.7" in formatted
assert "standalone-1.50.3+: frontend 1.50.3+" in formatted
assert "Source revision:" in formatted
assert "Applies to:" in formatted
def test_t28_reference_source_is_never_executed(tmp_path):
_create_minimal_reference(tmp_path)
marker = tmp_path / "reference-side-effect-marker"
main_path = tmp_path / "ComfyUI" / "main.py"
main_path.write_text(
main_path.read_text(encoding="utf-8")
+ "\nfrom pathlib import Path\n"
+ f"Path({str(marker)!r}).write_text('executed', encoding='utf-8')\n",
encoding="utf-8",
)
results = host_compat.run_checks(tmp_path)
assert all(result.ok for result in results)
assert not marker.exists()
def test_t30_records_current_revisions_and_contract_families():
labels = {check.label for check in host_compat.CHECKS}
assert host_compat.COMFYUI_REVISION == "dd79c643a95402136a75a28f6187d843bcf457ed" # pragma: allowlist secret
assert host_compat.FRONTEND_REVISION == "bdc0345da4610f16b4102f7e4628905b09b165ac" # pragma: allowlist secret
assert {
"dual positional and named widget serialization",
"partner node validation classification",
"partner policy 403 handling",
"datasets folder registration",
"first-party dataset node registrations",
"DETAIL logging CLI contract",
"DETAIL file logging bootstrap",
"minimum supported PyTorch version",
"named widget restore global default",
"named widget restore setting boundary",
"named widget restore authority branch",
"promoted widget concrete source resolution",
"missing media upload spec classification",
} <= labels
def test_t29_new_contract_anchors_fail_in_isolation(tmp_path):
cases = (
(
"named-widgets",
Path("ComfyUI_frontend/src/lib/litegraph/src/LGraphNode.ts"),
"o.widgets_values_named[widget.name] = serialisedVal",
"o.legacy_widget_values[widget.name] = serialisedVal",
"dual positional and named widget serialization",
),
(
"partner-classification",
Path("ComfyUI_frontend/src/utils/executionErrorUtil.ts"),
"'PARTNER_NODE_DISABLED'",
"'LEGACY_PARTNER_DISABLED'",
"partner node validation classification",
),
(
"partner-policy",
Path("ComfyUI_frontend/src/scripts/app.ts"),
"error.status === 403",
"error.status === 400",
"partner policy 403 handling",
),
(
"datasets-folder",
Path("ComfyUI/folder_paths.py"),
'folder_names_and_paths["datasets"] =',
'folder_names_and_paths["legacy_datasets"] =',
"datasets folder registration",
),
(
"dataset-nodes",
Path("ComfyUI/comfy_extras/nodes_dataset.py"),
'node_id="LoadTrainingDataset"',
'node_id="LoadLegacyTrainingDataset"',
"first-party dataset node registrations",
),
(
"detail-cli",
Path("ComfyUI/comfy/cli_args.py"),
"def get_file_log_outputs",
"def get_legacy_log_outputs",
"DETAIL logging CLI contract",
),
(
"detail-bootstrap",
Path("ComfyUI/main.py"),
"file_log_outputs = get_file_log_outputs(args.verbose)",
"file_log_outputs = get_legacy_log_outputs(args.verbose)",
"DETAIL file logging bootstrap",
),
)
for case_name, relative_path, current, mutated, expected_label in cases:
root = tmp_path / case_name
_create_minimal_reference(root)
path = root / relative_path
path.write_text(
path.read_text(encoding="utf-8").replace(current, mutated),
encoding="utf-8",
)
failed = _failed_results(root)
assert len(failed) == 1
assert failed[0].check.label == expected_label
assert current in failed[0].missing_patterns
def test_t30_current_contract_anchors_fail_in_isolation(tmp_path):
cases = (
(
"pytorch-minimum",
Path("ComfyUI/README.md"),
"torch 2.7 is minimally supported",
"torch 2.5 is minimally supported",
"minimum supported PyTorch version",
),
(
"named-global-default",
Path("ComfyUI_frontend/src/lib/litegraph/src/LiteGraphGlobal.ts"),
"namedValuesRestore: boolean = false",
"namedValuesRestore: boolean = true",
"named widget restore global default",
),
(
"named-setting",
Path("ComfyUI_frontend/src/platform/settings/constants/coreSettings.ts"),
" defaultValue: false,\n experimental: true",
" defaultValue: true,\n experimental: false",
"named widget restore setting boundary",
),
(
"named-authority",
Path("ComfyUI_frontend/src/lib/litegraph/src/LGraphNode.ts"),
"if (namedValues && LiteGraph.namedValuesRestore)",
"if (namedValues)",
"named widget restore authority branch",
),
(
"promoted-resolution",
Path("ComfyUI_frontend/src/core/graph/subgraph/resolveConcretePromotedWidget.ts"),
"resolved: { node: sourceNode, nodePath, widget: sourceWidget }",
"resolved: { node: hostNode, nodePath, widget: sourceWidget }",
"promoted widget concrete source resolution",
),
(
"promoted-execution-id",
Path("ComfyUI_frontend/src/core/graph/subgraph/resolveConcretePromotedWidget.ts"),
"createNodeExecutionId([...hostNodeIds, ...nodePath])",
"createNodeExecutionId(hostNodeIds)",
"promoted widget concrete source resolution",
),
(
"media-upload-flags",
Path("ComfyUI_frontend/src/platform/missingMedia/missingMediaScan.ts"),
"if (spec.audio_upload) return 'audio'",
"if (spec.legacy_audio_upload) return 'audio'",
"missing media upload spec classification",
),
)
for case_name, relative_path, current, mutated, expected_label in cases:
root = tmp_path / case_name
_create_minimal_reference(root)
path = root / relative_path
path.write_text(
path.read_text(encoding="utf-8").replace(current, mutated),
encoding="utf-8",
)
failed = _failed_results(root)
assert len(failed) == 1
assert failed[0].check.label == expected_label
assert any(current in missing for missing in failed[0].missing_patterns)
def test_t30_rejects_unconditional_default_detail_file(tmp_path):
_create_minimal_reference(tmp_path)
path = tmp_path / "ComfyUI" / "main.py"
path.write_text(
"file_log_outputs = [('DETAIL', 'comfyui_detail.log')]\n"
+ path.read_text(encoding="utf-8"),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "DETAIL file logging bootstrap"
assert "file_log_outputs = [('DETAIL', 'comfyui_detail.log')" in (
failed[0].present_forbidden_patterns
)