import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; const resolverSource = await readFile( new URL('../web/resolver.js', import.meta.url), 'utf8' ); const modelResolverSource = await readFile( new URL('../web/resolver/model_resolver.js', import.meta.url), 'utf8' ); test('frontend entrypoint registers the ComfyUI extension contract', () => { assert.match( resolverSource, /import\s+\{\s*app\s*\}\s+from\s+["']\.\.\/\.\.\/\.\.\/scripts\/app\.js["']/ ); assert.match(resolverSource, /registerGlobalHelpers\(\);/); assert.match(resolverSource, /const modelResolver = new ModelResolverClass\(\);/); assert.match(resolverSource, /app\.registerExtension\(\{/); assert.match(resolverSource, /name:\s*["']Model Resolver["']/); assert.match( modelResolverSource, /import\s+\{\s*isSidebarButtonActive\s*\}\s+from\s+["']\.\/utils\/dom_patch_utils\.js["']/ ); assert.match( modelResolverSource, /import\s+\{\s*fetchJson\s+as\s+fetchJsonRequest\s*\}\s+from\s+["']\.\/utils\/api_client\.js["']/ ); assert.match( modelResolverSource, /fetchJsonRequest\(\s*["']\/model_resolver\/workflow-model-hashes["']/ ); assert.match(modelResolverSource, /throwOnHttpError:\s*false/); assert.doesNotMatch( modelResolverSource, /api\.fetchApi\(\s*["']\/model_resolver\/workflow-model-hashes["']/ ); assert.match(resolverSource, /id:\s*MODEL_RESOLVER_OPEN_COMMAND_ID/); assert.match(resolverSource, /function:\s*\(\)\s*=>\s*modelResolver\.activateResolverButton\(\)/); assert.match(resolverSource, /keybindings:\s*\[\s*MODEL_RESOLVER_OPEN_DEFAULT_KEYBINDING/); const hookPatterns = { setup: /\bsetup\s*:/, beforeRegisterNodeDef: /\bbeforeRegisterNodeDef\s*\(/, nodeCreated: /\bnodeCreated\s*\(/, loadedGraphNode: /\bloadedGraphNode\s*\(/, afterConfigureGraph: /\bafterConfigureGraph\s*\(/, }; for (const [hook, pattern] of Object.entries(hookPatterns)) { assert.match( resolverSource, pattern, `expected ComfyUI lifecycle hook: ${hook}` ); } assert.equal( (resolverSource.match(/modelResolver\.configureNodeContextMenu\(node\?\.constructor\);/g) || []).length, 2, 'dynamically created and loaded node types must receive the widget-change hook' ); assert.match( resolverSource, /modelResolver\.dialog\?\.scheduleActiveWorkflowRefresh\?\.\('node-created'\)/, 'new nodes must notify the live workflow refresh mechanism' ); assert.match( modelResolverSource, /configureWorkflowNodeLifecycle\(node\)/, 'workflow nodes must receive removal tracking' ); });