feat(preflight): align inactive branch diagnostics

Refresh host compatibility anchors and governance expectations for current ComfyUI, frontend, and desktop references.

Add inactive branch suppression for workflow portability and preflight diagnostics, including Explorer rendering and regression coverage.

Validation: powershell -File scripts/run_full_tests_windows.ps1 passed.
This commit is contained in:
rookiestar28
2026-04-26 20:06:03 +08:00
parent da5fb1fcbd
commit 6f5d8c06e9
8 changed files with 496 additions and 43 deletions
+3 -2
View File
@@ -16,8 +16,9 @@ connector_state.json*
# Agent/local project exclusions
.pla*/
re*/
RE*/
reference/
REFERENCE/
.reference/
ROA*.md
roa*.md
AG*.md
+13 -13
View File
@@ -3,17 +3,17 @@
```openclaw-compat-matrix-meta
{
"anchors": {
"comfyui": "0.19.3",
"comfyui_frontend": "1.44.4",
"desktop": "0.8.32 (core 0.19.3 / frontend 1.42.11)"
"comfyui": "df22bcd5 (v0.19.3-41-gdf22bcd5 / frontend package 1.42.15)",
"comfyui_frontend": "1.44.10",
"desktop": "0.8.35 (core 0.19.5 / frontend 1.42.14)"
},
"evidence": {
"evidence_id": "compat-matrix-refresh-20260418",
"updated_at": "2026-04-18T10:23:56.704178+00:00",
"evidence_id": "compat-matrix-refresh-20260426",
"updated_at": "2026-04-26T00:00:00+00:00",
"updated_by": "manual"
},
"last_validated_date": "2026-04-18",
"matrix_version": "v0.2.2",
"last_validated_date": "2026-04-26",
"matrix_version": "v0.2.3",
"policy": {
"max_age_days": 45,
"warn_age_days": 30
@@ -28,17 +28,17 @@ This document tracks the current reference anchors and validated environments fo
| Component | Validated Range | Best Effort / Experimental | Notes |
| :--- | :--- | :--- | :--- |
| **ComfyUI** | `0.19.3` reference anchor | Older snapshots | Current upstream reference repo version used for compatibility review |
| **ComfyUI Frontend** | `1.44.4` reference anchor | Minor drift around the anchor | Sidebar extension contract (`registerSidebarTab`) still matches this repo |
| **ComfyUI Desktop** | `0.8.32 (core 0.19.3 / frontend 1.42.11)` reference anchor | Desktop bundle may lag standalone frontend | Treat desktop parity as a distinct host surface, not an alias of standalone frontend HEAD |
| **ComfyUI** | `df22bcd5` reference anchor (`v0.19.3-41-gdf22bcd5`, frontend package `1.42.15`) | Older snapshots | Current upstream reference repo snapshot used for compatibility review |
| **ComfyUI Frontend** | `1.44.10` reference anchor | Minor drift around the anchor | Sidebar extension contract (`registerSidebarTab`) still matches this repo |
| **ComfyUI Desktop** | `0.8.35 (core 0.19.5 / frontend 1.42.14)` reference anchor | Desktop bundle may lag standalone frontend | Treat desktop parity as a distinct host surface, not an alias of standalone frontend HEAD |
| **Python** | 3.10, 3.11, 3.12 | 3.9 | 3.13 not yet validated |
| **Torch** | 2.1.2+ | 1.13+ | CUDA 11.8/12.1 verified |
## Host-Surface Notes
- **ComfyUI host runtime**: current bootstrap assumptions remain aligned with upstream `PromptServer` startup and route registration flow.
- **Frontend host surface**: current sidebar integration contract remains compatible with the standalone frontend reference anchor, but nested-subgraph and promoted-widget behavior should be treated as a regression-sensitive seam.
- **Desktop host surface**: desktop currently embeds frontend `1.42.11`, which still lags the standalone frontend `1.44.4` reference. Validate desktop-specific behavior against the desktop anchor instead of assuming standalone-frontend parity.
- **ComfyUI host runtime**: current bootstrap assumptions remain aligned with upstream `PromptServer` startup and route registration flow, including `/api`-prefixed canonical API routing.
- **Frontend host surface**: current sidebar integration contract remains compatible with the standalone frontend reference anchor, while inactive subgraph diagnostics and promoted-widget behavior remain regression-sensitive seams.
- **Desktop host surface**: desktop currently embeds frontend `1.42.14`, which still lags the standalone frontend `1.44.10` reference. Validate desktop-specific behavior against the desktop anchor instead of assuming standalone-frontend parity.
## Operating Systems
+67 -8
View File
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Set, Tuple
from .workflow_portability import (
analyze_workflow_portability,
get_missing_node_fallback,
iter_workflow_diagnostic_nodes,
)
logger = logging.getLogger("ComfyUI-OpenClaw.services.preflight")
@@ -268,9 +269,17 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
"""
report = {
"ok": True,
"summary": {"missing_nodes": 0, "missing_models": 0, "invalid_inputs": 0},
"summary": {
"missing_nodes": 0,
"missing_models": 0,
"invalid_inputs": 0,
"suppressed_missing_nodes": 0,
"suppressed_missing_models": 0,
},
"missing_nodes": [],
"missing_models": [],
"suppressed_missing_nodes": [],
"suppressed_missing_models": [],
"invalid_inputs": [],
"notes": [],
"portability": {
@@ -278,6 +287,7 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
"export_mode": "advisory_metadata",
"summary": {
"openclaw_nodes": 0,
"suppressed_openclaw_nodes": 0,
"portable_mode_required": False,
"portable_mode_supported": False,
"requires_manual_rewire": False,
@@ -285,6 +295,7 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
"detected_class_types": [],
"recommended_actions": [],
"openclaw_nodes": [],
"suppressed_openclaw_nodes": [],
},
}
@@ -303,22 +314,57 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
inventory = _get_model_inventory()
missing_models_counts: Dict[str, Dict[str, Any]] = {}
for node_id, node_data in workflow.items():
for diagnostic_node in iter_workflow_diagnostic_nodes(workflow):
node_data = diagnostic_node.get("node_data")
if not isinstance(node_data, dict):
continue
node_id = str(diagnostic_node.get("node_id") or "")
active = bool(diagnostic_node.get("active", True))
inactive_reason = diagnostic_node.get("inactive_reason")
is_subgraph_container = bool(diagnostic_node.get("is_subgraph_container"))
# Check Node Class
class_type = node_data.get("class_type")
class_type = diagnostic_node.get("class_type")
if not class_type:
continue
if available_nodes and class_type not in available_nodes:
missing_node_counts[class_type] = missing_node_counts.get(class_type, 0) + 1
if (
available_nodes
and class_type not in available_nodes
and not is_subgraph_container
):
if not active:
item = {
"node_id": node_id,
"class_type": class_type,
"inactive_reason": inactive_reason or "inactive",
}
fallback = get_missing_node_fallback(class_type)
if fallback is not None:
item["fallback"] = fallback
report["suppressed_missing_nodes"].append(item)
else:
missing_node_counts[class_type] = (
missing_node_counts.get(class_type, 0) + 1
)
# Check Inputs for Models
inputs = node_data.get("inputs")
inputs = diagnostic_node.get("inputs")
if isinstance(inputs, dict):
_check_inputs_for_models(inputs, inventory, missing_models_counts)
if active:
_check_inputs_for_models(inputs, inventory, missing_models_counts)
else:
suppressed_counts: Dict[str, Dict[str, Any]] = {}
_check_inputs_for_models(inputs, inventory, suppressed_counts)
for info in suppressed_counts.values():
report["suppressed_missing_models"].append(
{
"node_id": node_id,
"type": info["type"],
"name": info["name"],
"count": info["count"],
"inactive_reason": inactive_reason or "inactive",
}
)
# Format Results
for cls in sorted(missing_node_counts):
@@ -336,6 +382,12 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
# Summarize
report["summary"]["missing_nodes"] = len(report["missing_nodes"])
report["summary"]["missing_models"] = len(report["missing_models"])
report["summary"]["suppressed_missing_nodes"] = len(
report["suppressed_missing_nodes"]
)
report["summary"]["suppressed_missing_models"] = len(
report["suppressed_missing_models"]
)
if (
report["summary"]["missing_nodes"] > 0
@@ -351,6 +403,13 @@ def run_preflight_check(workflow: Dict[str, Any]) -> Dict[str, Any]:
report["notes"].append(
"Portable mode guidance is available for missing OpenClaw nodes."
)
if (
report["summary"]["suppressed_missing_nodes"] > 0
or report["summary"]["suppressed_missing_models"] > 0
):
report["notes"].append(
"Inactive subgraph branches were suppressed from actionable diagnostics."
)
# F49: Inject Guidance Banners
# We serialize them so they are ready for JSON response
+157 -14
View File
@@ -20,6 +20,8 @@ else: # pragma: no cover (top-level test import mode)
get_node_portability_mappings,
)
INACTIVE_LITEGRAPH_MODES = {2, 4}
def get_workflow_portability_contract() -> Dict[str, Any]:
return {
@@ -46,30 +48,34 @@ def get_missing_node_fallback(class_type: str) -> Dict[str, Any] | None:
def analyze_workflow_portability(workflow: Dict[str, Any]) -> Dict[str, Any]:
contract = get_workflow_portability_contract()
entries = []
suppressed_entries = []
detected_class_types = set()
recommended_actions = []
for node_id, node_data in _iter_sorted_workflow_nodes(workflow):
class_type = node_data.get("class_type")
for node in iter_workflow_diagnostic_nodes(workflow):
class_type = node.get("class_type")
if not isinstance(class_type, str):
continue
metadata = contract["nodes"].get(class_type)
if metadata is None:
continue
item = {
"node_id": str(node["node_id"]),
"class_type": class_type,
"display_name": metadata["display_name"],
"portable_mode": metadata["portable_mode"],
"fallback_kind": metadata["fallback_kind"],
"portable_summary": metadata["portable_summary"],
"standard_field_targets": list(metadata["standard_field_targets"]),
"replacement_hints": list(metadata["replacement_hints"]),
}
if not node.get("active", True):
item["inactive_reason"] = node.get("inactive_reason") or "inactive"
suppressed_entries.append(item)
continue
detected_class_types.add(class_type)
recommended_actions.extend(metadata["replacement_hints"])
entries.append(
{
"node_id": str(node_id),
"class_type": class_type,
"display_name": metadata["display_name"],
"portable_mode": metadata["portable_mode"],
"fallback_kind": metadata["fallback_kind"],
"portable_summary": metadata["portable_summary"],
"standard_field_targets": list(metadata["standard_field_targets"]),
"replacement_hints": list(metadata["replacement_hints"]),
}
)
entries.append(item)
total_nodes = len(entries)
portable_mode_required = total_nodes > 0
@@ -82,6 +88,7 @@ def analyze_workflow_portability(workflow: Dict[str, Any]) -> Dict[str, Any]:
"export_mode": contract["export_mode"],
"summary": {
"openclaw_nodes": total_nodes,
"suppressed_openclaw_nodes": len(suppressed_entries),
"portable_mode_required": portable_mode_required,
"portable_mode_supported": portable_mode_supported,
"requires_manual_rewire": portable_mode_required,
@@ -89,9 +96,20 @@ def analyze_workflow_portability(workflow: Dict[str, Any]) -> Dict[str, Any]:
"detected_class_types": sorted(detected_class_types),
"recommended_actions": _dedupe_preserve_order(recommended_actions),
"openclaw_nodes": entries,
"suppressed_openclaw_nodes": suppressed_entries,
}
def iter_workflow_diagnostic_nodes(
workflow: Dict[str, Any]
) -> Iterable[Dict[str, Any]]:
if not isinstance(workflow, dict):
return []
if isinstance(workflow.get("nodes"), list):
return list(_iter_frontend_workflow_nodes(workflow))
return list(_iter_api_workflow_nodes(workflow))
def _iter_sorted_workflow_nodes(
workflow: Dict[str, Any],
) -> Iterable[Tuple[str, Dict[str, Any]]]:
@@ -106,6 +124,131 @@ def _iter_sorted_workflow_nodes(
yield str(node_id), node_data
def _iter_api_workflow_nodes(workflow: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
for node_id, node_data in _iter_sorted_workflow_nodes(workflow):
inactive = _node_is_inactive(node_data)
yield {
"node_id": node_id,
"node_data": node_data,
"class_type": _node_class_type(node_data),
"inputs": node_data.get("inputs") if isinstance(node_data, dict) else None,
"active": not inactive,
"inactive_reason": "self_inactive" if inactive else None,
"is_subgraph_container": False,
"source": "api_prompt",
}
def _iter_frontend_workflow_nodes(workflow: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
root_nodes = (
workflow.get("nodes") if isinstance(workflow.get("nodes"), list) else []
)
subgraph_defs = _collect_subgraph_defs(
workflow.get("definitions", {}).get("subgraphs", [])
if isinstance(workflow.get("definitions"), dict)
else []
)
subgraph_def_map = {str(item["id"]): item for item in subgraph_defs}
def walk(
nodes: list[Any],
*,
parent_prefix: str = "",
parent_active: bool = True,
visiting: set[Tuple[str, str]] | None = None,
) -> Iterable[Dict[str, Any]]:
visiting = visiting or set()
for raw_node in nodes:
if not isinstance(raw_node, dict):
continue
raw_id = raw_node.get("id")
if raw_id is None:
continue
node_id = f"{parent_prefix}:{raw_id}" if parent_prefix else str(raw_id)
class_type = _node_class_type(raw_node)
self_inactive = _node_is_inactive(raw_node)
active = parent_active and not self_inactive
if active:
inactive_reason = None
elif not parent_active:
inactive_reason = "ancestor_inactive"
else:
inactive_reason = "self_inactive"
is_subgraph_container = (
isinstance(class_type, str) and class_type in subgraph_def_map
)
yield {
"node_id": node_id,
"node_data": raw_node,
"class_type": class_type,
"inputs": raw_node.get("inputs"),
"active": active,
"inactive_reason": inactive_reason,
"is_subgraph_container": is_subgraph_container,
"source": "frontend_workflow",
}
if not is_subgraph_container:
continue
visit_key = (class_type, node_id)
if visit_key in visiting:
continue
nested_def = subgraph_def_map.get(class_type)
nested_nodes = nested_def.get("nodes") if nested_def else None
if not isinstance(nested_nodes, list):
continue
next_visiting = set(visiting)
next_visiting.add(visit_key)
yield from walk(
nested_nodes,
parent_prefix=node_id,
parent_active=active,
visiting=next_visiting,
)
return list(walk(root_nodes))
def _collect_subgraph_defs(raw_defs: Any) -> list[Dict[str, Any]]:
result: list[Dict[str, Any]] = []
seen: set[str] = set()
def collect(defs: Any) -> None:
if not isinstance(defs, list):
return
for raw_def in defs:
if not isinstance(raw_def, dict) or not isinstance(raw_def.get("id"), str):
continue
def_id = raw_def["id"]
if def_id in seen:
continue
seen.add(def_id)
result.append(raw_def)
nested = raw_def.get("definitions")
if isinstance(nested, dict):
collect(nested.get("subgraphs"))
collect(raw_defs)
return result
def _node_class_type(node_data: Dict[str, Any]) -> str | None:
class_type = node_data.get("class_type")
if isinstance(class_type, str):
return class_type
node_type = node_data.get("type")
if isinstance(node_type, str):
return node_type
return None
def _node_is_inactive(node_data: Dict[str, Any]) -> bool:
mode = node_data.get("mode")
try:
return int(mode) in INACTIVE_LITEGRAPH_MODES
except Exception:
return False
def _dedupe_preserve_order(items: Iterable[str]) -> list[str]:
seen = set()
result = []
+49
View File
@@ -38,6 +38,55 @@ test.describe('OpenClaw Sidebar', () => {
await expect(page.locator('#pnginfo-empty-state')).toContainText('Load an image to inspect');
});
test('Explorer preflight surfaces inactive-branch suppressed diagnostics', async ({ page }) => {
await page.route('**/preflight', async (route) => {
const request = route.request();
const url = new URL(request.url());
if (request.method() !== 'POST' || !url.pathname.endsWith('/preflight')) {
await route.fallback();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
ok: true,
summary: {
missing_nodes: 0,
missing_models: 0,
invalid_inputs: 0,
suppressed_missing_nodes: 2,
suppressed_missing_models: 1,
},
missing_nodes: [],
missing_models: [],
suppressed_missing_nodes: [
{ node_id: '5:7', class_type: 'MoltbotPromptPlanner' },
{ node_id: '5:8', class_type: 'MissingCustomNode' },
],
suppressed_missing_models: [
{ node_id: '5:8', type: 'checkpoints', name: 'missing-model.safetensors' },
],
notes: [
'Inactive subgraph branches were suppressed from actionable diagnostics.',
],
}),
});
});
await clickTab(page, 'Explorer');
await page.locator('.openclaw-preflight-results').waitFor({ state: 'attached' });
await page.locator('textarea').fill(JSON.stringify({ nodes: [] }));
await page.getByRole('button', { name: 'Run Preflight' }).click();
const results = page.locator('.openclaw-preflight-results');
await expect(results).toContainText('Workflow Compatible');
await expect(results).toContainText('Inactive Branch Findings Suppressed (3)');
await expect(results).toContainText('MissingCustomNode');
await expect(results).toContainText('missing-model.safetensors');
});
test('harness recovers from one transient openclaw entry fetch failure', async ({ page }) => {
let failedOnce = false;
+153
View File
@@ -13,6 +13,33 @@ from services.workflow_portability import (
class TestF47WorkflowPortability(unittest.TestCase):
def _frontend_subgraph_workflow(self, *, container_mode=0):
return {
"nodes": [
{
"id": 5,
"type": "subgraph-def-a",
"mode": container_mode,
}
],
"definitions": {
"subgraphs": [
{
"id": "subgraph-def-a",
"name": "OpenClaw portability subgraph",
"nodes": [
{"id": 7, "type": "MoltbotPromptPlanner", "inputs": {}},
{
"id": 8,
"type": "MissingCustomNode",
"inputs": {"ckpt_name": "missing-model.safetensors"},
},
],
}
]
},
}
def test_contract_matches_current_node_schema(self):
contract = get_workflow_portability_contract()
nodes = contract["nodes"]
@@ -56,6 +83,132 @@ class TestF47WorkflowPortability(unittest.TestCase):
["MoltbotBatchVariants", "MoltbotPromptPlanner"],
)
def test_frontend_workflow_active_subgraph_reports_openclaw_nodes(self):
report = analyze_workflow_portability(
self._frontend_subgraph_workflow(container_mode=0)
)
self.assertEqual(report["summary"]["openclaw_nodes"], 1)
self.assertEqual(report["summary"]["suppressed_openclaw_nodes"], 0)
self.assertEqual(report["openclaw_nodes"][0]["node_id"], "5:7")
self.assertEqual(
report["detected_class_types"],
["MoltbotPromptPlanner"],
)
def test_frontend_workflow_muted_subgraph_suppresses_openclaw_nodes(self):
report = analyze_workflow_portability(
self._frontend_subgraph_workflow(container_mode=2)
)
self.assertEqual(report["summary"]["openclaw_nodes"], 0)
self.assertFalse(report["summary"]["portable_mode_required"])
self.assertEqual(report["summary"]["suppressed_openclaw_nodes"], 1)
self.assertEqual(report["suppressed_openclaw_nodes"][0]["node_id"], "5:7")
self.assertEqual(
report["suppressed_openclaw_nodes"][0]["inactive_reason"],
"ancestor_inactive",
)
def test_api_prompt_muted_root_node_suppresses_openclaw_nodes(self):
workflow = {
"1": {"class_type": "MoltbotPromptPlanner", "mode": 2, "inputs": {}},
"2": {"class_type": "KSampler", "inputs": {}},
}
report = analyze_workflow_portability(workflow)
self.assertEqual(report["summary"]["openclaw_nodes"], 0)
self.assertEqual(report["summary"]["suppressed_openclaw_nodes"], 1)
self.assertEqual(report["suppressed_openclaw_nodes"][0]["node_id"], "1")
self.assertEqual(
report["suppressed_openclaw_nodes"][0]["inactive_reason"],
"self_inactive",
)
def test_frontend_workflow_bypassed_subgraph_suppresses_preflight_findings(self):
services.preflight._CACHE.clear()
with (
patch.object(
services.preflight, "nodes", MagicMock(), create=True
) as mock_nodes,
patch.object(
services.preflight, "folder_paths", MagicMock(), create=True
) as mock_folder_paths,
):
mock_nodes.NODE_CLASS_MAPPINGS = {"KSampler": object}
mock_folder_paths.folder_names_and_paths = {}
mock_folder_paths.get_filename_list.return_value = []
report = services.preflight.run_preflight_check(
self._frontend_subgraph_workflow(container_mode=4)
)
self.assertTrue(report["ok"])
self.assertEqual(report["summary"]["missing_nodes"], 0)
self.assertEqual(report["summary"]["missing_models"], 0)
self.assertEqual(report["summary"]["suppressed_missing_nodes"], 2)
self.assertEqual(report["summary"]["suppressed_missing_models"], 1)
self.assertEqual(
[item["node_id"] for item in report["suppressed_missing_nodes"]],
["5:7", "5:8"],
)
self.assertIn(
"Inactive subgraph branches were suppressed from actionable diagnostics.",
report["notes"],
)
def test_api_prompt_bypassed_root_node_suppresses_preflight_findings(self):
services.preflight._CACHE.clear()
with (
patch.object(
services.preflight, "nodes", MagicMock(), create=True
) as mock_nodes,
patch.object(
services.preflight, "folder_paths", MagicMock(), create=True
) as mock_folder_paths,
):
mock_nodes.NODE_CLASS_MAPPINGS = {"KSampler": object}
mock_folder_paths.folder_names_and_paths = {}
mock_folder_paths.get_filename_list.return_value = []
report = services.preflight.run_preflight_check(
{
"1": {
"class_type": "MoltbotPromptPlanner",
"mode": 4,
"inputs": {"ckpt_name": "missing-model.safetensors"},
}
}
)
self.assertTrue(report["ok"])
self.assertEqual(report["summary"]["missing_nodes"], 0)
self.assertEqual(report["summary"]["missing_models"], 0)
self.assertEqual(report["summary"]["suppressed_missing_nodes"], 1)
self.assertEqual(report["summary"]["suppressed_missing_models"], 1)
self.assertEqual(report["suppressed_missing_nodes"][0]["node_id"], "1")
self.assertEqual(
report["suppressed_missing_nodes"][0]["inactive_reason"],
"self_inactive",
)
def test_api_prompt_without_subgraph_metadata_remains_supported(self):
workflow = {
"11": {"class_type": "MoltbotBatchVariants"},
"2": {"class_type": "MoltbotPromptPlanner"},
}
report = analyze_workflow_portability(workflow)
self.assertEqual(report["summary"]["openclaw_nodes"], 2)
self.assertEqual(report["summary"]["suppressed_openclaw_nodes"], 0)
self.assertEqual(
[entry["node_id"] for entry in report["openclaw_nodes"]], ["2", "11"]
)
def test_preflight_attaches_openclaw_missing_node_fallback(self):
services.preflight._CACHE.clear()
@@ -21,6 +21,11 @@ from services.compatibility_matrix_governance import (
from services.operator_doctor import DoctorReport, check_compatibility_matrix_governance
REPO_ROOT = Path(__file__).resolve().parents[1]
EXPECTED_CURRENT_ANCHORS = {
"comfyui": "df22bcd5 (v0.19.3-41-gdf22bcd5 / frontend package 1.42.15)",
"comfyui_frontend": "1.44.10",
"desktop": "0.8.35 (core 0.19.5 / frontend 1.42.14)",
}
class TestR90CompatMatrixGovernance(unittest.TestCase):
@@ -33,6 +38,12 @@ class TestR90CompatMatrixGovernance(unittest.TestCase):
self.assertTrue(validation["ok"], msg=validation)
self.assertIn(validation["status"], ("fresh", "warning", "stale"))
def test_repo_matrix_tracks_current_reference_anchors(self):
doc = read_matrix_document(
REPO_ROOT / "docs" / "release" / "compatibility_matrix.md"
)
self.assertEqual(doc["metadata"]["anchors"], EXPECTED_CURRENT_ANCHORS)
def test_detect_anchor_drift(self):
published = {
"comfyui": "a",
@@ -52,15 +63,15 @@ class TestR90CompatMatrixGovernance(unittest.TestCase):
def test_build_host_surface_contract_tracks_desktop_embedded_frontend_lag(self):
contract = build_host_surface_contract(
{
"comfyui": "0.19.3",
"comfyui_frontend": "1.44.4",
"desktop": "0.8.32 (core 0.19.3 / frontend 1.42.11)",
"comfyui": EXPECTED_CURRENT_ANCHORS["comfyui"],
"comfyui_frontend": EXPECTED_CURRENT_ANCHORS["comfyui_frontend"],
"desktop": EXPECTED_CURRENT_ANCHORS["desktop"],
}
)
self.assertTrue(contract["ok"], msg=contract)
self.assertEqual(contract["code"], "R164_HOST_SURFACES_READY")
self.assertEqual(
contract["surfaces"]["desktop"]["embedded_frontend_version"], "1.42.11"
contract["surfaces"]["desktop"]["embedded_frontend_version"], "1.42.14"
)
self.assertEqual(
contract["surfaces"]["desktop"]["frontend_parity"]["status"], "lagging"
@@ -183,7 +194,7 @@ class TestR90CompatMatrixGovernance(unittest.TestCase):
self.assertTrue(contract["ok"], msg=contract)
desktop_surface = contract["surfaces"]["desktop"]
self.assertEqual(desktop_surface["frontend_parity"]["status"], "lagging")
self.assertEqual(desktop_surface["embedded_frontend_version"], "1.42.11")
self.assertEqual(desktop_surface["embedded_frontend_version"], "1.42.14")
self.assertTrue(
(
REPO_ROOT / "tests" / "e2e" / "specs" / "desktop_host_parity.spec.js"
+38 -1
View File
@@ -108,7 +108,7 @@ export const ExplorerTab = {
const diagHeader = makeEl("h3", "", "Preflight Diagnostics");
diagHeader.style.marginTop = "0";
const diagDesc = makeEl("p", "", "Paste a workflow JSON (API format) to check for missing nodes/models compatible with this environment.");
const diagDesc = makeEl("p", "", "Paste workflow JSON or API prompt JSON to check for missing nodes/models compatible with this environment.");
diagDesc.style.fontSize = "0.9em";
diagDesc.style.opacity = "0.8";
@@ -387,6 +387,43 @@ export const ExplorerTab = {
section.appendChild(ul);
resultsArea.appendChild(section);
}
const suppressedNodeCount = Number(report.summary?.suppressed_missing_nodes || 0);
const suppressedModelCount = Number(report.summary?.suppressed_missing_models || 0);
const suppressedTotal = suppressedNodeCount + suppressedModelCount;
if (suppressedTotal > 0) {
const section = makeEl("div", "openclaw-preflight-suppressed");
const heading = makeEl("h4", "", `Inactive Branch Findings Suppressed (${suppressedTotal})`);
section.appendChild(heading);
const note = makeEl(
"div",
"",
"Muted or bypassed workflow branches are not counted as actionable missing dependencies."
);
note.style.opacity = "0.75";
note.style.fontSize = "0.9em";
section.appendChild(note);
const ul = makeEl("ul");
(report.suppressed_missing_nodes || []).forEach(m => {
const li = makeEl(
"li",
"",
`${m.node_id || "unknown"}: ${m.class_type || "unknown node"}`
);
ul.appendChild(li);
});
(report.suppressed_missing_models || []).forEach(m => {
const li = makeEl(
"li",
"",
`${m.node_id || "unknown"}: ${m.type || "model"} ${m.name || "unknown model"}`
);
ul.appendChild(li);
});
section.appendChild(ul);
resultsArea.appendChild(section);
}
}
// Initial Load