Files
ttulttul-ComfyUI-Modal/tests/test_web_modal_toggle.py

905 lines
49 KiB
Python

"""Regression tests for the frontend Modal queue shim."""
from __future__ import annotations
from pathlib import Path
def _modal_toggle_source() -> str:
"""Return the current frontend extension source."""
return (Path(__file__).resolve().parents[1] / "web" / "modal_toggle.js").read_text(encoding="utf-8")
def test_synthetic_status_event_matches_comfyui_status_shape() -> None:
"""Synthetic status events should use the same detail shape as websocket status events."""
source = _modal_toggle_source()
assert 'dispatchSyntheticApiEvent("status", statusPayload(1));' in source
assert 'dispatchSyntheticApiEvent("status", statusPayload(0));' in source
def test_synthetic_preparation_events_avoid_native_node_execution() -> None:
"""Preparation should refresh queue state without starting ComfyUI's watchdog."""
source = _modal_toggle_source()
assert 'dispatchSyntheticApiEvent("notification", {' in source
assert "Waiting for remote capacity." in source
assert 'dispatchSyntheticApiEvent("execution_start"' not in source
assert 'dispatchSyntheticApiEvent("executing"' not in source
assert 'dispatchSyntheticApiEvent("execution_error"' not in source
def test_remote_queue_returns_before_capacity_preparation_finishes() -> None:
"""ComfyUI should own the job while remote capacity initializes."""
source = _modal_toggle_source()
submission = source.index("const queueSubmission = (async () => {")
acknowledgement = source.index("prompt_id: promptId,", submission)
assert source.index("await this.fetchApi(MODAL_ROUTE", submission) < acknowledgement
assert "void queueSubmission.catch(() => {});" in source
assert "node_errors: {}," in source[acknowledgement:]
assert "function reportBackgroundQueueFailure(" in source
assert "Remote queue submission failed for prompt" in source
assert "error?.modalQueueResponse?.cancelled === true" in source
assert "handlePromptInterruption(promptId);" in source
assert "if (remoteNodeIds.length === 0)" in source
def test_global_modal_status_badge_is_installed() -> None:
"""The frontend should expose a dedicated global Modal activity indicator."""
source = _modal_toggle_source()
assert 'element.id = "comfy-modal-global-status";' in source
assert 'class="modal-status-gpu" hidden' in source
assert "gpuText.textContent = activeState.modalGpu ?? \"\";" in source
assert "font-size: 10px;" in source
assert "Preparing remote workflow" in source
assert "Waiting for Modal startup" in source
assert "Receiving remote outputs" in source
assert "Remote workflow running on" in source
assert "setGlobalStatusBatchProgress(promptId, value, maxValue)" in source
assert "batchValue: state.batchValue ?? null," in source
assert "batchMax: state.batchMax ?? null," in source
assert "statusMessage: state.statusMessage ?? null," in source
assert "statusCurrent: state.statusCurrent ?? null," in source
assert "statusTotal: state.statusTotal ?? null," in source
assert "modalGpu: state.modalGpu ?? null," in source
assert "linear-gradient(90deg" in source
assert "installGlobalStatusStyles()" in source
assert "function pruneGlobalStatusStates()" in source
assert "function effectiveGlobalStatusPhase(promptId, phase)" in source
assert "function promptHasLiveRemoteWork(promptId)" in source
assert "function reconcilePromptGlobalStatus(promptId)" in source
assert "function promptRemoteNodeCount(promptId, fallbackCount = 1)" in source
assert "nodeCount: promptRemoteNodeCount(promptId, nodeCount)," in source
assert "hasRemoteExecutionStarted: false" in source
assert "Waiting for Modal container" in source
assert "Starting remote component" in source
assert "Cancelling remote workflow" in source
assert "function promptActiveNodeIsLive(promptId)" in source
def test_configurator_renders_out_of_band_plan_and_status() -> None:
"""Configured workflows should expose planning before normal node execution."""
source = _modal_toggle_source()
assert "function serializedRemoteConfiguratorNodeId(prompt)" in source
assert "function mountRemoteExecutionConfiguratorPanel(node)" in source
assert "function removeStaleRemoteConfiguratorWidgets(node)" in source
assert "function makeRemoteConfiguratorWidgetResponsive(widget)" in source
assert "function registerPromptConfigurator(promptId, configuratorNodeId)" in source
assert "function registerRemoteConfiguratorPlan(promptId, assignments, configurations)" in source
assert "function renderRemoteConfiguratorStatus(panel, state)" in source
assert "function renderRemoteConfiguratorEnvironments(panel, assignments, configurations)" in source
assert "function promoteProvisionalVastEnvironment(" in source
assert "function remoteConfiguratorStorageEntries(configurations)" in source
assert "function renderRemoteConfiguratorStorage(panel, configurations)" in source
assert "function updateRemoteConfiguratorEnvironmentStatus(promptId, detail)" in source
assert 'statusMessage: "Waiting for Vast.ai setup"' in source
assert 'statusMessage: "Waiting for remote preparation"' in source
assert "environmentId === `vast:${configurationId}`" in source
assert "?? entry.initialState" in source
assert "promptState?.remoteEnvironmentStatuses.set(environmentId" in source
assert "function renderRemoteConfiguratorPlan(panel, assignments, configurations)" in source
assert 'widget = node.addDOMWidget(\n "remote_execution_plan"' in source
assert "serialize: false" in source
assert "getMinHeight: () => panel.minHeight" in source
assert "makeRemoteConfiguratorWidgetResponsive(widget)" in source
assert "max-width: 100%" in source
assert "align-self: stretch" in source
assert 'class="remote-configurator-targets"' in source
assert "function remoteConfiguratorTargetGroups(assignments, configurations)" in source
assert "function remoteConfiguratorHardwareLabels(hardware)" in source
assert "function createRemoteConfiguratorTargetSection(group)" in source
assert 'machine.className = "remote-configurator-target-machine";' in source
assert 'label.className = "remote-configurator-component-label";' in source
assert "hardware?.gpu_memory_bytes_per_device" in source
assert "hardware?.ram_capacity_label" in source
assert 'class="remote-configurator-environments"' in source
assert 'class="remote-configurator-storage"' in source
assert "Storage backends" in source
assert 'class="remote-configurator-storage-reload"' in source
assert ">Reload</button>" in source
assert 'const R2_STORAGE_USAGE_ROUTE = "/remote/storage/r2/usage";' in source
assert (
'const R2_KEYCHAIN_UNLOCK_ROUTE = "/remote/storage/r2/keychain/unlock";'
in source
)
assert 'const R2_KEYCHAIN_UNLOCK_REQUIRED_CODE = "keychain_unlock_required";' in source
assert "function refreshRemoteConfiguratorStorage(panel)" in source
assert "function unlockRemoteConfiguratorKeychain(panel)" in source
assert "function setRemoteConfiguratorKeychainUnlockRequired(panel, required)" in source
assert "function setRemoteConfiguratorStorageCredentialError(panel, code)" in source
assert 'class="remote-configurator-keychain-unlock"' in source
assert "Unlock Keychain" in source
assert "background: rgba(154, 52, 18, 0.32)" in source
assert 'label: "Bucket"' in source
assert 'label: "Storage used"' in source
assert 'label: "Jurisdiction"' in source
assert 'label: "Cache policy"' in source
assert 'label: "Key prefix"' in source
assert 'return "Cloudflare R2";' in source
assert 'return "Unavailable";' in source
assert "renderRemoteConfiguratorStorage(panel, configurations);" in source
assert source.index('class="remote-configurator-targets"') < source.index(
'class="remote-configurator-storage"'
)
assert "responsePayload.remote_execution_assignments" in source
assert "responsePayload.remote_execution_configurations" in source
assert "responsePayload.remote_execution_configurator_node_id" in source
assert "detail.remote_execution_assignments" in source
assert "refreshRemoteConfiguratorPanelForPrompt(promptId);" in source
assert "setRemoteConfiguratorTerminalStatus(promptId, STATE_COMPLETE);" in source
assert "const configuratorPanel = remoteConfiguratorPanel(" in source
assert 'element.style.display = "none";' in source
def test_frontend_defaults_modal_gpu_to_rtx_pro_6000() -> None:
"""Unsaved workflows should receive the requested RTX GPU default."""
source = _modal_toggle_source()
assert 'const DEFAULT_MODAL_GPU = "RTX-PRO-6000";' in source
def test_endpoint_chat_node_is_not_offered_nested_modal_execution() -> None:
"""The local endpoint client should not be wrapped in another Modal execution island."""
source = _modal_toggle_source()
assert '"ModalEndpointChat"' in source
assert '"VastAILeaseConfiguration"' in source
assert '"ModalRemoteConfiguration"' in source
assert '"VastRemoteConfiguration"' in source
assert '"SshRemoteConfiguration"' in source
assert '"R2StorageBackingConfiguration"' in source
assert '"RemoteExecutionConfigurator"' in source
assert "!LOCAL_MODAL_NODE_IDS.has(String(node.comfyClass))" in source
assert "!LOCAL_MODAL_NODE_IDS.has(String(nodeData.name))" in source
def test_empty_modal_status_events_do_not_show_global_pill() -> None:
"""Queue requests without Modal-enabled nodes should not create global Modal UI state."""
source = _modal_toggle_source()
assert "if (nodeIds.length === 0 && components.length === 0) {\n return;\n }" in source
def test_remote_modal_status_tracks_active_node_ids() -> None:
"""The frontend should track and highlight the currently active remote node."""
source = _modal_toggle_source()
assert "activeNodeId: null" in source
assert "hasStreamedProgress: false" in source
assert "descendantNodeIdsByAncestor: new Map()" in source
assert "function setPromptActiveNode(promptId, activeNodeId)" in source
assert "detail.active_node_id" in source
assert "clearPromptRemoteStates(promptId)" in source
def test_interrupted_prompts_clear_modal_ui_by_prompt_id() -> None:
"""Interrupt cleanup should not depend on native execution events naming a specific node."""
source = _modal_toggle_source()
assert "const modalTerminalPromptStates = new Map();" in source
assert "const modalCancellingPromptIds = new Set();" in source
assert "function isPromptTerminal(promptId)" in source
assert "function isPromptCancelling(promptId)" in source
assert "function markPromptTerminal(promptId, phase)" in source
assert 'if (detail.phase === "execution_interrupted") {' in source
assert "function handlePromptInterruption(promptId)" in source
assert "modalCancellingPromptIds.delete(promptId);" in source
assert 'markPromptTerminal(promptId, "execution_interrupted");' in source
assert "clearGlobalStatusPhase(promptId);" in source
assert "clearPromptRemoteStates(promptId);" in source
assert 'api.addEventListener("execution_interrupted", (event) => {' in source
assert "const promptId = String(eventDetail(event).prompt_id ?? \"\");" in source
assert "handlePromptInterruption(promptId);" in source
def test_error_prompts_clear_remote_node_visual_state() -> None:
"""Prompt errors should not leave stale remote node colors behind for the next submission."""
source = _modal_toggle_source()
assert "function clearPromptRemoteNodeVisuals(promptId)" in source
assert "for (const [nodeIdValue, state] of modalNodeStates.entries()) {" in source
assert "clearNodeTimer(nodeIdValue);" in source
assert "clearNodeProgress(nodeIdValue, promptId);" in source
assert "clearNodeCached(nodeIdValue, promptId);" in source
assert "modalNodeStates.delete(nodeIdValue);" in source
assert "clearPromptRemoteNodeVisuals(promptId);" in source
assert "setNodesPhase(componentNodeIds, STATE_ERROR, promptId, detail.exception_message);" not in source
def test_remote_modal_uses_distinct_ready_active_and_complete_colors() -> None:
"""The frontend should distinguish ready, active, and completed remote nodes visually."""
source = _modal_toggle_source()
assert 'const READY_ACTIVE_COMPONENT_BORDER_COLOR = "#22c55e";' in source
assert 'const READY_INACTIVE_COMPONENT_BORDER_COLOR = "#166534";' in source
assert 'const STARTING_BORDER_COLOR = "#eab308";' in source
assert 'const ACTIVE_BORDER_COLOR = "#a855f7";' in source
assert 'const COMPLETE_BORDER_COLOR = "#004FA4";' in source
assert 'const COMPLETE_FILL_COLOR = "#001C71";' in source
assert 'const FINALIZING_NODE_BORDER_COLOR = "#00358A";' in source
assert 'const CANCELLING_BORDER_COLOR = "#fb7185";' in source
assert 'const STATE_STARTING = "starting";' in source
assert 'const STATE_WAITING = "waiting";' in source
assert 'const STATE_FINALIZING = "finalizing";' in source
assert 'const STATE_CANCELLING = "cancelling";' in source
assert 'const STATE_READY = "ready";' in source
assert 'const STATE_ACTIVE = "active";' in source
assert 'detail.phase === "execution_success"' in source
assert "phase === STATE_COMPLETE || phase === STATE_FINALIZING" in source
assert "state?.phase === STATE_STARTING" in source
assert "dot.style.background = STARTING_BORDER_COLOR;" in source
assert "state?.phase === STATE_FINALIZING" in source
assert "borderColor = FINALIZING_NODE_BORDER_COLOR;" in source
assert "setNodesPhase(nodeIds, STATE_FINALIZING, promptId);" in source
def test_nodes_2_0_dom_nodes_receive_remote_state_decorations() -> None:
"""Nodes 2.0 should receive the same Modal palette as legacy nodes."""
source = _modal_toggle_source()
assert "function remoteDecorationPalette(state, elapsed)" in source
assert "function syncVueNodeDecorations(timestamp = performance.now())" in source
assert 'document.querySelectorAll(".lg-node[data-node-id]")' in source
assert 'nodeElement.dataset.modalPhase = phase;' in source
assert 'decoration.style.borderColor = palette?.borderColor ?? "transparent";' in source
assert (
'decoration.style.backgroundColor = palette?.fillColor ?? "transparent";'
in source
)
assert 'decoration.style.boxShadow = palette ?' in source
assert 'decoration.className = "comfy-modal-vue-node-decoration";' in source
assert 'badge.className = "comfy-modal-vue-node-badge";' in source
def test_nodes_2_0_decorations_follow_vue_mounts_and_visual_refreshes() -> None:
"""Late Vue mounts and visual refreshes should resynchronize DOM nodes."""
source = _modal_toggle_source()
assert "function installVueNodeDecorationObserver()" in source
assert "vueNodeObserver = new MutationObserver((records) => {" in source
assert 'addedNode.matches?.(".lg-node[data-node-id]")' in source
assert (
"vueNodeObserver.observe(document.body, { childList: true, subtree: true });"
in source
)
assert "function refreshNodeDecorations()" in source
assert "queueVueNodeDecorationSync();" in source
assert "installVueNodeDecorationObserver();" in source
assert source.count("app.graph?.setDirtyCanvas(true, true);") == 1
def test_cancel_click_shows_immediate_modal_cancelling_feedback() -> None:
"""Cancel should paint Modal prompts as cancelling before backend cleanup finishes."""
source = _modal_toggle_source()
assert "function patchInterruptFeedback()" in source
assert "api.fetchApi = async function modalFetchApi(resource, options) {" in source
assert "MODAL_CANCEL_PREPARATION_ROUTE" in source
assert 'body: JSON.stringify({ prompt_id: promptId })' in source
assert "function promptIdsFromInterruptRequest(resource, options)" in source
assert 'route.includes("/interrupt")' in source
assert "function markPromptCancellationRequested(promptId)" in source
assert "modalCancellingPromptIds.add(promptId);" in source
assert "setGlobalStatusPhase(promptId, STATE_CANCELLING" in source
assert "setNodesPhase(remoteNodeIds, STATE_CANCELLING, promptId);" in source
assert "function clearSupersededCancellingPrompts(activePromptId)" in source
assert 'markPromptTerminal(promptId, "superseded_by_new_prompt");' in source
assert "clearSupersededCancellingPrompts(promptId);" in source
assert "if (isPromptCancelling(promptId)) {\n return;\n }" in source
assert "patchInterruptFeedback();" in source
def test_starting_modal_status_marks_component_before_remote_progress() -> None:
"""Dispatch-time Modal status should mark the component while remote progress is still pending."""
source = _modal_toggle_source()
assert "if (detail.phase === STATE_STARTING) {" in source
assert "setGlobalStatusPhase(promptId, STATE_STARTING, nodeIds.length, {" in source
assert "setNodesPhase(nodeIds, STATE_STARTING, promptId);" in source
assert "phases.find((state) => state.phase === STATE_STARTING)" in source
assert "function isPulsingNodePhase(phase)" in source
assert "STATE_FINALIZING" in source
assert "activeState.phase === STATE_STARTING" in source
assert "STARTING_BORDER_COLOR" in source
def test_remote_modal_nodes_show_component_badges() -> None:
"""Remote nodes should render a compact component label badge."""
source = _modal_toggle_source()
assert "componentLabelByMember: new Map()," in source
assert "promptState.componentLabelByMember.set(componentNodeId, componentLabel);" in source
assert "componentLabel: state.isRemoteContainer" in source
assert '(state?.isRemoteContainer ? "Σ" : state?.componentLabel);' in source
assert "if (nodeBadgeText) {" in source
assert "ctx.arc(badgeX, badgeY, badgeRadius, 0, Math.PI * 2);" in source
assert "ctx.fillText(String(nodeBadgeText), badgeX, badgeY + 0.5 / scale);" in source
def test_planner_marks_sandwiched_local_nodes_without_error_styling() -> None:
"""Local re-entry advice should preserve default node colors and explain itself."""
source = _modal_toggle_source()
assert "const modalSandwichedLocalNodeIds = new Set();" in source
assert "function isSandwichedLocalNode(node)" in source
assert "function localBottleneckDecorationPalette()" not in source
assert "LOCAL_BOTTLENECK_BORDER_COLOR" not in source
assert "LOCAL_BOTTLENECK_FILL_COLOR" not in source
assert "LOCAL_BOTTLENECK_SHADOW_COLOR" not in source
assert 'const palette = localBottleneck ? null :' in source
assert 'decoration.style.borderColor = palette?.borderColor ?? "transparent";' in source
assert 'decoration.style.boxShadow = palette ?' in source
assert 'const phase = localBottleneck ? "local-bottleneck"' in source
assert "const nodeBadgeText = localBottleneck" in source
assert (
'const LOCAL_BOTTLENECK_TOOLTIP = "Did you mean to make this node execute '
'on Modal?";'
in source
)
assert "badge.title = localBottleneck" in source
assert "function localBottleneckBadgeContainsPoint(" in source
assert "updateLegacyModalTooltip(graphCanvas, tooltip);" in source
assert 'data-modal-phase="local-bottleneck"' in source
assert "pointer-events: auto;" in source
assert "cursor: help;" in source
assert "responsePayload.modal_sandwiched_local_node_ids" in source
assert "setSandwichedLocalNodeIds(sandwichedLocalNodeIds);" in source
def test_global_modal_status_badge_supports_setup_and_finalizing_details() -> None:
"""The frontend should surface detailed setup and result-receive messages in the global pill."""
source = _modal_toggle_source()
assert 'const FINALIZING_BORDER_COLOR = "#3b82f6";' in source
assert "detail.status_message ?? null" in source
assert "detail.status_current ?? null" in source
assert "detail.status_total ?? null" in source
assert "setGlobalStatusPhase(promptId, STATE_SETUP, nodeIds.length, {" in source
assert "if (detail.phase === STATE_FINALIZING) {" in source
assert "dot.style.background = FINALIZING_BORDER_COLOR;" in source
assert "READY_BORDER_COLOR" not in source
assert 'text.textContent = activeState.statusMessage ?? "Receiving remote outputs";' in source
def test_global_modal_status_badge_polls_and_renders_active_containers() -> None:
"""The global pill should adaptively poll and show every active Modal container."""
source = _modal_toggle_source()
assert 'const MODAL_CONTAINER_STATUS_ROUTE = MODAL_ROUTE.replace(' in source
assert '"/container_status"' in source
assert "function pollModalContainerStatus()" in source
assert "function modalContainerStatusPollDelay()" in source
assert "CONTAINER_STATUS_FAST_POLL_MS = 1500" in source
assert "CONTAINER_STATUS_STABLE_POLL_MS = 5000" in source
assert "CONTAINER_STATUS_HIDDEN_POLL_MS = 15000" in source
assert "CONTAINER_STATUS_MAX_BACKOFF_MS = 30000" in source
assert "document.visibilityState !== \"visible\"" in source
assert "modalContainerStatusUnchangedPolls >= 2" in source
assert "modalContainerStatusFailureCount > 0" in source
assert "function renderModalContainerStatuses(containerElement)" in source
assert "modalContainerStatuses.forEach((container, index) =>" in source
assert "Container ${index + 1} · ${state}" in source
assert 'class="modal-status-containers" hidden' in source
assert "requestImmediateModalContainerStatusPoll();" in source
assert "?modal_gpu=${encodeURIComponent(requestedModalGpu)}" in source
def test_configurator_lists_and_kills_exact_managed_capacity() -> None:
"""The Configurator should expose guarded exact-instance lifecycle controls."""
source = _modal_toggle_source()
assert 'const VAST_STATUS_ROUTE = "/remote/vast/status";' in source
assert 'const VAST_DESTROY_ROUTE = "/remote/vast/destroy";' in source
assert '"/container_stop"' in source
assert "function remoteManagedCapacityEntries(" in source
assert "function refreshRemoteManagedCapacity(" in source
assert "function killRemoteManagedCapacity(" in source
assert "function invalidateRemoteConfiguratorExecutionPlan(panel)" in source
assert "promptState.remoteExecutionPlanInvalidated = true;" in source
assert "Any workflow using it will fail. This cannot be undone." in source
assert "?include_billing=false" in source
assert 'class="remote-configurator-capacity-title">Managed capacity</div>' in source
assert 'button.textContent = "KILL";' in source
assert "{ instance_id: Number(entry.resourceId), force: true }" in source
assert 'for (const eventName of ["pointerdown", "pointermove", "pointerup", "wheel"])' in source
assert 'event.target?.closest?.("button")' in source
assert source.count("pointer-events: auto;") >= 3
assert ".remote-configurator-capacity-kill:active:not(:disabled)" in source
def test_global_modal_status_badge_estimates_prompt_gpu_cost() -> None:
"""The global pill should integrate active GPU-container seconds per prompt."""
source = _modal_toggle_source()
assert 'class="modal-status-cost" hidden' in source
assert "estimatedGpuCostPerSecond:" in source
assert "function modalPromptCostContainers(promptId, containers)" in source
assert "container.modalGpu === selectedModalGpu" in source
assert "function modalContainerGpuBurnRate(" in source
assert "function updateModalContainerCostEstimate(" in source
assert "function liveModalContainerCostEstimate()" in source
assert "previousIntervalSeconds *" in source
assert "modalContainerGpuBurnRate(previousPromptStatuses, promptId)" in source
assert "modalContainerCostUpdatedAtSeconds" in source
assert "Estimated GPU cost ${formatEstimatedModalUsd(estimatedCostUsd)}" in source
assert "formatEstimatedModalUsd(burnRatePerMinuteUsd)}/min" in source
assert "modalContainerStatusPromptId !== requestedPromptId" in source
def test_global_modal_status_badge_renders_hourly_app_billing() -> None:
"""The global pill should show cached actual billing for the selected GPU app."""
source = _modal_toggle_source()
assert 'class="modal-status-billing" hidden' in source
assert "function normalizedModalHourlyBillingStatus(payload)" in source
assert "appCostUsdBeforeCredits:" in source
assert "function renderModalHourlyBilling(billingElement, activeState)" in source
assert "Reported app cost ${reportedCost} · hour ending ${intervalEnd}" in source
assert "actual Modal metered cost before credits and reservations" in source
assert "Hourly reports exclude the partial current hour" in source
assert 'element.querySelector(".modal-status-billing")' in source
assert "renderModalHourlyBilling(billingText, activeState);" in source
def test_queue_success_marks_all_remote_nodes_ready_before_component_execution() -> None:
"""Once the Modal route accepts the prompt, all remote nodes should flip from setup to ready."""
source = _modal_toggle_source()
assert "const modalQueuedPromptIds = new Set();" in source
assert "function markPromptQueuedBehindActiveModal(promptId)" in source
assert "function isPromptQueuedBehindActiveModal(promptId)" in source
assert "const queuedBehindActiveModal =" in source
assert "if (!queuedBehindActiveModal) {" in source
assert "const resolvedRemoteNodeIds = (responsePayload.modal_remote_node_ids ?? []).map((nodeIdValue) =>" in source
assert "const resolvedComponents = Array.isArray(responsePayload.modal_components)" in source
assert "registerPromptComponents(promptId, resolvedRemoteNodeIds, resolvedComponents);" in source
assert "if (!promptState) {\n return responsePayload;\n }" in source
assert "endSyntheticExecutionUi(promptId);" in source
assert 'setGlobalStatusPhase(promptId, STATE_WAITING, acceptedRemoteNodeIds.length, {' in source
assert "message: remoteCapacityWaitingMessage(selectedProviders)," in source
assert "setNodesPhase(acceptedRemoteNodeIds, STATE_READY, promptId);" in source
def test_queue_failure_preserves_server_validation_detail() -> None:
"""Queue failures should preserve detail without faking a node execution error."""
source = _modal_toggle_source()
assert "function queueErrorMessage(error)" in source
assert "promptError.modalQueueResponse = responsePayload;" in source
assert "endSyntheticExecutionUi(promptId, true, message);" in source
assert "endSyntheticExecutionUi(promptId, true, errorMessage);" in source
assert 'dispatchSyntheticApiEvent("execution_error"' not in source
assert (
"setRemoteConfiguratorTerminalStatus(promptId, STATE_ERROR, errorMessage);"
in source
)
def test_completed_remote_nodes_clear_stale_global_status_entries() -> None:
"""The global pill should clear once a prompt has no active remote work left."""
source = _modal_toggle_source()
assert "promptState?.hasRemoteExecutionStarted &&\n nodeStates.some((state) => state.phase === STATE_READY)" in source
assert "nodeStates.some((state) => state.phase === STATE_READY || state.phase === STATE_COMPLETE)" not in source
assert "modalGlobalStatusStates.delete(promptId);" in source
assert "reconcilePromptGlobalStatus(promptId);" in source
assert "promptActiveNodeIsLive(promptId)" in source
assert "activeNodeState?.promptId === promptId && activeNodeState.phase === STATE_ACTIVE" in source
assert "if (promptState?.activeNodeId === String(currentNodeId))" in source
def test_streamed_modal_progress_takes_precedence_over_proxy_events() -> None:
"""Once streamed node progress starts, coarse proxy execution events should stop overriding it."""
source = _modal_toggle_source()
assert "promptState.hasStreamedProgress = true;" in source
assert "promptState.hasRemoteExecutionStarted = true;" in source
assert "if (promptState.hasStreamedProgress && phase === EXECUTION_PHASE)" in source
def test_streamed_modal_execution_ends_synthetic_setup_without_waiting_for_final_executed() -> None:
"""Real streamed execution should end synthetic setup on progress, not on the first executed node."""
source = _modal_toggle_source()
assert "if (detail.phase === EXECUTION_PHASE) {\n clearPromptQueued(promptId);\n endSyntheticExecutionUi(promptId);" in source
assert "function handleModalProgress(event)" in source
assert " endSyntheticExecutionUi(promptId);" in source
assert 'endSyntheticExecutionUi(String(eventDetail(event).prompt_id ?? ""));' not in source
def test_streamed_modal_node_progress_updates_active_overlay() -> None:
"""The frontend should listen for numeric Modal node progress and render it on the node."""
source = _modal_toggle_source()
assert 'const PROGRESS_FADE_MS = 900;' in source
assert "function fadeNodeProgress(nodeIdValue, promptId)" in source
assert "function progressVisualOpacity(progressState)" in source
assert 'api.addEventListener("modal_progress", handleModalProgress);' in source
def test_mixed_remote_environments_render_clipped_runtime_locations() -> None:
"""Mixed planner assignments should add provider identity beneath progress bars."""
source = _modal_toggle_source()
assert "function registerPromptExecutionAssignments(promptId, assignments)" in source
assert "promptState.scheduledEnvironmentCount = environmentIds.size;" in source
assert "updateNodeExecutionLocation(promptId, progressNodeId, detail);" in source
assert "updateNodeExecutionLocation(promptId, nextActiveNodeId, detail);" in source
assert "function drawRemoteExecutionLocation(" in source
assert "function updateVueExecutionLocation(decoration, state)" in source
assert 'text-overflow: ellipsis;' in source
assert "Number(state?.scheduledEnvironmentCount ?? 0) > 1" in source
assert "fitCanvasText(ctx, label, availableTextWidth)" in source
assert "REMOTE_LOCATION_ICON_SOURCES" in source
assert 'viewBox="0 0 54.3 46.28"' in source
assert 'viewBox="0 0 173.64 46.28"' not in source
assert "function drawProgressProviderIcon(" in source
assert "function handleModalProgress(event)" in source
assert "if (detail.aggregate_only) {" in source
assert "setNodeBatchProgress(" in source
assert (
"function setNodeProgress(nodeIdValue, promptId, value, maxValue, metadata = {})"
in source
)
assert '"KB/s",' in source
assert '"TB/s",' in source
assert "function formatByteRate(bytesPerSecond)" in source
assert "BYTE_PROGRESS_UNITS.has(normalizedUnit)" in source
assert "timeToFirstTokenSeconds" in source
assert "if (state.progress.indeterminate) {" in source
assert "preGpu: Boolean(metadata.pre_gpu)" in source
assert "state.phase === STATE_STARTING && state.progress.preGpu" in source
assert "function setNodeProgressLane(" in source
assert "metadata = {}," in source
assert "function clearNodeProgressLane(nodeIdValue, promptId, laneId)" in source
assert "function clearNodeProgress(nodeIdValue, promptId)" in source
assert "fadeNodeProgress(nodeIdValue, promptId);" in source
assert "function progressIterationRate(previousState, value, maxValue, updatedAt)" in source
assert "function formatIterationRate(iterationRate, unit = null)" in source
assert "function drawIterationRateOverlay(" in source
assert 'ctx.fillStyle = "rgba(0, 0, 0, 0.9)";' in source
assert "const progressIconColumnWidth = hasProgressProviderIcon ? 24 / scale : 0;" in source
assert "const progressBarWidth = Math.max(0, barWidth - progressIconColumnWidth);" in source
assert "iterationRateColumnWidth" not in source
assert ": progressIterationRate(" in source
assert "laneProgress.iterationRate," in source
assert "state.progress.iterationRate," in source
assert "state?.progress" in source
assert "state?.progressLanes" in source
assert "detail.lane_id != null" in source
assert "clearNodeProgressLane(progressNodeId, promptId, String(detail.lane_id));" in source
assert "setNodeProgressLane(" in source
assert "function deriveRemoteNodePhase(phase, hasLiveProgress)" in source
assert "isActiveRemoteNode: hasLiveProgress || promptState?.activeNodeId === visualNodeId," in source
assert "isActiveComponentMember: isNodeInActiveComponent(state.promptId, visualNodeId)," in source
assert "isCachedRemoteNode: Boolean(cachedState)," in source
assert 'const panelY = node.size[1] + 6 / scale;' in source
assert 'ctx.roundRect(-borderWidth, panelY, barWidth, panelHeight, 10 / scale);' in source
assert "const llmTimingLabel = aggregateProgress?.timeToFirstTokenSeconds" in source
def test_modal_canvas_animation_loop_is_selective_and_throttled() -> None:
"""Static progress data should not keep an unbounded canvas rAF loop alive."""
source = _modal_toggle_source()
assert "const MODAL_ANIMATION_FRAME_INTERVAL_MS = 100;" in source
assert "let modalLastAnimationRedrawAt = 0;" in source
assert "function shouldAnimateModalVisuals()" in source
assert "function progressNeedsAnimation()" in source
assert "function stopAnimationLoopIfIdle()" in source
assert "function isPulsingNodePhase(phase)" in source
assert "progressFadeNeedsAnimation(progressState)" in source
assert "if (laneProgress.setupOnly || progressFadeNeedsAnimation(laneProgress))" in source
assert "timestamp - modalLastAnimationRedrawAt >= MODAL_ANIMATION_FRAME_INTERVAL_MS" in source
assert "cancelAnimationFrame(animationFrameHandle);" in source
assert "const hasProgressState =" not in source
assert "const hasCachedPulse =" not in source
def test_streamed_modal_node_progress_fades_previous_active_node() -> None:
"""Only same-component progress should complete the previous active node."""
source = _modal_toggle_source()
assert "function nodesShareRemoteComponent(promptId, leftNodeId, rightNodeId)" in source
assert "const previousActiveNodeId = promptState.activeNodeId;" in source
assert "previousActiveNodeId !== progressNodeId" in source
assert "nodesShareRemoteComponent(promptId, previousActiveNodeId, progressNodeId)" in source
assert "fadeNodeProgress(previousActiveNodeId, promptId);" in source
assert "setNodesPhase([previousActiveNodeId], STATE_COMPLETE, promptId);" in source
assert "setPromptActiveNode(promptId, progressNodeId);" in source
def test_parallel_component_progress_does_not_complete_other_active_nodes() -> None:
"""Parallel component progress should not treat the prompt-wide previous active node as complete."""
source = _modal_toggle_source()
assert "nodesShareRemoteComponent(promptId, previousActiveNodeId, nextActiveNodeId)" in source
assert "nodesShareRemoteComponent(promptId, previousActiveNodeId, progressNodeId)" in source
assert "promptState.activeNodeId && nodeIds.includes(promptState.activeNodeId)" in source
def test_streamed_modal_node_progress_completes_reported_ancestors() -> None:
"""When a remote node becomes active, reported upstream nodes should be marked complete."""
source = _modal_toggle_source()
assert "function completeRemoteAncestorsBeforeActiveNode(promptId, ancestorNodeIds, activeNodeId)" in source
assert "detail.completed_ancestor_node_ids" in source
assert "setNodesPhase(completedNodeIds, STATE_COMPLETE, promptId);" in source
assert "completeRemoteAncestorsBeforeActiveNode(\n promptId,\n detail.completed_ancestor_node_ids,\n progressNodeId,\n );" in source
assert "completeRemoteAncestorsBeforeActiveNode(\n promptId,\n detail.completed_ancestor_node_ids,\n nextActiveNodeId,\n );" in source
def test_ready_updates_do_not_downgrade_completed_remote_nodes() -> None:
"""Coarse execution status should not turn already completed remote nodes green again."""
source = _modal_toggle_source()
assert "const existingState = modalNodeStates.get(currentNodeId);" in source
assert "phase === STATE_READY &&\n existingState?.promptId === promptId" in source
assert "[STATE_COMPLETE, STATE_FINALIZING, STATE_ERROR].includes(existingState.phase)" in source
def test_mapped_parallel_modal_progress_renders_multiple_lane_bars() -> None:
"""Parallel mapped Modal runs should render one local progress lane per active worker."""
source = _modal_toggle_source()
assert "const modalNodeProgressLanes = new Map();" in source
assert "const modalNodeBatchProgress = new Map();" in source
assert "visibleLaneProgress.length > 0" in source
assert "const laneColors = [" in source
assert 'const badgeText = hasBatchBadge' in source
assert 'const badgeY = panelY + panelPaddingY;' in source
assert "if (detail.clear) {" in source
assert "clearNodeProgress(progressNodeId, promptId);" in source
assert 'let laneY = barY;' in source
assert "laneNodeIdsByLane: new Map()," in source
assert "representativeNodeIdByMember: new Map()," in source
assert "function deleteNodeProgressLane(nodeIdValue, promptId, laneId)" in source
assert "function laneOwnerKey(promptId, nodeIdValue, laneId)" in source
assert "promptState.laneNodeIdsByLane.set(safeLaneKey, safeNodeIdValue);" in source
assert "const setupProgressLanes = progressLanes.filter((laneProgress) => laneProgress.setupOnly);" in source
assert "const activeProgressLanes = progressLanes.filter((laneProgress) => !laneProgress.setupOnly);" in source
assert "laneProgress.setupOnly" in source
assert 'laneColor.replace("0.94)", `${0.28 + elapsedPulse * 0.22})`)' in source
def test_mapped_lane_setup_events_render_placeholder_lane_bars() -> None:
"""Lane setup events should render translucent placeholder bars before real node progress arrives."""
source = _modal_toggle_source()
assert "function setNodeProgressLane(" in source
assert "provider: String(" in source
assert "setupOnly: Boolean(setupOnly)," in source
assert "Boolean(detail.setup_only)," in source
assert "const hasSetupLaneProgress = setupProgressLanes.length > 0;" in source
assert "const hasVisibleLaneProgress = visibleLaneProgress.length > 0;" in source
def test_ready_nodes_distinguish_active_component_members_and_cached_nodes() -> None:
"""Ready nodes should visually distinguish active-component membership and cache hits."""
source = _modal_toggle_source()
assert "function traceRoundedRectPath(ctx, x, y, width, height, radius)" in source
assert "function activeProgressNodeIds(promptId)" in source
assert "function isNodeInActiveComponent(promptId, nodeIdValue)" in source
assert "const promptActiveNodeId = promptState.activeNodeId;" in source
assert "for (const liveProgressNodeId of activeProgressNodeIds(promptId)) {" in source
assert "const liveProgressComponentNodeIds = resolveComponentNodeIds(promptId, liveProgressNodeId);" in source
assert "const descendantNodeIds = promptState.descendantNodeIdsByAncestor.get(safeNodeIdValue);" in source
assert "const pulseRate = state?.isCachedRemoteNode ? 2 : 6;" in source
assert "borderColor = state?.isActiveComponentMember" in source
assert "READY_INACTIVE_COMPONENT_BORDER_COLOR" in source
assert "READY_ACTIVE_COMPONENT_BORDER_COLOR" in source
assert "const cornerRadius = 12 / scale;" in source
assert "traceRoundedRectPath(" in source
assert "ctx.strokeRect(" not in source
def test_cached_node_hits_are_marked_without_fake_progress() -> None:
"""Cache-hit markers should be tracked separately from numeric progress."""
source = _modal_toggle_source()
assert "const modalNodeCachedStates = new Map();" in source
assert "function markNodeCached(nodeIdValue, promptId)" in source
assert "function clearNodeCached(nodeIdValue, promptId)" in source
assert "function nodeCachedState(nodeIdValue, promptId)" in source
assert "if (detail.cached_hit) {" in source
assert "const cachedNodeIds = new Set(" in source
assert "[detail.node_id, detail.display_node_id, detail.real_node_id]" in source
assert "for (const cachedNodeId of cachedNodeIds) {" in source
assert "markNodeCached(cachedNodeId, promptId);" in source
assert "function shouldAnimateModalVisuals()" in source
assert "const hasCachedPulse =" not in source
def test_modal_context_menu_can_expand_required_upstream_nodes() -> None:
"""Right-clicking a node should offer a dry-run expansion action backed by the backend."""
source = _modal_toggle_source()
assert 'const MODAL_ANALYZE_ROUTE = MODAL_ROUTE.replace(/\\/queue_prompt$/, "/analyze_remote_nodes");' in source
assert "function workflowNodePath(node)" in source
assert "function findNodeByWorkflowPath(workflowPath)" in source
assert "function selectedWorkflowNodePaths(node)" in source
assert "function analyzeAndSetUpstreamRemoteNodes(node, value)" in source
assert "function requestModalMaintenance(route, successMessage)" in source
assert "function installModalContextMenu(nodeType, nodeData)" in source
assert 'api.fetchApi(MODAL_ANALYZE_ROUTE, {' in source
assert 'const MODAL_DELETE_CACHES_ROUTE = MODAL_ROUTE.replace(/\\/queue_prompt$/, "/delete_caches");' in source
assert 'const MODAL_DELETE_VOLUME_ROUTE = MODAL_ROUTE.replace(/\\/queue_prompt$/, "/delete_volume");' in source
assert "async beforeRegisterNodeDef(nodeType, nodeData)" in source
assert "installModalContextMenu(nodeType, nodeData);" in source
assert 'content: "Remote Execution Tools"' in source
assert 'const REMOTE_WIDGET_NAME = "Run Remotely";' in source
assert 'const LEGACY_REMOTE_WIDGET_NAME = "Run on Modal";' in source
assert "has_submenu: true" in source
assert "submenu: {" in source
assert '"Enable on Upstream Nodes"' in source
assert '"Enable on Upstream Nodes for Selection"' in source
assert '"Disable on Upstream Nodes"' in source
assert '"Disable on Upstream Nodes for Selection"' in source
assert '"Enable All Nodes"' in source
assert '"Disable All Nodes"' in source
assert '"Delete Modal Caches"' in source
assert '"Delete Modal Volume"' in source
assert '"Modal: Enable on Upstream Nodes"' not in source
assert '"Modal: Disable on Upstream Nodes"' not in source
assert '"Modal: Enable All Nodes"' not in source
assert "analyzeAndSetUpstreamRemoteNodes(this, true)" in source
assert "analyzeAndSetUpstreamRemoteNodes(this, false)" in source
assert "setAllEligibleWorkflowNodesRemote(true);" in source
assert "setAllEligibleWorkflowNodesRemote(false);" in source
assert "requestModalMaintenance(" in source
assert "MODAL_DELETE_CACHES_ROUTE" in source
assert "MODAL_DELETE_VOLUME_ROUTE" in source
def test_modal_context_menu_marks_nodes_across_subgraphs() -> None:
"""The UI action should be able to resolve and mark nested workflow-node paths."""
source = _modal_toggle_source()
assert "function rootGraph()" in source
assert "function findSomethingInAllSubgraphs(matcher)" in source
assert "function allWorkflowNodes()" in source
assert "function findContainingSubgraphNode(subgraphId)" in source
assert "candidate.subgraph?.id === subgraphId" in source
assert "function setWorkflowNodePathsRemote(workflowNodePaths, value)" in source
assert "setRemoteFlag(node, value);" in source
assert "node.__modalToggleWidget.value = enabled;" in source
def test_modal_context_menu_can_set_all_eligible_nodes() -> None:
"""The graph-wide menu actions should toggle every eligible live workflow node."""
source = _modal_toggle_source()
assert "function setAllEligibleWorkflowNodesRemote(value)" in source
assert "for (const node of allWorkflowNodes()) {" in source
assert "if (!isEligibleNode(node)) {" in source
assert "setRemoteFlag(node, value);" in source
assert 'const actionLabel = value ? "Enabled" : "Disabled";' in source
assert "${actionLabel} Modal on ${appliedCount} node" in source
def test_prompt_component_registration_does_not_shrink_remote_node_count() -> None:
"""Per-component status updates should not overwrite the prompt-wide remote node list."""
source = _modal_toggle_source()
assert "const mergedRemoteNodeIds = new Set(remoteNodeIds.map((nodeIdValue) => String(nodeIdValue)));" in source
assert "if (promptState.remoteNodeIds.length === 0) {" in source
assert "const mergedRemoteNodeIds = new Set(promptState.remoteNodeIds);" in source
def test_prompt_cleanup_prunes_orphaned_global_status_entries() -> None:
"""Workflow cleanup should remove stale global badge states once prompt state is gone."""
source = _modal_toggle_source()
assert "pruneGlobalStatusStates();" in source
assert "refreshGlobalStatusElement();" in source
def test_subgraph_descendant_states_percolate_to_visible_ancestor_nodes() -> None:
"""Subgraph-expanded remote prompt ids should aggregate their phase onto visible ancestor nodes."""
source = _modal_toggle_source()
assert "function ancestorNodeIds(nodeIdValue)" in source
assert "function rebuildPromptAncestorMap(promptState)" in source
assert "function refreshAncestorNodePhase(promptId, ancestorNodeId, errorMessage)" in source
assert "promptState.descendantNodeIdsByAncestor.get(ancestorNodeId)" in source
assert "function remoteContainerVisualState(promptId, ancestorNodeId, errorMessage)" in source
assert "function dominantRemoteContainerPhase(phaseCounts)" in source
assert "isMixedRemoteContainer: phases.length > 1" in source
assert "remoteDescendantCount: descendantNodeIds.size" in source
def test_subgraph_containers_receive_recursive_idle_and_runtime_decorations() -> None:
"""Every containing subgraph should summarize marked descendants without becoming remote."""
source = _modal_toggle_source()
assert "const modalRemoteDescendantNodeIdsByAncestor = new Map();" in source
assert "function rebuildRemoteDescendantIndex()" in source
assert "for (const ancestorNodeId of ancestorNodeIds(remoteNodePath)) {" in source
assert "function hasRemoteDescendants(node)" in source
assert "state?.isRemoteContainer" in source
assert 'state?.isRemoteContainer ? "Σ"' in source
assert "function remoteContainerTooltip(state)" in source
assert 'nodeElement.dataset.modalContainer = "true";' in source
assert '[data-modal-container="true"]' in source
assert "function installNodeDecorationHooks(node)" in source
assert "installNodeDecorationHooks(node);" in source
def test_subgraph_nodes_resolve_visual_state_by_composed_workflow_path() -> None:
"""Inner subgraph nodes should read status stored under composed prompt ids."""
source = _modal_toggle_source()
assert "const visualNodeId = workflowNodePath(node) || nodeId(node);" in source
assert "const storedState = modalNodeStates.get(visualNodeId) ?? null;" in source
assert "const promptContainerState = storedState?.promptId" in source
assert "const progressState = nodeProgressState(visualNodeId, state.promptId);" in source
assert "const progressLanes = nodeProgressLanes(visualNodeId, state.promptId);" in source
assert "const cachedState = nodeCachedState(visualNodeId, state.promptId);" in source
assert "const batchProgressState = modalNodeBatchProgress.get(visualNodeId) ?? null;" in source
assert "const hasLiveProgress = hasLiveNodeProgress(visualNodeId, state.promptId);" in source
def test_modal_ui_refreshes_after_visibility_or_focus_returns() -> None:
"""Background-tab throttling should not leave the status pill stale after refocus."""
source = _modal_toggle_source()
assert 'const MODAL_PROGRESS_STATE_ROUTE = MODAL_ROUTE.replace(/\\/queue_prompt$/, "/progress_state");' in source
assert 'const COMFY_QUEUE_ROUTE = "/queue";' in source
assert 'const COMFY_HISTORY_ROUTE = "/history";' in source
assert "const REFOCUS_STALE_PROMPT_GRACE_MS = 30000;" in source
assert "function refreshModalUiAfterVisibilityChange()" in source
assert "function replayModalUiEventsAfterVisibilityChange()" in source
assert "function replayModalUiEvent(eventRecord)" in source
assert "modalReplayedEventUpdatedAtMs = Number.isFinite(updatedAtSeconds)" in source
assert "function reconcileModalUiAfterVisibilityChange()" in source
assert "function activeModalUiPromptIds()" in source
assert "for (const laneState of modalNodeProgressLanes.values())" in source
assert "for (const batchState of modalNodeBatchProgress.values())" in source
assert "function promptIdsFromQueuePayload(queuePayload)" in source
assert "function historyPromptTerminalOutcome(historyPayload, promptId)" in source
assert "function clearRefocusCompletedPrompt(promptId, outcome)" in source
assert "configuratorPanel.promptId === activeState.promptId" in source
assert "function clearPromptProgressStates(promptId)" in source
assert "clearPromptProgressStates(promptId);" in source
assert "stopAnimationLoopIfIdle();" in source
assert "for (const [nodeIdValue, state] of Array.from(modalNodeStates.entries()))" in source
assert "const startedAt = modalReplayedEventUpdatedAtMs ?? nowMs();" in source
assert "modalVisibilityRefreshInFlight" in source
assert "api.fetchApi(" in source
assert "fetchComfyJson(COMFY_QUEUE_ROUTE)" in source
assert "queuedPromptIds.has(promptId)" in source
assert "`${COMFY_HISTORY_ROUTE}/${encodeURIComponent(promptId)}`" in source
assert "clearRefocusCompletedPrompt(promptId, historyOutcome);" in source
assert 'message: "Remote workflow is no longer running"' in source
assert ".then(() => reconcileModalUiAfterVisibilityChange())" in source
assert "handleModalStatus({ detail: payload });" in source
assert "handleModalProgress({ detail: payload });" in source
assert 'document.addEventListener("visibilitychange"' in source
assert 'window.addEventListener("focus", refreshModalUiAfterVisibilityChange);' in source
assert "selectedModalGpu();\n refreshModalUiAfterVisibilityChange();" in source