From 7fb58195fb4ceb8a2cc35863014b420c309bea4d Mon Sep 17 00:00:00 2001 From: Dom Date: Mon, 29 Jun 2026 11:05:10 +0200 Subject: [PATCH] fix(workflow): conserve machine_id au round-trip to_dict/from_dict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les workflows rechargés du disque retombaient sur machine_id='default' : to_dict ne sérialisait pas l'attribut d'instance _machine_id et from_dict ne le reposait pas (il dormait dans metadata['machine_id']). to_dict le sérialise si présent (pas de 'default' parasite) ; from_dict le restaure depuis le champ explicite ou metadata (rétrocompat des workflows déjà sur disque). Test de non-régression round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) --- core/models/workflow_graph.py | 14 ++++++- tests/unit/test_workflow_graph_machine_id.py | 44 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_workflow_graph_machine_id.py diff --git a/core/models/workflow_graph.py b/core/models/workflow_graph.py index 7af4520ac..c35569b91 100644 --- a/core/models/workflow_graph.py +++ b/core/models/workflow_graph.py @@ -1250,12 +1250,16 @@ class Workflow: } if self.chain_config: result["chain_config"] = self.chain_config.to_dict() if hasattr(self.chain_config, 'to_dict') else self.chain_config + # machine_id : attribut d'instance posé au runtime (pas un champ dataclass) + machine_id = getattr(self, "_machine_id", None) + if machine_id: + result["machine_id"] = machine_id return result @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Workflow': """Désérialiser depuis JSON""" - return cls( + wf = cls( workflow_id=data["workflow_id"], name=data.get("name", data["workflow_id"]), description=data.get("description", ""), @@ -1277,7 +1281,13 @@ class Workflow: references=data.get("references", []), chain_config=data.get("chain_config") ) - + # Reposer machine_id (attribut d'instance) : priorité au champ explicite, + # sinon depuis metadata['machine_id'] (rétrocompat des workflows déjà sur disque) + machine_id = data.get("machine_id") or (wf.metadata or {}).get("machine_id") + if machine_id: + wf._machine_id = machine_id + return wf + def to_json(self) -> str: """Sérialiser en JSON string""" return json.dumps(self.to_dict(), indent=2) diff --git a/tests/unit/test_workflow_graph_machine_id.py b/tests/unit/test_workflow_graph_machine_id.py new file mode 100644 index 000000000..6fb464a2c --- /dev/null +++ b/tests/unit/test_workflow_graph_machine_id.py @@ -0,0 +1,44 @@ +""" +Test de non-régression : conservation du machine_id au round-trip to_dict/from_dict. + +Bug : les workflows listés via /api/v1/traces/stream/workflows étaient tous +attribués à machine_id="default" alors que les sessions portaient le bon +machine_id (lea-*). Cause : to_dict ne sérialisait pas l'attribut d'instance +`_machine_id` et from_dict ne le reposait pas (il dormait dans +metadata['machine_id']). list_workflows tombait alors sur le fallback "default". +""" + +from datetime import datetime + +from core.models.workflow_graph import Workflow + + +def _make_minimal_workflow(machine_id: str) -> Workflow: + """Construit un workflow minimal portant un machine_id dans ses métadonnées.""" + now = datetime.now().isoformat() + return Workflow.from_dict({ + "workflow_id": "wf-test", + "name": "wf-test", + "nodes": [], + "edges": [], + "safety_rules": {}, + "stats": {}, + "learning": {}, + "entry_nodes": [], + "end_nodes": [], + "created_at": now, + "updated_at": now, + "metadata": {"machine_id": machine_id}, + }) + + +def test_machine_id_preserved_after_to_dict_from_dict_round_trip(): + """Un workflow doit conserver son machine_id après un round-trip de (dé)sérialisation.""" + wf = _make_minimal_workflow("lea-poste-3") + # Simule l'étiquetage runtime fait par le stream_processor + wf._machine_id = "lea-poste-3" + + restored = Workflow.from_dict(wf.to_dict()) + + # Invariant : le machine_id survit au round-trip (comme le fait list_workflows) + assert getattr(restored, "_machine_id", "default") == "lea-poste-3"