1888 lines
82 KiB
Python
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
|