Make signature artifact timestamps deterministic

This commit is contained in:
2026-07-02 20:10:09 +02:00
parent 25b3f69d0d
commit ecd8f7c082
2 changed files with 130 additions and 93 deletions
+117 -91
View File
@@ -2,7 +2,9 @@ import json
import tempfile import tempfile
import textwrap import textwrap
import unittest import unittest
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from unittest import mock
from tools.generate_popular_node_signatures import ( from tools.generate_popular_node_signatures import (
extract_repo_signatures, extract_repo_signatures,
@@ -12,10 +14,6 @@ from tools.generate_popular_node_signatures import (
class StaticExtractionTests(unittest.TestCase): class StaticExtractionTests(unittest.TestCase):
def _normalise_generated_at(self, text):
parsed = json.loads(text)
return text.replace(parsed["generated_at"], "<generated-at>")
def _extract_source(self, source, pack_id="sample-pack"): def _extract_source(self, source, pack_id="sample-pack"):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
Path(tmp, "__init__.py").write_text(textwrap.dedent(source), encoding="utf-8") Path(tmp, "__init__.py").write_text(textwrap.dedent(source), encoding="utf-8")
@@ -4766,108 +4764,136 @@ NODE_CLASS_MAPPINGS = {
self.assertEqual("no_static_nodes", result["pack"]["status"]) self.assertEqual("no_static_nodes", result["pack"]["status"])
def test_write_artifact_is_deterministic(self): def test_write_artifact_is_deterministic(self):
class FakeDateTime:
values = iter(
(
datetime(2026, 1, 1, tzinfo=timezone.utc),
datetime(2026, 1, 2, tzinfo=timezone.utc),
)
)
@classmethod
def now(cls, tz=None):
value = next(cls.values)
return value if tz is None else value.astimezone(tz)
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
out_one = Path(tmp, "one.json") out_one = Path(tmp, "one.json")
out_two = Path(tmp, "two.json") out_two = Path(tmp, "two.json")
write_artifact( with mock.patch("tools.generate_popular_node_signatures.datetime", FakeDateTime):
out_one, write_artifact(
sources={ out_one,
"manager_url": "https://example.invalid/manager.json", sources={
"limit": 1, "manager_url": "https://example.invalid/manager.json",
"registry": {"z": "last", "a": "first"}, "limit": 1,
}, "registry": {"z": "last", "a": "first"},
packs={
"b-pack": {
"id": "b-pack",
"title": "B Pack",
"status": "ok",
"metadata": {"z": 2, "a": 1},
}, },
"a-pack": { packs={
"id": "a-pack", "b-pack": {
"title": "A Pack", "id": "b-pack",
"status": "ok", "title": "B Pack",
"metadata": {"z": 4, "a": 3}, "status": "ok",
"metadata": {"z": 2, "a": 1},
},
"a-pack": {
"id": "a-pack",
"title": "A Pack",
"status": "ok",
"metadata": {"z": 4, "a": 3},
},
}, },
}, nodes={
nodes={ "BNode": {
"BNode": { "type": "BNode",
"type": "BNode", "display": "B Node",
"display": "B Node", "pack": "b-pack",
"pack": "b-pack", "repository": "https://github.com/example/b-pack",
"repository": "https://github.com/example/b-pack", "inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"}, "required": [],
"required": [], "outputs": ["IMAGE"],
"outputs": ["IMAGE"], "output_names": ["image"],
"output_names": ["image"], "confidence": "static_exact",
"confidence": "static_exact", },
"ANode": {
"type": "ANode",
"display": "A Node",
"pack": "a-pack",
"repository": "https://github.com/example/a-pack",
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
"required": [],
"outputs": ["IMAGE"],
"output_names": ["image"],
"confidence": "static_exact",
},
}, },
"ANode": { )
"type": "ANode", write_artifact(
"display": "A Node", out_two,
"pack": "a-pack", sources={
"repository": "https://github.com/example/a-pack", "registry": {"a": "first", "z": "last"},
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"}, "limit": 1,
"required": [], "manager_url": "https://example.invalid/manager.json",
"outputs": ["IMAGE"],
"output_names": ["image"],
"confidence": "static_exact",
}, },
}, packs={
) "a-pack": {
write_artifact( "metadata": {"a": 3, "z": 4},
out_two, "status": "ok",
sources={ "title": "A Pack",
"registry": {"a": "first", "z": "last"}, "id": "a-pack",
"limit": 1, },
"manager_url": "https://example.invalid/manager.json", "b-pack": {
}, "metadata": {"a": 1, "z": 2},
packs={ "status": "ok",
"a-pack": { "title": "B Pack",
"metadata": {"a": 3, "z": 4}, "id": "b-pack",
"status": "ok", },
"title": "A Pack",
"id": "a-pack",
}, },
"b-pack": { nodes={
"metadata": {"a": 1, "z": 2}, "ANode": {
"status": "ok", "confidence": "static_exact",
"title": "B Pack", "output_names": ["image"],
"id": "b-pack", "outputs": ["IMAGE"],
"required": [],
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
"repository": "https://github.com/example/a-pack",
"pack": "a-pack",
"display": "A Node",
"type": "ANode",
},
"BNode": {
"confidence": "static_exact",
"output_names": ["image"],
"outputs": ["IMAGE"],
"required": [],
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
"repository": "https://github.com/example/b-pack",
"pack": "b-pack",
"display": "B Node",
"type": "BNode",
},
}, },
}, )
nodes={
"ANode": {
"confidence": "static_exact",
"output_names": ["image"],
"outputs": ["IMAGE"],
"required": [],
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
"repository": "https://github.com/example/a-pack",
"pack": "a-pack",
"display": "A Node",
"type": "ANode",
},
"BNode": {
"confidence": "static_exact",
"output_names": ["image"],
"outputs": ["IMAGE"],
"required": [],
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
"repository": "https://github.com/example/b-pack",
"pack": "b-pack",
"display": "B Node",
"type": "BNode",
},
},
)
text_one = out_one.read_text(encoding="utf-8") text_one = out_one.read_text(encoding="utf-8")
text_two = out_two.read_text(encoding="utf-8") text_two = out_two.read_text(encoding="utf-8")
parsed = json.loads(text_one) parsed = json.loads(text_one)
self.assertEqual(["a-pack", "b-pack"], list(parsed["packs"])) self.assertEqual(["a-pack", "b-pack"], list(parsed["packs"]))
self.assertEqual(["ANode", "BNode"], list(parsed["nodes"])) self.assertEqual(["ANode", "BNode"], list(parsed["nodes"]))
self.assertEqual(self._normalise_generated_at(text_one), self._normalise_generated_at(text_two)) self.assertEqual(text_one, text_two)
def test_write_artifact_uses_explicit_generated_at(self):
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp, "popular_node_signatures.json")
write_artifact(
out,
sources={"manager_url": "https://example.invalid/manager.json", "limit": 1},
packs={},
nodes={},
generated_at="2026-07-02T00:00:00Z",
)
parsed = json.loads(out.read_text(encoding="utf-8"))
self.assertEqual("2026-07-02T00:00:00Z", parsed["generated_at"])
if __name__ == "__main__": if __name__ == "__main__":
+13 -2
View File
@@ -10,6 +10,7 @@ from pathlib import Path
SCHEMA_VERSION = 1 SCHEMA_VERSION = 1
MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json" MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json"
REGISTRY_NODES_URL = "https://api.comfy.org/nodes" REGISTRY_NODES_URL = "https://api.comfy.org/nodes"
DEFAULT_GENERATED_AT = "1970-01-01T00:00:00Z"
class UnsupportedStaticExpression(Exception): class UnsupportedStaticExpression(Exception):
@@ -2410,10 +2411,20 @@ def _sorted_json_value(value):
return value return value
def write_artifact(path, sources, packs, nodes): def _format_generated_at(generated_at):
if isinstance(generated_at, datetime):
if generated_at.tzinfo is None:
generated_at = generated_at.replace(tzinfo=timezone.utc)
else:
generated_at = generated_at.astimezone(timezone.utc)
return generated_at.replace(microsecond=0).isoformat().replace("+00:00", "Z")
return str(generated_at)
def write_artifact(path, sources, packs, nodes, *, generated_at=DEFAULT_GENERATED_AT):
payload = { payload = {
"schema_version": SCHEMA_VERSION, "schema_version": SCHEMA_VERSION,
"generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), "generated_at": _format_generated_at(generated_at),
"sources": _sorted_json_value(sources), "sources": _sorted_json_value(sources),
"packs": _sorted_json_value(packs), "packs": _sorted_json_value(packs),
"nodes": _sorted_json_value(nodes), "nodes": _sorted_json_value(nodes),