188 lines
7.8 KiB
Python
188 lines
7.8 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
"""Trustee node schema-compliance under the Pick-not-Push typed port system.
|
|
|
|
Verifies that:
|
|
- All three trustee actions (extractFromFiles, processDocuments,
|
|
syncToAccounting) declare ``ActionResult`` as output, matching what the
|
|
Python implementations actually return at runtime
|
|
(``ActionResult.isSuccess(documents=[...])``).
|
|
- processDocuments / syncToAccounting accept ``ActionResult`` (the producer
|
|
schema) plus ``DocumentList`` and ``Transit`` for back-compat.
|
|
- The ``documentList`` parameter is required, typed ``List[ActionDocument]``
|
|
(the concrete shape consumed by ``_resolveDocumentList``) and rendered via
|
|
the dataRef picker so the user can bind it to ``upstream → documents``.
|
|
- The end-to-end Trustee pipeline graph (extract -> process -> sync) passes
|
|
hard port-compat validation (validateGraph).
|
|
- actionNodeExecutor produces canonical ``documents`` field — no legacy
|
|
``documentList`` alias — so that DataRef path=['documents'] is the single
|
|
source of truth.
|
|
"""
|
|
|
|
import inspect
|
|
|
|
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
|
from modules.features.graphicalEditor.portTypes import PORT_TYPE_CATALOG
|
|
from modules.workflows.automation2.executors import actionNodeExecutor as _actionExec
|
|
from modules.workflows.automation2.graphUtils import validateGraph
|
|
|
|
|
|
def _node(nodeId: str) -> dict:
|
|
return next(n for n in STATIC_NODE_TYPES if n["id"] == nodeId)
|
|
|
|
|
|
def test_extractFromFiles_outputs_ActionResult():
|
|
"""Runtime returns ActionResult.isSuccess(documents=[...]) — see
|
|
actions/extractFromFiles.py. The adapter must declare the same."""
|
|
n = _node("trustee.extractFromFiles")
|
|
assert n["outputPorts"][0]["schema"] == "ActionResult"
|
|
|
|
|
|
def test_processDocuments_outputs_ActionResult():
|
|
n = _node("trustee.processDocuments")
|
|
assert n["outputPorts"][0]["schema"] == "ActionResult"
|
|
|
|
|
|
def test_syncToAccounting_outputs_ActionResult():
|
|
n = _node("trustee.syncToAccounting")
|
|
assert n["outputPorts"][0]["schema"] == "ActionResult"
|
|
|
|
|
|
def test_processDocuments_accepts_ActionResult_and_legacy():
|
|
"""processDocuments must accept ActionResult (the new producer schema for
|
|
extractFromFiles) plus DocumentList / Transit for back-compat."""
|
|
n = _node("trustee.processDocuments")
|
|
accepts = n["inputPorts"][0]["accepts"]
|
|
assert "ActionResult" in accepts
|
|
assert "DocumentList" in accepts
|
|
assert "Transit" in accepts
|
|
assert "UdmDocument" not in accepts, (
|
|
"UdmDocument was dropped from accepts during the Pick-not-Push schema cleanup."
|
|
)
|
|
|
|
|
|
def test_syncToAccounting_accepts_ActionResult_and_legacy():
|
|
n = _node("trustee.syncToAccounting")
|
|
accepts = n["inputPorts"][0]["accepts"]
|
|
assert "ActionResult" in accepts
|
|
assert "DocumentList" in accepts
|
|
assert "Transit" in accepts
|
|
|
|
|
|
def test_processDocuments_documentList_param_typed_required_dataRef():
|
|
"""documentList is a Pick-not-Push DataRef parameter — must be visible
|
|
and typed exactly like the producer field (``ActionResult.documents`` is
|
|
``List[ActionDocument]``) so DataPicker's strict-filter accepts it.
|
|
"""
|
|
params = {p["name"]: p for p in _node("trustee.processDocuments")["parameters"]}
|
|
p = params["documentList"]
|
|
assert p["type"] == "List[ActionDocument]", (
|
|
"documentList must declare the concrete producer type so the DataPicker "
|
|
"strict-filter resolves upstream ActionResult.documents as compatible."
|
|
)
|
|
assert p["required"] is True
|
|
assert p["frontendType"] == "dataRef", (
|
|
"documentList must use the dataRef renderer so the binding is visible"
|
|
)
|
|
|
|
|
|
def test_syncToAccounting_documentList_param_typed_required_dataRef():
|
|
params = {p["name"]: p for p in _node("trustee.syncToAccounting")["parameters"]}
|
|
p = params["documentList"]
|
|
assert p["type"] == "List[ActionDocument]", (
|
|
"documentList must declare the concrete producer type so the DataPicker "
|
|
"strict-filter resolves upstream ActionResult.documents as compatible."
|
|
)
|
|
assert p["required"] is True
|
|
assert p["frontendType"] == "dataRef", (
|
|
"documentList must use the dataRef renderer so the binding is visible"
|
|
)
|
|
|
|
|
|
def test_trustee_pipeline_graph_passes_hard_port_validation():
|
|
"""End-to-end pipeline: trigger.manual -> extract -> process -> sync.
|
|
|
|
Mirrors what frontend_nyla/.../trusteePipelineGraph.ts builds for
|
|
_buildScanUploadGraph. Port-compat must hold without warnings.
|
|
"""
|
|
graph = {
|
|
"nodes": [
|
|
{"id": "trigger-manual", "type": "trigger.manual", "parameters": {}},
|
|
{
|
|
"id": "extract",
|
|
"type": "trustee.extractFromFiles",
|
|
"parameters": {
|
|
"fileIds": ["f1"],
|
|
"featureInstanceId": "inst-1",
|
|
"prompt": "",
|
|
},
|
|
},
|
|
{
|
|
"id": "process",
|
|
"type": "trustee.processDocuments",
|
|
"parameters": {
|
|
"documentList": {"type": "ref", "nodeId": "extract", "path": ["documents"]},
|
|
"featureInstanceId": "inst-1",
|
|
},
|
|
},
|
|
{
|
|
"id": "sync",
|
|
"type": "trustee.syncToAccounting",
|
|
"parameters": {
|
|
"documentList": {"type": "ref", "nodeId": "process", "path": ["documents"]},
|
|
"featureInstanceId": "inst-1",
|
|
},
|
|
},
|
|
],
|
|
"connections": [
|
|
{"source": "trigger-manual", "sourceOutput": 0, "target": "extract", "targetInput": 0},
|
|
{"source": "extract", "sourceOutput": 0, "target": "process", "targetInput": 0},
|
|
{"source": "process", "sourceOutput": 0, "target": "sync", "targetInput": 0},
|
|
],
|
|
}
|
|
nodeTypeIds = {n["id"] for n in STATIC_NODE_TYPES}
|
|
errors = validateGraph(graph, nodeTypeIds)
|
|
portMismatches = [e for e in errors if "Port mismatch" in e]
|
|
assert not portMismatches, f"Trustee pipeline must be port-compatible: {portMismatches}"
|
|
|
|
|
|
def test_catalog_ActionResult_exposes_documents_field():
|
|
"""Without ``documents`` on the ActionResult schema the DataPicker cannot
|
|
surface the canonical list-of-documents path that every downstream node
|
|
(processDocuments, syncToAccounting, AI consumers, ...) needs to bind to.
|
|
"""
|
|
schema = PORT_TYPE_CATALOG.get("ActionResult")
|
|
assert schema is not None
|
|
fieldNames = {f.name for f in schema.fields}
|
|
assert "documents" in fieldNames, (
|
|
"ActionResult.documents must be in PORT_TYPE_CATALOG so the frontend "
|
|
"DataPicker can offer it as a bindable path."
|
|
)
|
|
|
|
|
|
def test_catalog_ActionDocument_is_registered():
|
|
"""ActionResult.documents is List[ActionDocument]; the inner schema must
|
|
be registered so the picker can drill down to ``documents → * → documentName``.
|
|
"""
|
|
schema = PORT_TYPE_CATALOG.get("ActionDocument")
|
|
assert schema is not None
|
|
fieldNames = {f.name for f in schema.fields}
|
|
assert {"documentName", "documentData", "mimeType"}.issubset(fieldNames), (
|
|
"ActionDocument schema must mirror datamodelChat.ActionDocument."
|
|
)
|
|
|
|
|
|
def test_actionNodeExecutor_does_not_emit_legacy_documentList_alias():
|
|
"""Source-code assertion: out dict in execute() must not write documentList alias.
|
|
|
|
Pick-not-Push canonicalises on ``documents``. Removing the alias prevents
|
|
DataRefs from drifting back to the legacy field name.
|
|
"""
|
|
src = inspect.getsource(_actionExec)
|
|
assert '"documentList": docsList' not in src, (
|
|
"Legacy alias ``documentList`` must be removed from actionNodeExecutor "
|
|
"out-dict (use canonical ``documents`` only — see issues.md "
|
|
"'Trustee Schema-Compliance')."
|
|
)
|
|
assert '"documents": docsList' in src, (
|
|
"Canonical ``documents`` field missing from actionNodeExecutor out-dict."
|
|
)
|