1316 lines
50 KiB
Python
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
|
|
)
|