import json import importlib.util import subprocess import sys import textwrap from types import SimpleNamespace import pytest from smart_metadata_reader.models import LoraInfo, MetadataBundle, ParseResult def test_format_setting_outputs_complete_human_readable_text(): from smart_metadata_reader.settings_formatter import format_setting result = ParseResult( filename="001.png", model_name="model.safetensors", vae_name="vae.vae.safetensors", loras=[ LoraInfo( node_id="10", class_type="LoraLoader", lora_name="lora_a.safetensors", strength_model=0.8, strength_clip=1.0, path=["KSampler 9.model", "LoraLoader 10"], ), LoraInfo( node_id="11", class_type="LoraLoader", lora_name="lora_b.safetensors", strength_model=0.6, strength_clip=0.75, path=["KSampler 9.model", "LoraLoader 11"], ), ], seed=123456789, steps=30, cfg=7.0, sampler_name="euler", scheduler="normal", width=1200, height=1600, status_message="OK", confidence=0.94, partial_result={"source_format": "ComfyUI prompt/workflow"}, ) setting = format_setting(result) assert "Filename: 001.png" in setting assert "Source: ComfyUI prompt/workflow" in setting assert "Model: model.safetensors" in setting assert "VAE: vae.vae.safetensors" in setting assert "LoRA:" in setting assert "* lora_a.safetensors (model 0.80, clip 1.00)" in setting assert "* lora_b.safetensors (model 0.60, clip 0.75)" in setting assert "Seed: 123456789" in setting assert "Steps: 30" in setting assert "CFG: 7.0" in setting assert "Sampler: euler" in setting assert "Scheduler: normal" in setting assert "Size: 1200 x 1600" in setting assert "Status: OK" in setting assert "Confidence: 0.94" in setting def test_format_setting_handles_missing_fields_without_error(): from smart_metadata_reader.settings_formatter import format_setting setting = format_setting(ParseResult()) assert "Filename: unknown" in setting assert "Source: unknown" in setting assert "Model: unknown" in setting assert "VAE: unknown" in setting assert "LoRA: none" in setting assert "Seed: -1" in setting assert "Steps: 0" in setting assert "CFG: 0.0" in setting assert "Sampler: unknown" in setting assert "Scheduler: unknown" in setting assert "Size: unknown" in setting def test_format_setting_displays_unresolved_partial_result_entries(): from smart_metadata_reader.settings_formatter import format_setting result = ParseResult( filename="partial.png", status_message="PARTIAL", confidence=0.52, partial_result={ "source_format": "ComfyUI prompt/workflow", "unresolved": [ { "node_id": "88", "class_type": "UnknownConditioningRouter", "field": "positive_conditioning", "role": "positive", "reason": "no conditioning adapter matched", } ], }, ) setting = format_setting(result) assert "Status: PARTIAL" in setting assert "Confidence: 0.52" in setting assert "Unresolved:" in setting assert ( "* Node 88 UnknownConditioningRouter input positive_conditioning " "role=positive reason=no conditioning adapter matched" ) in setting def test_format_setting_displays_a1111_source_and_status(): from smart_metadata_reader.settings_formatter import format_setting result = ParseResult( filename="", positive=", nsfw", negative="low quality", seed=123, steps=25, cfg=7.5, sampler_name="Euler a", width=768, height=1024, model_name="demo_model", status_message="A1111 parameters fallback", confidence=0.75, partial_result={"source_format": "A1111 parameters"}, ) setting = format_setting(result) assert "Source: A1111 parameters" in setting assert "Status: A1111 parameters fallback" in setting assert "Model: demo_model" in setting def test_format_setting_is_plain_text_not_json(): from smart_metadata_reader.settings_formatter import format_setting setting = format_setting( ParseResult( filename="plain.png", partial_result={"source_format": "ComfyUI prompt/workflow"}, ) ) assert not setting.strip().startswith("{") with pytest.raises(json.JSONDecodeError): json.loads(setting) def test_node_mappings_expose_smart_metadata_reader(): import nodes assert nodes.NODE_CLASS_MAPPINGS["SmartMetadataReader"] is nodes.SmartMetadataReader assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader" def test_plugin_root_init_exports_comfyui_mappings(): root_dir = pytest.importorskip("pathlib").Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location( "comfyui_smart_metadata_reader_plugin", root_dir / "__init__.py", submodule_search_locations=[str(root_dir)], ) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module try: spec.loader.exec_module(module) finally: sys.modules.pop(spec.name, None) sys.modules.pop(f"{spec.name}.nodes", None) assert "SmartMetadataReader" in module.NODE_CLASS_MAPPINGS assert module.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader" def test_plugin_package_import_works_without_repo_root_on_sys_path(tmp_path): root_dir = pytest.importorskip("pathlib").Path(__file__).resolve().parents[1] script = textwrap.dedent( f""" import importlib.util import pathlib import sys root = pathlib.Path({str(root_dir)!r}) sys.path = [ path for path in sys.path if pathlib.Path(path or ".").resolve() != root ] for name in list(sys.modules): if name == "smart_metadata_reader" or name.startswith("smart_metadata_reader."): sys.modules.pop(name, None) spec = importlib.util.spec_from_file_location( "comfyui_custom_node_under_test", root / "__init__.py", submodule_search_locations=[str(root)], ) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) assert "SmartMetadataReader" in module.NODE_CLASS_MAPPINGS assert module.NODE_DISPLAY_NAME_MAPPINGS["SmartMetadataReader"] == "Smart Metadata Reader" print("package import ok") """ ) completed = subprocess.run( [sys.executable, "-c", script], cwd=tmp_path, text=True, capture_output=True, check=False, ) assert completed.returncode == 0, completed.stderr assert "package import ok" in completed.stdout def test_input_types_ignores_unusable_comfyui_filename_list_and_scans_input_dir( tmp_path, monkeypatch, ): import nodes (tmp_path / "root.png").write_bytes(b"fake") subdir = tmp_path / "sub" subdir.mkdir() (subdir / "a.jpg").write_bytes(b"fake") (subdir / "b.webp").write_bytes(b"fake") (subdir / "note.txt").write_text("ignore me") def broken_filename_list(category): raise KeyError(category) monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace( get_filename_list=broken_filename_list, get_input_directory=lambda: str(tmp_path), ), ) input_types = nodes.SmartMetadataReader.INPUT_TYPES() assert input_types["required"]["image"][0] == [ "root.png", "sub/a.jpg", "sub/b.webp", ] def test_smart_metadata_reader_input_types_include_uploadable_image(tmp_path, monkeypatch): import nodes (tmp_path / "a.png").write_bytes(b"fake") (tmp_path / "c.jpg").write_bytes(b"fake") (tmp_path / "d.jpeg").write_bytes(b"fake") (tmp_path / "b.webp").write_bytes(b"fake") (tmp_path / "ignored.bmp").write_bytes(b"fake") (tmp_path / "note.txt").write_text("ignore me") monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_input_directory=lambda: str(tmp_path)), ) input_types = nodes.SmartMetadataReader.INPUT_TYPES() required = input_types["required"] assert required["image"][0] == ["a.png", "b.webp", "c.jpg", "d.jpeg"] assert required["image"][1]["image_upload"] is True assert required["parameter_index"][0] == "INT" assert required["parameter_index"][1]["default"] == 0 assert required["parameter_index"][1]["min"] == 0 assert required["prefer_cached_text"] == ("BOOLEAN", {"default": True}) assert required["include_raw_json"] == ("BOOLEAN", {"default": True}) assert required["max_depth"][0] == "INT" assert required["max_depth"][1]["default"] == 40 assert required["max_depth"][1]["min"] == 1 assert required["max_depth"][1]["max"] == 200 def test_smart_metadata_reader_input_types_empty_or_missing_input_dir_is_safe( tmp_path, monkeypatch, ): import nodes monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_input_directory=lambda: str(tmp_path)), ) assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == [] monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_input_directory=lambda: str(tmp_path / "missing")), ) assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == [] def test_smart_metadata_reader_input_types_handles_input_directory_errors(monkeypatch): import nodes monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_input_directory=lambda: (_ for _ in ()).throw(KeyError("input"))), ) assert nodes.SmartMetadataReader.INPUT_TYPES()["required"]["image"][0] == [] def test_smart_metadata_reader_return_contract(): import nodes node_cls = nodes.SmartMetadataReader assert node_cls.RETURN_TYPES == ( "IMAGE", "MASK", "STRING", "STRING", "INT", "INT", "FLOAT", "INT", "INT", "STRING", "STRING", "STRING", ) assert node_cls.RETURN_NAMES == ( "image", "mask", "positive", "negative", "seed", "steps", "cfg", "width", "height", "model_name", "filename", "setting", ) assert len(node_cls.RETURN_TYPES) == len(node_cls.RETURN_NAMES) assert hasattr(node_cls, node_cls.FUNCTION) assert node_cls.CATEGORY def test_smart_metadata_reader_text_output_indices_match_parser_mapping(): import nodes node_cls = nodes.SmartMetadataReader assert node_cls.RETURN_NAMES[0] == "image" assert node_cls.RETURN_NAMES[1] == "mask" assert node_cls.RETURN_NAMES[2] == "positive" assert node_cls.RETURN_NAMES[3] == "negative" assert node_cls.RETURN_TYPES[2] == "STRING" assert node_cls.RETURN_TYPES[3] == "STRING" def test_smart_metadata_reader_method_returns_expected_tuple_order(tmp_path, monkeypatch): import nodes image_path = tmp_path / "sample.png" image_path.write_bytes(b"fake image bytes") calls = {} monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)), ) def fake_read_metadata(path): calls["read_metadata"] = path return MetadataBundle( filename="sample.png", width=1200, height=1600, prompt_raw="{}", workflow_raw=None, parameters_raw=None, source_format="ComfyUI prompt/workflow", ) def fake_parse_metadata_bundle( bundle, parameter_index, prefer_cached_text, include_raw_json, max_depth, ): calls["parse"] = { "bundle": bundle, "parameter_index": parameter_index, "prefer_cached_text": prefer_cached_text, "include_raw_json": include_raw_json, "max_depth": max_depth, } return ParseResult( positive="real positive", negative="real negative", seed=123, steps=30, cfg=7.5, width=1200, height=1600, model_name="model.safetensors", filename="sample.png", setting="Filename: sample.png\nStatus: OK", ) monkeypatch.setattr(nodes, "read_metadata", fake_read_metadata) monkeypatch.setattr(nodes, "parse_metadata_bundle", fake_parse_metadata_bundle) monkeypatch.setattr(nodes, "load_image_and_mask", lambda path: ("IMAGE_TENSOR", "MASK_TENSOR", 1200, 1600)) output = nodes.SmartMetadataReader().read_metadata( "sample.png", parameter_index=2, prefer_cached_text=False, include_raw_json=False, max_depth=12, ) assert output == ( "IMAGE_TENSOR", "MASK_TENSOR", "real positive", "real negative", 123, 30, 7.5, 1200, 1600, "model.safetensors", "sample.png", "Filename: sample.png\nStatus: OK", ) assert calls["read_metadata"] == str(image_path) assert calls["parse"]["parameter_index"] == 2 assert calls["parse"]["prefer_cached_text"] is False assert calls["parse"]["include_raw_json"] is False assert calls["parse"]["max_depth"] == 12 def test_smart_metadata_reader_parse_failure_returns_failed_setting(tmp_path, monkeypatch): import nodes image_path = tmp_path / "broken-metadata.png" image_path.write_bytes(b"fake image bytes") monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)), ) monkeypatch.setattr( nodes, "read_metadata", lambda path: MetadataBundle( filename="broken-metadata.png", width=640, height=480, prompt_raw=None, workflow_raw=None, parameters_raw=None, source_format="none", ), ) monkeypatch.setattr(nodes, "parse_metadata_bundle", lambda *args, **kwargs: (_ for _ in ()).throw(ValueError("boom"))) monkeypatch.setattr(nodes, "load_image_and_mask", lambda path: ("IMAGE_TENSOR", "MASK_TENSOR", 640, 480)) output = nodes.SmartMetadataReader().read_metadata("broken-metadata.png", 0, True, True, 40) assert output[:2] == ("IMAGE_TENSOR", "MASK_TENSOR") assert output[2] == "" assert output[3] == "" assert output[4] == -1 assert output[5] == 0 assert output[6] == 0.0 assert output[7] == 640 assert output[8] == 480 assert output[9] == "" assert output[10] == "broken-metadata.png" assert "Status: FAILED" in output[11] assert "boom" in output[11] def test_smart_metadata_reader_image_load_failure_raises_clear_error(monkeypatch): import nodes monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: "missing.png"), ) monkeypatch.setattr( nodes, "load_image_and_mask", lambda path: (_ for _ in ()).throw(OSError("cannot identify image file")), ) with pytest.raises(RuntimeError, match="Failed to load image 'missing.png'"): nodes.SmartMetadataReader().read_metadata("missing.png", 0, True, True, 40) def test_smart_metadata_reader_is_changed_tracks_file_content(tmp_path, monkeypatch): import nodes image_path = tmp_path / "changed.png" image_path.write_bytes(b"first") monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: str(image_path)), ) first = nodes.SmartMetadataReader.IS_CHANGED("changed.png") image_path.write_bytes(b"second") second = nodes.SmartMetadataReader.IS_CHANGED("changed.png") assert first assert second assert first != second def test_smart_metadata_reader_is_changed_handles_missing_or_bad_paths(monkeypatch): import nodes monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: (_ for _ in ()).throw(KeyError("input"))), ) value = nodes.SmartMetadataReader.IS_CHANGED("missing.png") assert isinstance(value, str) assert value.startswith("missing:") def test_smart_metadata_reader_validate_inputs_checks_image_exists(tmp_path, monkeypatch): import nodes existing = tmp_path / "exists.png" existing.write_bytes(b"fake") missing = tmp_path / "missing.png" monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(get_annotated_filepath=lambda image: str(tmp_path / image)), ) assert nodes.SmartMetadataReader.VALIDATE_INPUTS("exists.png") is True invalid = nodes.SmartMetadataReader.VALIDATE_INPUTS("missing.png") assert isinstance(invalid, str) assert "Invalid image file" in invalid assert str(missing) in invalid def test_smart_metadata_reader_validate_inputs_falls_back_when_exists_helper_errors( tmp_path, monkeypatch, ): import nodes existing = tmp_path / "exists.png" existing.write_bytes(b"fake") monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace( exists_annotated_filepath=lambda image: (_ for _ in ()).throw(KeyError("input")), get_input_directory=lambda: str(tmp_path), ), ) assert nodes.SmartMetadataReader.VALIDATE_INPUTS("exists.png") is True assert "Invalid image file" in nodes.SmartMetadataReader.VALIDATE_INPUTS("missing.png") def test_smart_metadata_reader_validate_inputs_uses_comfyui_exists_helper(monkeypatch): import nodes monkeypatch.setattr( nodes, "folder_paths", SimpleNamespace(exists_annotated_filepath=lambda image: image == "ok.png"), ) assert nodes.SmartMetadataReader.VALIDATE_INPUTS("ok.png") is True assert "Invalid image file" in nodes.SmartMetadataReader.VALIDATE_INPUTS("bad.png")