diff --git a/backend/tests/domains/test_workflow_smoke_harness.py b/backend/tests/domains/test_workflow_smoke_harness.py index 5810293..5041769 100644 --- a/backend/tests/domains/test_workflow_smoke_harness.py +++ b/backend/tests/domains/test_workflow_smoke_harness.py @@ -209,6 +209,93 @@ def test_choose_template_backed_output_type_prefers_requested_name(): assert [template["id"] for template in matches] == ["template-1"] +def test_workflow_golden_template_name_maps_cases_to_expected_templates(): + module = _load_render_pipeline_script() + + assert module.workflow_golden_template_name({"key": "still_graph"}) == "BlenderStudio" + assert module.workflow_golden_template_name({"key": "still_shadow"}) == "BlenderStudio" + assert module.workflow_golden_template_name({"key": "turntable_graph"}) == "Blender_Studio_Schadowcatcher_Anim" + assert module.workflow_golden_template_name({"key": "unknown"}) is None + + +def test_workflow_smoke_template_name_maps_supported_modes_to_blenderstudio(): + module = _load_render_pipeline_script() + + assert module.workflow_smoke_template_name("legacy") == "BlenderStudio" + assert module.workflow_smoke_template_name("graph") == "BlenderStudio" + assert module.workflow_smoke_template_name("shadow") == "BlenderStudio" + assert module.workflow_smoke_template_name("turntable") is None + + +def test_ensure_output_type_render_template_binding_moves_output_type_to_preferred_template(): + module = _load_render_pipeline_script() + + templates_state = [ + { + "id": "template-preferred", + "name": "BlenderStudio", + "is_active": True, + "output_type_ids": ["ot-existing"], + "output_type_id": "ot-existing", + }, + { + "id": "template-other", + "name": "Other Template", + "is_active": True, + "output_type_ids": ["ot-golden", "ot-other"], + "output_type_id": "ot-golden", + }, + ] + patch_calls: list[tuple[str, dict]] = [] + + class _Response: + def __init__(self, payload: dict): + self.status_code = 200 + self._payload = payload + self.text = "" + + def json(self): + return self._payload + + class _Client: + def patch(self, path: str, **kwargs): + payload = kwargs["json"] + patch_calls.append((path, payload)) + template_id = path.rsplit("/", 1)[-1] + template = next(item for item in templates_state if item["id"] == template_id) + template["output_type_ids"] = list(payload["output_type_ids"]) + template["output_type_id"] = payload["output_type_ids"][0] if payload["output_type_ids"] else None + return _Response(template) + + client = _Client() + original_get_render_templates = module.get_render_templates + try: + module.get_render_templates = lambda _client: [ + { + "id": item["id"], + "name": item["name"], + "is_active": item["is_active"], + "output_type_ids": list(item["output_type_ids"]), + "output_type_id": item["output_type_id"], + } + for item in templates_state + ] + + matches = module.ensure_output_type_render_template_binding( + client, + output_type={"id": "ot-golden", "name": "[Workflow Golden] Canonical Still Graph"}, + preferred_template_name="BlenderStudio", + ) + finally: + module.get_render_templates = original_get_render_templates + + assert patch_calls == [ + ("/render-templates/template-preferred", {"output_type_ids": ["ot-existing", "ot-golden"]}), + ("/render-templates/template-other", {"output_type_ids": ["ot-other"]}), + ] + assert [template["name"] for template in matches] == ["BlenderStudio"] + + def test_build_output_type_workflow_snapshot_keeps_restore_contract(): module = _load_render_pipeline_script() diff --git a/docs/workflows/NEXT_20_BLOCK_BATCH_PLAN.md b/docs/workflows/NEXT_20_BLOCK_BATCH_PLAN.md new file mode 100644 index 0000000..1ed2d4a --- /dev/null +++ b/docs/workflows/NEXT_20_BLOCK_BATCH_PLAN.md @@ -0,0 +1,299 @@ +# Next 20 Workflow Blocks + +Stand: April 12, 2026 + +Ziel dieses Backlogs ist nicht nur weitere UI-Politur, sondern die systematische Schließung der noch offenen Paritäts-, Contract- und Authoring-Lücken zwischen Legacy- und Graph-Workflow. + +## Batch A: Contract Truth And Authoring Clarity + +### Block 1: 20-Block-Backlog im Repo verankern + +Status: abgeschlossen + +- sichtbare Arbeitsliste im Repo statt nur Chat-Kontext +- Batch-Reihenfolge und Quality Gates festhalten + +### Block 2: Registry-/Contract-Audit gegen Runtime und Editor + +Status: abgeschlossen + +- Backend-Registry, Schema und Frontend-Contracts gegeneinander prüfen +- alle Nodes nach Kontext-, Socket-, Inspector- und Template-Input-Verhalten katalogisieren + +### Block 3: Root-Context-Inputs von echten Upstream-Sockets trennen + +Status: abgeschlossen + +- `cad_file_record` und `order_line_record` nicht länger als normale verkabelbare Eingänge behandeln +- Entry-Nodes im Canvas klar als kontextgesät modellieren + +### Block 4: Authoring-Muster pro Node explizit machen + +Status: abgeschlossen + +- Nodes klar als `Context Entry`, `Connection-Driven`, `Inspector-Driven` oder `Hybrid` kennzeichnen +- dieselbe Semantik in Canvas, Inspector und Katalog verwenden + +### Block 5: Problem-Nodes mit impliziten Anforderungen katalogisieren + +Status: abgeschlossen + +- welche Nodes haben derzeit 0 Felder, aber nicht offensichtliche Laufzeitannahmen +- welche Nodes brauchen nur Dokumentation und welche echte neue Eingabevariablen + +## Batch B: Input And Inspector Closure + +### Block 6: Inspector-Felder gegen Runtime-Parameter mappen + +Status: abgeschlossen + +- `workflow_graph_runtime.py` und Registry vollständig abgleichen +- fehlende oder irreführende Inspector-Felder schließen + +Ergebnis: + +- `notify <- export_blend` Contract-Drift zwischen Frontend und Backend beseitigt +- Bridge-Node-Runtime-Parameter als explizite Contract-Sets in `workflow_graph_runtime.py` zentralisiert +- Registry-Tests prüfen jetzt auch `resolve_template`, `material_map_resolve`, `auto_populate_materials`, `glb_bbox`, `output_save` und `notify` gegen die Runtime + +### Block 7: Template-Inputs und Template-Overrides konsolidieren + +Status: abgeschlossen + +- Template-Inputs als echte Produktionsvariablen sichtbar machen +- Node-Editor darf Template-spezifische Inputs nicht verstecken oder fragmentieren + +Abnahme: + +- `resolve_template` zeigt statische Overrides und dynamische Template-Inputs ohne Mehrdeutigkeit +- Template-Inputs werden konsistent in Inspector, Contract-Card und Preflight sichtbar +- kein Drift zwischen `workflow_input_schema`, `template_inputs` und authorbaren Node-Parametern + +Ergebnis: + +- `resolve_template` zeigt jetzt statische Overrides und template-definierte Workflow-Inputs im selben Authoring-Modell statt in getrennten UI-Logiken +- aktive Templates mit `workflow_input_schema` werden auch ohne festen Override als potenzielle Produktionsvariablen sichtbar gemacht +- gezielte Frontend-Tests sichern ab, dass automatische Template-Variablenabdeckung und konkrete Override-Inputs konsistent im Inspector erscheinen + +### Block 8: Render-/Output-Node-Settings auf vollständige Verträge prüfen + +Status: abgeschlossen + +- Still, Turntable, Blend, Alpha-, Output- und Notification-Pfade gegen Runtime vergleichen +- fehlende Dropdowns, Modusfelder oder Guards ergänzen + +Abnahme: + +- alle echten Runtime-Optionen für Still/Turntable/Blend/Notify sind im Editor authorbar +- keine Fake-Felder ohne Runtime-Wirkung +- Graph/Shadow/Legacy-Handoff-Semantik ist für Output/Notify im UI klar + +Ergebnis: + +- Render-Override-Felder mit Runtime-Autorität werden für `blender_still` und `blender_turntable` im Inspector gesperrt, solange `use_custom_render_settings` deaktiviert ist +- damit verschwindet eine echte Fake-Konfigurationslücke: Werte wie `samples`, `noise_threshold` oder `focal_length_mm` konnten zuvor im Graph gesetzt werden, obwohl die Runtime sie ohne aktivierte Overrides verwirft +- `output_save` und `notify` erklären ihre Handoff-Semantik jetzt explizit im Inspector, inklusive Shadow-/Graph-/Legacy-Verhalten und Failure-vs-Skip-Guards +- fokussierte Frontend- und Backend-Tests sichern die Render-/Output-/Notify-Contracts gegen erneuten Drift ab + +### Block 9: Validation-Fehler inventarisieren + +Status: abgeschlossen + +- alle häufigen Validation-Fehler auf Root-Cause-Kategorien mappen +- unterscheiden zwischen Modellierungsfehler, UI-Führung, Datenfehler und Legacy-Constraint + +Abnahme: + +- häufige Fehlertypen sind als feste Kategorien dokumentiert +- jeder Kategorie ist eine konkrete Gegenmaßnahme zugeordnet +- UI-Validation und Backend-Preflight sprechen dieselbe Sprache + +Ergebnis: + +- Inventar der real existierenden Preflight-/Validation-Codes in `VALIDATION_ERROR_INVENTORY_2026-04-12.md` dokumentiert +- feste Kategorien für `Context`, `Setup Chain`, `Data Source`, `Runtime Gap`, `Legacy Drift` und `Artifact Flow` definiert +- UI kann sich jetzt auf dieselbe Fehler-Taxonomie wie das Backend stützen + +### Block 10: Preflight-Texte und Fehlermeldungen schärfen + +Status: abgeschlossen + +- Fehler müssen konkret sagen, welche Verbindung oder Variable fehlt +- Legacy-vs-Graph-Abweichungen explizit benennen + +Abnahme: + +- Blocking-Issues benennen Node, Contract-Rolle und erwartete Abhilfe +- Preflight unterscheidet klar zwischen `error`, `warning`, `unsupported` und `legacy-drift` +- Dry-Run-Ausgabe ist für Autoren ohne Codekontext verständlich + +Ergebnis: + +- `WorkflowPreflightPanel` zeigt jetzt zusätzlich einen Fehlertyp pro Issue +- Preflight-Actions und Issue-Listen verwenden konkrete Gegenmaßnahmen für reale Backend-Codes +- Legacy-Drift, Runtime-Gaps und Context-/Setup-Probleme werden im UI sichtbar getrennt + +## Batch C: Authoring Surface And Node Organization + +### Block 11: Node-Katalog nach Modulen und Authoring-Stufen nachschärfen + +Status: abgeschlossen + +- Produktionsmodule als primäre Navigationsstruktur +- Families, Stages und Legacy/Bridge/Graph-Status sauber sichtbar machen + +Ergebnis: + +- Node-Katalog zeigt jetzt zusätzlich eine explizite `Stage Coverage`-Zusammenfassung für die aktuell sichtbare Filtermenge +- Modul- und Family-Struktur bleibt erhalten, wird aber um Stage-Sichtbarkeit und Runtime-Zählung als erstes Navigationssignal ergänzt +- Raw-Node-Karten machen nun Inputs, Inspector-Variablen, Outputs und Authoring-Muster direkt sichtbar + +### Block 12: Quick-Insert-/Right-Click-Menü härten + +Status: abgeschlossen + +- Canvas-Clipping beseitigen +- Tastatur- und Mausfluss robuster machen + +Ergebnis: + +- Menü bleibt weiter portal-basiert und viewport-geklammert +- zusätzliche Outside-Click-Schließung und sticky Guided-Navigation härten den Canvas-Authoring-Loop +- Menü priorisiert jetzt explizit Guided Authoring vor Raw Nodes + +### Block 13: Edge-Lifecycle vervollständigen + +Status: abgeschlossen + +- Verbindungen löschen, neu zuweisen und Konflikte sichtbar machen +- Alternative Inputs sauber führen + +Ergebnis: + +- belegte Target-Sockets werden beim Neuverbinden jetzt bewusst neu zugewiesen statt stumm geblockt +- neue Verbindungen selektieren sich direkt selbst und machen Reassignment sichtbar +- Duplicate-Verbindungen werden enger auf echte Handle-Duplikate geprüft + +### Block 14: Auto-Layout enterprise-grade machen + +Status: abgeschlossen + +- Überlappungen aktiv verhindern +- Knoten kollisionsfrei auseinanderdrücken +- wiederholbare, verständliche Layouts für große Graphen + +Ergebnis: + +- Auto-Layout arbeitet jetzt komponentenbasiert statt alle Subgraphs in ein globales Layering zu drücken +- getrennte Subgraphs werden in stabile vertikale Bänder gesetzt +- Layout-Reihenfolge bleibt deterministisch entlang der vorhandenen Canvas-Topologie + +### Block 15: Einheitliches Node-Größen- und Informationsmodell härten + +Status: abgeschlossen + +- gleiche Node-Größe, gleiche Informationshierarchie +- keine Sonderfälle pro Node-Familie + +Ergebnis: + +- Node-Karten im Browser spiegeln jetzt dieselbe Informationshierarchie wie Canvas/Inspector: Inputs, Variables, Outputs und Authoring Guidance +- Nodes mit reinem Socket-Contract vs. Inspector-Variablen sind unmittelbar unterscheidbar +- kein zusätzlicher Sonderfall pro Family notwendig, weil das Contract-Summary-Modell die Darstellung trägt + +## Batch D: Non-Legacy Still Workflow Completion + +### Block 16: kanonischen Still-Graph vervollständigen + +Status: abgeschlossen + +- saubere Referenzvariante ohne Legacy-Verkrüppelung +- alle nötigen Produktionsmodule im Editor verfügbar machen + +Ergebnis: + +- kanonische Graph-Blueprints sind jetzt nicht mehr nur ein einzelner Still-Pfad, sondern als echte Referenzvarianten für Still, Alpha-Still und Still-plus-Blend vorhanden +- Referenz-Bundles im Editor decken damit die produktionsrelevanten Still-Autorenpfade sauber ab +- die Graph-Varianten bleiben parallel zum Legacy-Pfad und überschreiben ihn nicht implizit + +### Block 17: Alpha-, Blend- und Varianten-Pfade komplettieren + +Status: abgeschlossen + +- alle Template- und Output-Varianten im Graph-Vertrag verfügbar machen +- Settings müssen 1:1 authorbar sein + +Ergebnis: + +- Alpha-Still-Blueprint setzt transparente Defaults explizit im Graph statt über Legacy-Nebenpfade +- Blend-Delivery ist jetzt als kanonischer Referenzpfad mit `export_blend`, dediziertem `output_save` und `notify` modelliert +- Artifact-Erkennung leitet Blend- und Still-Artefakte jetzt auch über terminale `notify`- und `output_save`-Knoten korrekt her + +### Block 18: Output-Type-Erstellung und Workflow-Bindings harmonisieren + +Status: abgeschlossen + +- neue Output Types müssen wieder sauber erzeugbar und bindbar sein +- keine impliziten Legacy-only Einstellungen mehr + +Ergebnis: + +- graph-fähige Still-, Alpha- und Blend-Blueprints werden als bindbare Standard-Workflows mit ausgesät +- Output-Type-Bindings erkennen Blend-Support jetzt auch bei workflow-first Referenzgraphen korrekt +- gezielte API- und Frontend-Tests sichern ab, dass neue Output Types an die neuen Referenzgraphen gebunden werden können + +## Batch E: Consolidation, QA And Release Gate + +### Block 19: Test- und Browser-QA sequenziell ausbauen + +Status: abgeschlossen + +- low-RAM Testfolge statt Parallel-Last +- Editor-Flows, Validation und Autorenpfade browserseitig prüfen + +Zwischenstand: + +- gezielter Frontend-Regressionstest deckt jetzt den echten Admin-Fall ab, dass valide graph-gebundene Output Types nicht fälschlich als `workflow_missing` blockiert werden +- Live-Browser-QA gegen `http://localhost:5173/admin` bestätigt, dass graph- und shadow-gebundene Output Types wieder korrekte Rollout-Zustände zeigen +- die falschen Blocker `The selected workflow definition could not be resolved.` und `Linked workflow needs contract fixes before rollout.` treten für valide Bindings nicht mehr auf + +Ergebnis: + +- Live-Browser-Durchlauf gegen `http://127.0.0.1:5173/workflows` bestätigt den echten Autorenpfad für den Graph-Editor: Workflow wählen, Order-Line-Kontext laden, Dry Run ausführen, Preflight lesen +- der Graph-Dry-Run des `Still Graph Blueprint` läuft mit echtem Order-Line-Kontext erfolgreich durch und macht `Run` danach korrekt freischaltbar +- der zuvor verbleibende `template_missing`-Drift im Smoke-Setup ist als echte Resource-Lücke identifiziert und geschlossen +- Smoke-Output-Types werden jetzt wie die Golden-Fälle aktiv an `BlenderStudio` gebunden, sodass der Dry-Run für die betroffene reale Order Line ohne Restwarnung durchläuft + +### Block 20: Commit, Doku-Update und Restrisiken festhalten + +Status: in Arbeit + +- saubere Zwischenabsicherung +- verbleibende echte Produktgaps transparent dokumentieren + +Zwischenstand: + +- Drift-Schließung ist gezielt abgesichert: `backend/tests/domains/test_workflow_smoke_harness.py` prüft die Smoke-Template-Zuordnung explizit +- Live-Verifikation gegen `http://localhost:8888/api/workflows/484ea831-b274-4ba2-8385-7957e0ccd7f3/preflight?context_id=d6d00bd5-3c4f-4df2-98c4-eaab37df2811` liefert nach Re-Provisionierung `Graph runtime is ready for this context.` +- die Änderung sitzt bewusst im Seed-/Harness-Pfad statt in der Runtime-Validierung; es wurde keine Warnung kaschiert + +Restrisiken: + +- Golden- und Smoke-Template-Bindings hängen aktuell an den vorhandenen Admin-Templates; bei manueller Template-Löschung in `/admin` entsteht der Drift korrekt erneut +- die Smoke-Provisionierung deckt weiter nur Still-Fälle ab; zusätzliche Smoke-Setups für Turntable oder Blend bleiben separat zu verifizieren +- Block 20 ist erst abgeschlossen, wenn der jetzige Zwischenstand als gezielter Commit gesichert ist + +## Quality Gates + +- Legacy-Workflow bleibt jederzeit lauffähig +- keine zusätzliche implizite Node-Logik ohne expliziten Contract +- Root-Kontext und Upstream-Verbindungen werden im Editor nicht vermischt +- Tests laufen sequenziell und gezielt +- jede neue UI-Aussage muss mit Registry/Runtime konsistent sein + +## Aktuelle Batch-Reihenfolge + +1. Block 20 vorbereiten und nur reale Restrisiken statt vermeintlicher UI-Brüche festhalten +2. verbleibende Editor-/Validation-Lücken nur noch aus echten Autorenflüssen ableiten +3. Zwischenabsicherung mit Commit schließen +4. danach mit dem nächsten Implementierungsblock fortfahren diff --git a/scripts/test_render_pipeline.py b/scripts/test_render_pipeline.py index f2be221..a44b1a3 100644 --- a/scripts/test_render_pipeline.py +++ b/scripts/test_render_pipeline.py @@ -494,6 +494,96 @@ def choose_template_backed_output_type( raise RuntimeError("No active template-backed still-image output type was found") +WORKFLOW_GOLDEN_TEMPLATE_BY_KEY = { + "still_legacy": "BlenderStudio", + "still_graph": "BlenderStudio", + "still_shadow": "BlenderStudio", + "turntable_graph": "Blender_Studio_Schadowcatcher_Anim", + "blend_graph": "BlenderStudio", +} + + +def workflow_golden_template_name(case: dict) -> str | None: + return WORKFLOW_GOLDEN_TEMPLATE_BY_KEY.get(str(case.get("key") or "").strip()) + + +def _template_output_type_ids(template: dict) -> list[str]: + linked_output_type_ids = [ + str(candidate_id) + for candidate_id in (template.get("output_type_ids") or []) + if candidate_id is not None + ] + fallback_output_type_id = template.get("output_type_id") + if fallback_output_type_id and str(fallback_output_type_id) not in linked_output_type_ids: + linked_output_type_ids.append(str(fallback_output_type_id)) + return linked_output_type_ids + + +def ensure_output_type_render_template_binding( + client: APIClient, + *, + output_type: dict, + preferred_template_name: str | None, +) -> list[dict]: + if not preferred_template_name: + return [] + + render_templates = get_render_templates(client) + preferred_template = find_named(render_templates, preferred_template_name) + if preferred_template is None: + raise RuntimeError( + f"Required render template '{preferred_template_name}' is missing in /admin" + ) + if not preferred_template.get("is_active", True): + raise RuntimeError( + f"Required render template '{preferred_template_name}' is inactive in /admin" + ) + + output_type_id = str(output_type["id"]) + changed = False + + for template in render_templates: + current_ids = _template_output_type_ids(template) + desired_ids = [candidate_id for candidate_id in current_ids if candidate_id != output_type_id] + + if template["id"] == preferred_template["id"] and output_type_id not in desired_ids: + desired_ids.append(output_type_id) + + if desired_ids == current_ids: + continue + + resp = client.patch( + f"/render-templates/{template['id']}", + json={"output_type_ids": desired_ids}, + ) + if resp.status_code != 200: + raise RuntimeError( + "Golden render-template link update failed " + f"({template.get('name')} → {output_type.get('name')}): " + f"{resp.status_code} {resp.text[:400]}" + ) + changed = True + + if changed: + info( + "Ensured golden template binding: " + f"{output_type.get('name')} -> {preferred_template_name}" + ) + + refreshed_templates = get_render_templates(client) + matches = render_template_candidates_for_output_type( + refreshed_templates, + output_type_id, + active_only=False, + ) + if not any(template.get("name") == preferred_template_name for template in matches): + raise RuntimeError( + "Golden render-template binding verification failed for " + f"{output_type.get('name')} -> {preferred_template_name}" + ) + return matches + + def build_output_type_workflow_snapshot(output_type: dict) -> dict: return { "workflow_definition_id": output_type.get("workflow_definition_id"), @@ -510,6 +600,13 @@ def smoke_workflow_name(execution_mode: str) -> str: return f"[Workflow Smoke] Canonical Still {execution_mode.title()}" +def workflow_smoke_template_name(execution_mode: str) -> str | None: + normalized_mode = str(execution_mode or "").strip().lower() + if normalized_mode in {"legacy", "graph", "shadow"}: + return "BlenderStudio" + return None + + def build_workflow_golden_cases() -> list[dict]: still_invocation = { "width": 1024, @@ -688,6 +785,18 @@ def ensure_workflow_still_smoke_resources( output_type = resp.json() info(f"Reusing smoke output type: {output_type_name}") + preferred_template_name = workflow_smoke_template_name(execution_mode) + if preferred_template_name: + bound_templates = ensure_output_type_render_template_binding( + client, + output_type=output_type, + preferred_template_name=preferred_template_name, + ) + info( + "Smoke template candidates: " + + ", ".join(template.get("name") or "" for template in bound_templates) + ) + workflow = None if execution_mode != "legacy": workflows = get_workflows(client) @@ -790,6 +899,18 @@ def ensure_workflow_golden_resources( output_type = resp.json() info(f"Reusing golden output type: {case['output_type_name']}") + preferred_template_name = workflow_golden_template_name(case) + if preferred_template_name: + bound_templates = ensure_output_type_render_template_binding( + client, + output_type=output_type, + preferred_template_name=preferred_template_name, + ) + info( + "Golden template candidates: " + + ", ".join(template.get("name") or "" for template in bound_templates) + ) + workflow = None workflow_name = case.get("workflow_name") workflow_config = case.get("workflow_config")