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

1888 lines
82 KiB
Python

import importlib.util
import sys
from dataclasses import replace
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:
for relative, text in CURRENT_CONTRACT_FIXTURES.items():
_write(root / relative, text)
_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"
"def dynamic_vram_supported():\n"
" if comfy.model_management.is_nvidia():\n"
" return True\n"
" if comfy.model_management.is_amd():\n"
" if comfy.model_management.rocm_version >= (7, 14):\n"
" return True\n"
" return False\n"
"if args.enable_dynamic_vram or (enables_dynamic_vram() and dynamic_vram_supported()):\n"
" if (not args.enable_dynamic_vram) and "
"(comfy.model_management.torch_version_numeric < (2, 8)):\n"
" logging.warning('Unsupported Pytorch detected. DynamicVRAM support requires "
"Pytorch version 2.8 or later (2.12+ is recommended). Falling back to legacy "
"ModelPatcher. VRAM estimates may be unreliable especially on Windows')\n",
)
_write(root / "ComfyUI" / "nodes.py", "EXTENSION_WEB_DIRS = {}\nWEB_DIRECTORY = './web'\n")
_write(root / "ComfyUI" / "requirements.txt", "comfyui-frontend-package==1.57.1\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 = register_executed_outputs(output_ui, prompt_id, asset_manager, asset_previews)\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'
'"partner_run_gate_enabled": {\n'
' "type": "bool",\n'
' "default": True,\n'
'}\n'
"}\n"
"def get_server_features():\n return {}\n",
)
_write(
root / "ComfyUI" / "comfy_extras" / "nodes_sam3d_body.py",
"class SAM3DBody_Loader(io.ComfyNode):\n"
" node_id=\"SAM3DBody_Loader\"\n"
" model_file = io.Combo.Input(\n"
" \"model_file\",\n"
" options=folder_paths.get_filename_list(\"detection\"),\n"
" )\n"
" path = folder_paths.get_full_path_or_raise(\"detection\", model_file)\n",
)
_write(
root / "ComfyUI_frontend" / "package.json",
'{"name": "@comfyorg/comfyui-frontend", "version": "1.59.1"}\n',
)
_write(
root / "ComfyUI_frontend" / "src" / "types" / "extensionTypes.ts",
"registerSidebarTab(tab)\nsetting: {\nget: <T = unknown>(id: string) => undefined\n"
"set: (id: string, value: unknown) => 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" / "platform" / "remote" / "comfyui" / "execution" / "types.ts",
"export interface ExecutionErrorWsMessage extends ExecutionWsMessageBase {\n"
" node_id?: NodeId | null\n node_type: string\n traceback: string[]\n"
" exception_message: string\n exception_type: string\n"
" current_inputs?: unknown\n current_outputs?: unknown\n}\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",
"async function callHandler(setting, newValue, oldValue) {\n"
" try {\n"
" await setting?.onChange?.(newValue, oldValue)\n"
" } catch (error) {\n"
" console.warn(`[settings] onChange handler for ${setting?.id} failed`, error)\n"
" }\n"
"}\n"
"const handled = callHandler(setting, newValue, oldValue)\n"
"await handled\n"
"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<TValue = unknown> {\n"
" onChange?(newValue: TValue, oldValue?: TValue): void | Promise<void>\n"
" telemetry?: SettingTelemetryOptions\n"
"}\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",
"import { isHostTelemetryEnabled } from './hostTelemetryEnabled'\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",
"function serialiseWidgetValues(widgets: IBaseWidget[]) {\n"
" const positional = []\n"
" const named = {}\n"
" return { widgets_values: positional, widgets_values_named: named }\n"
"}\n"
"export function createWidgetRestorationState(info, fallbackNames) {\n"
" const named = info.widgets_values_named\n"
" return { restoreNamed: Boolean(\n"
" named && (LiteGraph.namedValuesRestore || fallbackNames)\n"
" ) }\n"
"}\n"
"const restoration = createWidgetRestorationState(\n"
" info,\n"
" this.constructor.nodeData?.fallbackWidgetsValuesNames\n"
")\n"
"Object.assign(o, serialiseWidgetValues(widgets))\n",
)
_write(
root / "ComfyUI_frontend" / "src" / "stores" / "widgetValueStore.ts",
"""\
function getRestoredWidgetValue(graphId, nodeId, widget, positionalIndex) {
const restoration = graphWidgetRestorations.get(graphId)?.get(nodeId)
if (!restoration) return
if (restoration.restoreNamed && restoration.named) {
if (!Object.hasOwn(restoration.named, widget.name)) return
return { value: coerceRestoredWidgetValue(widget, restoration.named[widget.name]) }
}
if (positionalIndex >= restoration.positional.length) return
return { value: coerceRestoredWidgetValue(
widget,
restoration.positional[positionalIndex]
) }
}
export function stripGraphPrefix(scopedId: SerializedNodeId): NodeId | null {
let rest = String(scopedId)
let separatorIndex = rest.indexOf(':')
while (
separatorIndex !== -1 &&
isUuidShapedSubgraphId(rest.slice(0, separatorIndex))
) {
rest = rest.slice(separatorIndex + 1)
separatorIndex = rest.indexOf(':')
}
return parseNodeId(rest)
}
""",
)
_write(
root
/ "ComfyUI_frontend"
/ "src"
/ "platform"
/ "billing"
/ "composables"
/ "usePartnerNodesRunGate.ts",
"export function partnerRunGateBlocksAutoQueue(): boolean {\n"
" if (isCloud) return false\n"
" if (!useFeatureFlags().flags.partnerRunGateEnabled) return false\n"
" if (isAuthResolving(apiKeyStore)) return false\n"
"function isAuthResolving(apiKeyStore: ApiKeyAuthStore): boolean\n"
"!useAuthStore().isInitialized ||\n"
"(Boolean(apiKeyStore.getApiKey()) && !apiKeyStore.isAuthenticated)\n"
" const partnerNodes = scanPartnerNodesInGraph()\n"
" if (partnerNodes.length === 0) return false\n"
" reportGateBlocked('auto-queue', partnerNodes, isLoggedIn.value)\n"
" return true\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"
/ "workflowErrors"
/ "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"
" const rootGraph = useApp().rootGraphOrUndefined\n"
" lastNodeErrors.value && rootGraph\n"
" ? liftNodeErrorsToBoundary(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("register_executed_outputs(output_ui, prompt_id, asset_manager, asset_previews)", "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 "register_executed_outputs(output_ui, prompt_id, asset_manager, asset_previews)" 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",
}
PRE_T32_CHECK_LABELS = PRE_T28_CHECK_LABELS | {
"frontend runtime lane: Desktop bundle",
"frontend runtime lane: ComfyUI package pin",
"frontend runtime lane: standalone source",
"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",
"model type tag feature flag",
"models directory CLI override",
"cached history query without client id",
"setting telemetry defaults",
"setting telemetry opt-out type",
"cloud telemetry initialization gate",
"host telemetry initialization gate",
"real SubgraphNode public shape",
"boundary error source provenance",
"surfaced error derivation",
"nested promoted missing-model serialization",
"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",
"Desktop packaged resource topology",
"Desktop bundled extension topology",
"Desktop user custom-node restore topology",
}
T32_NEW_CHECK_LABELS = {
"DynamicVRAM device applicability",
"DynamicVRAM PyTorch threshold",
"standalone settings onChange return type",
"standalone settings awaited handler containment",
}
PRE_T33_CHECK_LABELS = PRE_T32_CHECK_LABELS | T32_NEW_CHECK_LABELS
T33_NEW_CHECK_LABELS = {
"named widget restoration store precedence",
"SAM3D Body detection model root",
"frontend partner run gate ownership",
}
EXPECTED_HOST_REVISIONS = {
"ComfyUI": "19ae172bff6c9e4c738e7f2e00fe087f5b5b46ab", # pragma: allowlist secret
"ComfyUI_frontend": "43845693a1b72d16509e4eb7be6d1d95a1d3cdb2", # pragma: allowlist secret
"desktop": "e2d964b7456cea8423c7b9d3371c612313c06baa", # pragma: allowlist secret
}
def _failed_results(root: Path):
return [result for result in host_compat.run_checks(root) if not result.ok]
def _revision_metadata_failures(
*,
comfyui_revision=None,
frontend_revision=None,
lanes=None,
checks=None,
):
comfyui_revision = comfyui_revision or host_compat.COMFYUI_REVISION
frontend_revision = frontend_revision or host_compat.FRONTEND_REVISION
lanes = host_compat.FRONTEND_RUNTIME_LANES if lanes is None else lanes
checks = host_compat.CHECKS if checks is None else checks
failures = []
if comfyui_revision != EXPECTED_HOST_REVISIONS["ComfyUI"]:
failures.append("constant:ComfyUI")
if frontend_revision != EXPECTED_HOST_REVISIONS["ComfyUI_frontend"]:
failures.append("constant:ComfyUI_frontend")
for lane in lanes:
expected = EXPECTED_HOST_REVISIONS[lane.source_repo]
if lane.source_revision != expected:
failures.append(f"lane:{lane.id}")
for check in checks:
if not check.source_revision:
continue
expected = EXPECTED_HOST_REVISIONS[check.repo]
if check.source_revision != expected:
failures.append(f"check:{check.label}")
return failures
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,
lane.async_setting_on_change,
)
for lane in lanes
] == [
("desktop-0.9.4", "1.43.18", False, False),
("core-pin-1.57.1", "1.57.1", True, True),
("standalone-1.59.1", "1.59.1", True, 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.57.1",
"comfyui-frontend-package==1.49.5",
"frontend runtime lane: ComfyUI package pin",
),
(
"standalone",
Path("ComfyUI_frontend/package.json"),
'"version": "1.59.1"',
'"version": "1.52.0"',
"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(
"if (!isHostTelemetryEnabled()) return",
"if (false) return",
),
encoding="utf-8",
)
failed = _failed_results(tmp_path)
assert len(failed) == 1
assert failed[0].check.label == "host telemetry initialization gate"
assert "if (!isHostTelemetryEnabled()) return" 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(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(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.57.1: frontend 1.57.1" in formatted
assert "standalone-1.59.1: frontend 1.59.1" 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 == "19ae172bff6c9e4c738e7f2e00fe087f5b5b46ab" # pragma: allowlist secret
assert host_compat.FRONTEND_REVISION == "43845693a1b72d16509e4eb7be6d1d95a1d3cdb2" # 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_t32_preserves_62_prior_checks_and_adds_exactly_four_named_checks():
labels = {check.label for check in host_compat.CHECKS}
assert len(PRE_T32_CHECK_LABELS) == 62
assert labels >= PRE_T32_CHECK_LABELS
assert PRE_T33_CHECK_LABELS - PRE_T32_CHECK_LABELS == T32_NEW_CHECK_LABELS
def test_current_host_manifest_preserves_prior_checks_and_adds_named_contracts():
labels = {check.label for check in host_compat.CHECKS}
assert len(PRE_T33_CHECK_LABELS) == 66
assert labels >= PRE_T33_CHECK_LABELS
assert labels - PRE_T33_CHECK_LABELS == T33_NEW_CHECK_LABELS | CURRENT_NEW_CHECK_LABELS | ALIGNMENT_CHECK_LABELS
assert len(host_compat.CHECKS) == 78
def test_t32_revision_metadata_is_exact_and_stale_constants_fail_independently():
assert _revision_metadata_failures() == []
cases = (
(
{"comfyui_revision": "dd79c643a95402136a75a28f6187d843bcf457ed"}, # pragma: allowlist secret
["constant:ComfyUI"],
),
(
{"frontend_revision": "bdc0345da4610f16b4102f7e4628905b09b165ac"}, # pragma: allowlist secret
["constant:ComfyUI_frontend"],
),
)
for overrides, expected in cases:
assert _revision_metadata_failures(**overrides) == expected
def test_t32_stale_lane_and_surface_revision_metadata_fail_independently():
lanes = host_compat.FRONTEND_RUNTIME_LANES
core_index = next(index for index, lane in enumerate(lanes) if lane.source_repo == "ComfyUI")
mutated_lanes = list(lanes)
mutated_lanes[core_index] = replace(
lanes[core_index],
source_revision="dd79c643a95402136a75a28f6187d843bcf457ed", # pragma: allowlist secret
)
assert _revision_metadata_failures(lanes=tuple(mutated_lanes)) == ["lane:core-pin-1.57.1"]
checks = host_compat.CHECKS
check_index = next(
index for index, check in enumerate(checks) if check.label == "frontend runtime lane: standalone source"
)
mutated_checks = list(checks)
mutated_checks[check_index] = replace(
checks[check_index],
source_revision="bdc0345da4610f16b4102f7e4628905b09b165ac", # pragma: allowlist secret
)
assert _revision_metadata_failures(checks=tuple(mutated_checks)) == [
"check:frontend runtime lane: standalone source"
]
def test_t32_new_contract_anchors_fail_in_isolation(tmp_path):
cases = (
(
"dynamic-vram-nvidia-wsl",
Path("ComfyUI/main.py"),
"if comfy.model_management.is_nvidia():",
"if comfy.model_management.is_nvidia() and not comfy.model_management.is_wsl():",
"DynamicVRAM device applicability",
),
(
"dynamic-vram-amd-rocm",
Path("ComfyUI/main.py"),
"if comfy.model_management.rocm_version >= (7, 14):",
"if comfy.model_management.rocm_version >= (7, 15):",
"DynamicVRAM device applicability",
),
(
"dynamic-vram-torch-threshold",
Path("ComfyUI/main.py"),
"comfy.model_management.torch_version_numeric < (2, 8)",
"comfy.model_management.torch_version_numeric < (2, 9)",
"DynamicVRAM PyTorch threshold",
),
(
"base-torch-minimum",
Path("ComfyUI/README.md"),
"torch 2.7 is minimally supported",
"torch 2.8 is minimally supported",
"minimum supported PyTorch version",
),
(
"async-setting-type",
Path("ComfyUI_frontend/src/platform/settings/types.ts"),
"onChange?(newValue: TValue, oldValue?: TValue): void | Promise<void>",
"onChange?(newValue: TValue, oldValue?: TValue): void",
"standalone settings onChange return type",
),
(
"async-setting-handler-await",
Path("ComfyUI_frontend/src/platform/settings/settingStore.ts"),
"await setting?.onChange?.(newValue, oldValue)",
"setting?.onChange?.(newValue, oldValue)",
"standalone settings awaited handler containment",
),
(
"async-setting-write-order-await",
Path("ComfyUI_frontend/src/platform/settings/settingStore.ts"),
"await handled",
"void handled",
"standalone settings awaited handler containment",
),
)
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_t29_new_contract_anchors_fail_in_isolation(tmp_path):
cases = (
(
"named-widgets",
Path("ComfyUI_frontend/src/lib/litegraph/src/LGraphNode.ts"),
"return { widgets_values: positional, widgets_values_named: named }",
"return { widgets_values: positional, legacy_widget_values: named }",
"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"),
"named && (LiteGraph.namedValuesRestore || fallbackNames)",
"named && LiteGraph.namedValuesRestore",
"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/workflowErrors/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_t33_current_contract_anchors_fail_in_isolation(tmp_path):
cases = (
(
"dynamic-vram-recommendation",
Path("ComfyUI/main.py"),
"version 2.8 or later (2.12+ is recommended)",
"version 2.8 or later",
"DynamicVRAM PyTorch threshold",
),
(
"partner-feature-default",
Path("ComfyUI/comfy_api/feature_flags.py"),
'"partner_run_gate_enabled": {\n'
' "type": "bool",\n'
' "default": True,',
'"partner_run_gate_enabled": {\n'
' "type": "bool",\n'
' "default": False,',
"server feature flags",
),
(
"named-widget-store-precedence",
Path("ComfyUI_frontend/src/stores/widgetValueStore.ts"),
"if (restoration.restoreNamed && restoration.named) {",
"if (restoration.named) {",
"named widget restoration store precedence",
),
(
"sam3d-detection-root",
Path("ComfyUI/comfy_extras/nodes_sam3d_body.py"),
'options=folder_paths.get_filename_list("detection")',
'options=folder_paths.get_filename_list("checkpoints")',
"SAM3D Body detection model root",
),
(
"partner-auto-queue-gate",
Path("ComfyUI_frontend/src/platform/billing/composables/usePartnerNodesRunGate.ts"),
"export function partnerRunGateBlocksAutoQueue(): boolean {",
"export function partnerRunGateBlocksAfterQueue(): boolean {",
"frontend partner run gate ownership",
),
)
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
)
# Minimal inert source fragments; no fixture is imported or executed.
CURRENT_CONTRACT_FIXTURES = {
"ComfyUI/comfy_execution/asset_enrichment.py": "def register_executed_outputs(\nenriched = copy.deepcopy(output_ui)\nif not asset_manager.enabled:\nlambda path, job: asset_manager.register_executed_output(path, job, preview_ref=previews.get(path))\ndef register_cached_outputs(\nenriched = copy.deepcopy(ui_wrapper)\n_strip_ids(output_ui)\n_enrich_in_place(output_ui, job_id, asset_manager.register_cached_output)\nenriched = register_cached_outputs(cached.ui, prompt_id, asset_manager)\n\"display_node\": display_node_id\n",
"ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts": "export function isHostTelemetryEnabled(): boolean\nconst ENABLE_TELEMETRY_FEATURE = 'enable_telemetry'\ngetDevOverride<boolean>(ENABLE_TELEMETRY_FEATURE)\nif (override !== undefined) return override\nremoteConfig.value.enable_telemetry === true\n",
"ComfyUI/comfy_extras/nodes_logic.py": "node_id=\"ComfySwitchNode\"\nnode_id=\"ComfySoftSwitchNode\"\nselected = on_true if switch else on_false\nreturn io.NodeOutput(None if selected is MISSING else selected)\nio.MatchType.Input(\"on_false\", template=template, lazy=True, optional=True)\nio.MatchType.Input(\"on_false\", template=template, lazy=True, optional=True)\nio.MatchType.Input(\"on_true\", template=template, lazy=True, optional=True)\nio.MatchType.Input(\"on_true\", template=template, lazy=True, optional=True)\n",
"ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts": "const groupInputs: INodeInputSlot[] = node.inputs.filter(\n(inp) => inp.name in matchGroup\nconst connectedTypes = groupInputs.map(\nif (!link) return '*'\n...connectedTypes.slice(0, idx)\n...connectedTypes.slice(idx + 1)\ninput.type = combinedType\nconst typedSpec = { ...inputSpec, type: allowed_types }\naddNodeInput(node, typedSpec)\n",
"ComfyUI/comfy_execution/validation.py": "class LoopValidationError(Exception):\n\"type\": \"custom_validation_failed\"\n\"input_name\": \"loop boundary\"\n\"loop_error_type\": error_type\n\"node_ids\": sorted(node_ids)\n\"output_ids\": sorted(output_ids)\n",
"ComfyUI_frontend/src/composables/canvas/useFocusNode.ts": "const navigated = await navigationStore.navigateToGraph(node.graph)\nif (!navigated) return\nif (graphChanged) await waitForCanvasNavigation()\nconst activeCanvas = canvasStore.canvas\nactiveCanvas.graph !== node.graph\n!node.graph.nodes.includes(node)\nwindow.setTimeout(resolve, 100)\nactiveCanvas.animateToBounds(node.boundingRect\n",
}
CURRENT_NEW_CHECK_LABELS = {
"executed asset registration helper",
"host telemetry enablement helper",
"optional Switch branch contract",
"MatchType sibling input propagation",
"loop validation error envelope",
"focus navigation lifecycle",
}
CURRENT_CONTRACT_MUTATIONS = (
("ComfyUI/comfy_execution/asset_enrichment.py", "def register_executed_outputs(", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "enriched = copy.deepcopy(output_ui)", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "if not asset_manager.enabled:", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "lambda path, job: asset_manager.register_executed_output(path, job, preview_ref=previews.get(path))", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "def register_cached_outputs(", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "enriched = copy.deepcopy(ui_wrapper)", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "_strip_ids(output_ui)", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "_enrich_in_place(output_ui, job_id, asset_manager.register_cached_output)", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "enriched = register_cached_outputs(cached.ui, prompt_id, asset_manager)", "executed asset registration helper"),
("ComfyUI/comfy_execution/asset_enrichment.py", "\"display_node\": display_node_id", "executed asset registration helper"),
("ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts", "export function isHostTelemetryEnabled(): boolean", "host telemetry enablement helper"),
("ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts", "const ENABLE_TELEMETRY_FEATURE = 'enable_telemetry'", "host telemetry enablement helper"),
("ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts", "getDevOverride<boolean>(ENABLE_TELEMETRY_FEATURE)", "host telemetry enablement helper"),
("ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts", "if (override !== undefined) return override", "host telemetry enablement helper"),
("ComfyUI_frontend/src/platform/telemetry/hostTelemetryEnabled.ts", "remoteConfig.value.enable_telemetry === true", "host telemetry enablement helper"),
("ComfyUI/comfy_extras/nodes_logic.py", "node_id=\"ComfySwitchNode\"", "optional Switch branch contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "node_id=\"ComfySoftSwitchNode\"", "optional Switch branch contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "selected = on_true if switch else on_false", "optional Switch branch contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "return io.NodeOutput(None if selected is MISSING else selected)", "optional Switch branch contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "io.MatchType.Input(\"on_false\", template=template, lazy=True, optional=True)", "optional Switch branch contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "io.MatchType.Input(\"on_true\", template=template, lazy=True, optional=True)", "optional Switch branch contract"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "const groupInputs: INodeInputSlot[] = node.inputs.filter(", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "(inp) => inp.name in matchGroup", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "const connectedTypes = groupInputs.map(", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "if (!link) return '*'", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "...connectedTypes.slice(0, idx)", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "...connectedTypes.slice(idx + 1)", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "input.type = combinedType", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "const typedSpec = { ...inputSpec, type: allowed_types }", "MatchType sibling input propagation"),
("ComfyUI_frontend/src/core/graph/widgets/dynamicWidgets.ts", "addNodeInput(node, typedSpec)", "MatchType sibling input propagation"),
("ComfyUI/comfy_execution/validation.py", "class LoopValidationError(Exception):", "loop validation error envelope"),
("ComfyUI/comfy_execution/validation.py", "\"type\": \"custom_validation_failed\"", "loop validation error envelope"),
("ComfyUI/comfy_execution/validation.py", "\"input_name\": \"loop boundary\"", "loop validation error envelope"),
("ComfyUI/comfy_execution/validation.py", "\"loop_error_type\": error_type", "loop validation error envelope"),
("ComfyUI/comfy_execution/validation.py", "\"node_ids\": sorted(node_ids)", "loop validation error envelope"),
("ComfyUI/comfy_execution/validation.py", "\"output_ids\": sorted(output_ids)", "loop validation error envelope"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "const navigated = await navigationStore.navigateToGraph(node.graph)", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "if (!navigated) return", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "if (graphChanged) await waitForCanvasNavigation()", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "const activeCanvas = canvasStore.canvas", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "activeCanvas.graph !== node.graph", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "!node.graph.nodes.includes(node)", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "window.setTimeout(resolve, 100)", "focus navigation lifecycle"),
("ComfyUI_frontend/src/composables/canvas/useFocusNode.ts", "activeCanvas.animateToBounds(node.boundingRect", "focus navigation lifecycle"),
)
def test_current_contract_anchors_fail_independently(tmp_path):
for index, (relative, anchor, label) in enumerate(CURRENT_CONTRACT_MUTATIONS):
root = tmp_path / str(index)
_create_minimal_reference(root)
assert not _failed_results(root)
path = root / relative
text = path.read_text(encoding="utf-8")
assert anchor in text
path.write_text(text.replace(anchor, "removed_contract", 1), encoding="utf-8")
failed = _failed_results(root)
assert [r.check.label for r in failed] == [label], (label, anchor)
def test_current_relocated_contract_fields_fail_independently(tmp_path):
cases = (
("ComfyUI_frontend/src/types/extensionTypes.ts", "set: (id: string, value: unknown) => void", "extensionManager settings/sidebar API"),
("ComfyUI_frontend/src/platform/remote/comfyui/execution/types.ts", "node_id?: NodeId | null", "frontend execution_error schema"),
("ComfyUI_frontend/src/platform/remote/comfyui/execution/types.ts", "exception_message: string", "frontend execution_error schema"),
("ComfyUI_frontend/src/platform/remote/comfyui/execution/types.ts", "current_outputs?: unknown", "frontend execution_error schema"),
("ComfyUI_frontend/src/platform/telemetry/initHostTelemetry.ts", "import { isHostTelemetryEnabled } from './hostTelemetryEnabled'", "host telemetry initialization gate"),
("ComfyUI_frontend/src/platform/telemetry/initHostTelemetry.ts", "if (!isHostTelemetryEnabled()) return", "host telemetry initialization gate"),
("ComfyUI_frontend/src/stores/executionErrorStore.ts", "const rootGraph = useApp().rootGraphOrUndefined", "surfaced error derivation"),
("ComfyUI_frontend/src/stores/executionErrorStore.ts", "liftNodeErrorsToBoundary(rootGraph, lastNodeErrors.value)", "surfaced error derivation"),
)
for index, (relative, anchor, label) in enumerate(cases):
root = tmp_path / str(index)
_create_minimal_reference(root)
assert not _failed_results(root)
path = root / relative
text = path.read_text(encoding="utf-8")
assert anchor in text
path.write_text(text.replace(anchor, "removed_contract", 1), encoding="utf-8")
assert [r.check.label for r in _failed_results(root)] == [label]
def test_current_settings_contract_provenance_is_independent_of_package_pin():
expected = {
"desktop-0.9.4": ("e2d964b7456cea8423c7b9d3371c612313c06baa", False), # pragma: allowlist secret
"core-pin-1.57.1": ("c875ee420aa027a359b2953361eb3b356b7bef00", True), # pragma: allowlist secret
"standalone-1.59.1": ("43845693a1b72d16509e4eb7be6d1d95a1d3cdb2", True), # pragma: allowlist secret
} # pragma: allowlist secret
def mismatches(lanes):
return [
lane.id for lane in lanes
if (getattr(lane, "setting_contract_revision", None), lane.async_setting_on_change)
!= expected.get(lane.id)
]
lanes = host_compat.FRONTEND_RUNTIME_LANES
assert mismatches(lanes) == []
for index, lane in enumerate(lanes):
changed = list(lanes)
changed[index] = replace(lane, setting_contract_revision="stale")
assert mismatches(changed) == [lane.id]
changed[index] = replace(lane, async_setting_on_change=not lane.async_setting_on_change)
assert mismatches(changed) == [lane.id]
formatted = host_compat.format_results([])
assert "c875ee420aa027a359b2953361eb3b356b7bef00" in formatted # pragma: allowlist secret
ALIGNMENT_CHECK_LABELS = {
"opaque node identity preservation",
"Branch optional autogrow contract",
"asset preview extraction and cache stripping",
}
# Text fragments are inert and are never imported from the reference tree.
CURRENT_CONTRACT_FIXTURES["ComfyUI/comfy_execution/asset_enrichment.py"] += (
'def take_asset_previews(output_ui: dict) -> dict[str, dict]:\n'
'if not isinstance(entry, dict) or "asset_preview" not in entry:\n'
'preview = entry.pop("asset_preview")\n'
'entry.pop("id", None)\n'
'entry.pop("preview_id", None)\n'
)
CURRENT_CONTRACT_FIXTURES["ComfyUI/comfy_extras/nodes_logic.py"] += (
'node_id="BranchNode"\n'
'minput = io.MatchType.Input("branch", template=mtemplate, lazy=True, optional=True)\n'
'template = _io.Autogrow.TemplatePrefix(input=minput, prefix="branch", min=1, max=32)\n'
'_io.Autogrow.Input("autogrow", template=template)\n'
'key = f"branch{branch_names.index(branch)}"\n'
'if key not in autogrow:\n'
"return ['autogrow.' + key]\n"
)
def test_refreshed_host_contracts_fail_independently(tmp_path):
# All eight repaired labels plus each newly added guard are pinned here.
cases = (
("ComfyUI/execution.py", "register_executed_outputs(output_ui, prompt_id, asset_manager, asset_previews)", "executed output asset enrichment tolerance"),
("ComfyUI/requirements.txt", "comfyui-frontend-package==1.57.1", "frontend runtime lane: ComfyUI package pin"),
("ComfyUI_frontend/package.json", '\"version\": \"1.59.1\"', "frontend runtime lane: standalone source"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "Object.hasOwn(restoration.named, widget.name)", "named widget restoration store precedence"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "if (positionalIndex >= restoration.positional.length) return", "named widget restoration store precedence"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "value: coerceRestoredWidgetValue(widget, restoration.named[widget.name])", "named widget restoration store precedence"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "coerceRestoredWidgetValue(\n", "named widget restoration store precedence"),
("ComfyUI_frontend/src/platform/workflowErrors/missingMedia/missingMediaScan.ts", "if (spec.video_upload) return 'video'", "missing media upload spec classification"),
("ComfyUI_frontend/src/stores/executionErrorStore.ts", "const rootGraph = useApp().rootGraphOrUndefined", "surfaced error derivation"),
("ComfyUI_frontend/src/platform/billing/composables/usePartnerNodesRunGate.ts", "export function partnerRunGateBlocksAutoQueue(): boolean {", "frontend partner run gate ownership"),
("ComfyUI_frontend/src/platform/billing/composables/usePartnerNodesRunGate.ts", "if (isAuthResolving(apiKeyStore)) return false", "frontend partner run gate ownership"),
("ComfyUI_frontend/src/platform/billing/composables/usePartnerNodesRunGate.ts", "(Boolean(apiKeyStore.getApiKey()) && !apiKeyStore.isAuthenticated)", "frontend partner run gate ownership"),
("ComfyUI/comfy_execution/asset_enrichment.py", "lambda path, job: asset_manager.register_executed_output(path, job, preview_ref=previews.get(path))", "executed asset registration helper"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "isUuidShapedSubgraphId(rest.slice(0, separatorIndex))", "opaque node identity preservation"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "rest = rest.slice(separatorIndex + 1)", "opaque node identity preservation"),
("ComfyUI_frontend/src/stores/widgetValueStore.ts", "return parseNodeId(rest)", "opaque node identity preservation"),
("ComfyUI/comfy_extras/nodes_logic.py", 'io.MatchType.Input("branch", template=mtemplate, lazy=True, optional=True)', "Branch optional autogrow contract"),
("ComfyUI/comfy_extras/nodes_logic.py", '_io.Autogrow.TemplatePrefix(input=minput, prefix="branch", min=1, max=32)', "Branch optional autogrow contract"),
("ComfyUI/comfy_extras/nodes_logic.py", "return ['autogrow.' + key]", "Branch optional autogrow contract"),
("ComfyUI/comfy_execution/asset_enrichment.py", 'def take_asset_previews(output_ui: dict) -> dict[str, dict]:', "asset preview extraction and cache stripping"),
("ComfyUI/comfy_execution/asset_enrichment.py", 'preview = entry.pop("asset_preview")', "asset preview extraction and cache stripping"),
("ComfyUI/comfy_execution/asset_enrichment.py", 'entry.pop("preview_id", None)', "asset preview extraction and cache stripping"),
)
covered = set()
for index, (relative, anchor, label) in enumerate(cases):
root = tmp_path / str(index)
_create_minimal_reference(root)
assert not _failed_results(root)
path = root / relative
source = path.read_text(encoding="utf-8")
assert anchor in source, (relative, anchor)
path.write_text(source.replace(anchor, "REMOVED_CONTRACT", 1), encoding="utf-8")
assert [result.check.label for result in _failed_results(root)] == [label]
covered.add(label)
assert len(covered) == 11
assert covered >= ALIGNMENT_CHECK_LABELS