Remote Type Fidelity
A flow passes values from one step to the next as if every step ran in the same process: a step puts a value on
its result, and the flow hands it to the next step's config. When the steps run on endpoints, those values have
to cross the network, and a naive transport would flatten them on the way: a tuple would come back as a list,
an enum member as its string, a nested structure as a bag of generic containers. Code that checks
isinstance, uses a tuple as a dict key, or compares enum members would then behave differently on a
remote step than on a local one.
Orcraft keeps the types intact. Arguments are serialized to the endpoint and results serialized back with
their Python types preserved, lists, tuples and dicts are walked so nested structures keep their shape, and enum
members and instances of the suite's own classes arrive as what they were, because the
test package is on the endpoint too. This is
what lets a flow be written against localhost and moved to a lab later without touching the code
that wires one step's output into the next step's input. The framework reference covers it under
type fidelity.
The Type Fidelity test of the Endpoints group makes the guarantee visible: one step assembles typed values on an endpoint, and a second step on the same endpoint checks every type after the round trip through the controller.
Producing typed values
Source directory:
orcraft-test-suite-tutorial/test_group_endpoints/test_type_fidelity/
The first step solves a maze and builds values of the kinds that a careless transport would damage: a tuple of
dimensions, an enum member from the suite's own MazeSolveStatus, tuple coordinates, and a dict
that nests tuples, an enum and another dict:
def _run_step(self) -> OrcraftTestStepResult:
self.result = StepProduceMazeTypedDataResult()
exe_folder = normalize_long_path(self.config.maze_executable_folder)
width = int(self.config.width)
height = int(self.config.height)
result = MazeApp(exe_folder, "solve", width=width, height=height).run()
# Determine solve status from the run result
status = result.status
maze_area = width * height
# Assemble typed data that exercises RPC type preservation:
# - tuple (would flatten to list without proper serialization)
# - enum (would flatten to string value without proper serialization)
# - nested dict with mixed types
self.result.dimensions = (width, height)
self.result.solve_status = status
self.result.start_coords = (0, 0)
self.result.metadata = {
"bounds": (0, 0, width - 1, height - 1),
"tags": ("maze", "tutorial", "generated"),
"status": status,
"nested": {
"area": maze_area,
"pair": (True, False),
},
}
Logger.log(f"dimensions = {self.result.dimensions!r}", color=Logger.Color.BLUE, module_tag=False)
Logger.log(f"solve_status = {self.result.solve_status!r}", color=Logger.Color.BLUE, module_tag=False)
Logger.log(f"start_coords = {self.result.start_coords!r}", color=Logger.Color.BLUE, module_tag=False)
Logger.log(f"metadata = {self.result.metadata!r}", color=Logger.Color.BLUE, module_tag=False)
return self.result
The values are plain attributes of the result rather than declared result fields. Structured data is rarely something a person wants as a column in the controller, and plain attributes travel to the flow without ever becoming one; the framework reference draws the line between the two.
Verifying the types
The flow passes the four values to the second step as config values, exactly as it would pass a path or a number:
step_2_result = self.run_step(
StepVerifyMazeTypedData,
endpoint=self.config.test_endpoint,
dimensions=step_1_result.dimensions,
solve_status=step_1_result.solve_status,
start_coords=step_1_result.start_coords,
metadata=step_1_result.metadata,
)
The second step declares them as plain attributes of its config, prints each one with its type, and asserts the type with a checkpoint:
class StepVerifyMazeTypedDataConfig(OrcraftTestStepConfig):
name = OrcraftConfigField.Text(default="Verify Maze Typed Data")
dimensions = None
solve_status = None
start_coords = None
metadata = None
...
self.checkpoint(isinstance(dimensions, tuple), "'dimensions' is tuple", f"'dimensions' type is {type(dimensions).__name__}")
self.checkpoint(isinstance(dimensions, tuple) and len(dimensions) == 2, "'dimensions' has length 2", "'dimensions' length is not 2")
self.checkpoint(isinstance(solve_status, MazeSolveStatus), "'solve_status' is MazeSolveStatus enum", f"'solve_status' type is {type(solve_status).__name__}")
self.checkpoint(isinstance(solve_status, MazeSolveStatus) and solve_status in list(MazeSolveStatus), "'solve_status' has valid value", "'solve_status' value is not valid")
self.checkpoint(isinstance(start_coords, tuple), "'start_coords' is tuple", f"'start_coords' type is {type(start_coords).__name__}")
self.checkpoint(isinstance(metadata, dict), "'metadata' is dict", f"'metadata' type is {type(metadata).__name__}")
self.checkpoint(isinstance(bounds, tuple), "'metadata.bounds' is tuple", f"'metadata.bounds' type is {type(bounds).__name__}")
self.checkpoint(isinstance(tags, tuple), "'metadata.tags' is tuple", f"'metadata.tags' type is {type(tags).__name__}")
self.checkpoint(isinstance(meta_status, MazeSolveStatus), "'metadata.status' is MazeSolveStatus enum", f"'metadata.status' type is {type(meta_status).__name__}")
self.checkpoint(isinstance(nested, dict), "'metadata.nested' is dict", f"'metadata.nested' type is {type(nested).__name__}")
self.checkpoint(isinstance(pair, tuple), "'metadata.nested.pair' is tuple", f"'metadata.nested.pair' type is {type(pair).__name__}")
self.checkpoint(isinstance(area, int), "'metadata.nested.area' is int", f"'metadata.nested.area' type is {type(area).__name__}")
Each value made two trips: from the first step on the endpoint to the flow on the controller, and from the flow back to the second step on the endpoint. A type lost on either leg would fail its checkpoint.
Running the test
Select the Type Fidelity test, set its Maze Executable Folder, and choose the
macOS machine as the Test Endpoint so the values cross both a network and an operating
system boundary. Click Update Plan, then
Run and confirm. When the run finishes, search the log for
'metadata.nested.area':
The verify step prints what it received: (5, 8) with type tuple,
<MazeSolveStatus.SOLVED: 'solved'> with type MazeSolveStatus, the nested
(True, False) still a tuple, and the area still an int. Every
checkpoint below passes.
Click Details for the run:
Both steps ran on GMG-TEST-5, and the typed values never appear in the tree: they were plain attributes, handed from step to flow to step, and only the checkpoints that judged them left a trace. Choose localhost as the endpoint instead and the same test passes the same way, which is the point: the code that passes values between steps does not know or care where the steps run.
The tests of the tour are all loaded and configured by now. The next page, Sessions, saves that state to a file and brings it back.