Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion backend/orbit_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import UTC, datetime
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Literal

Expand Down Expand Up @@ -107,7 +108,21 @@ def register(handler: Callable[..., Any]) -> Callable[..., Any]:
)
self._nodes[node_id] = node
setattr(handler, "__orbit_graph_node__", node)
return handler

@wraps(handler)
def instrumented(*args: Any, **kwargs: Any) -> Any:
context = next(
(value for value in (*args, *kwargs.values()) if isinstance(value, RunnerContext)),
None,
)
if context is None:
return handler(*args, **kwargs)
with context.function(node_id):
return handler(*args, **kwargs)

setattr(instrumented, "__orbit_graph_node__", node)
setattr(handler, "__orbit_graph_wrapper__", instrumented)
return instrumented

return register

Expand Down Expand Up @@ -241,6 +256,7 @@ def function(self, function_id: str):
if not function_id.strip():
raise ValueError("function_id must not be empty")
started = datetime.now(UTC)
self.log(f"workflow function started: {function_id}")
self.emit_result(
{
"workflow_functions": [
Expand All @@ -255,6 +271,7 @@ def function(self, function_id: str):
try:
yield
except BaseException:
self.log(f"workflow function failed: {function_id}")
self.emit_result(
{
"workflow_functions": [
Expand All @@ -269,6 +286,7 @@ def function(self, function_id: str):
)
raise
else:
self.log(f"workflow function succeeded: {function_id}")
self.emit_result(
{
"workflow_functions": [
Expand Down Expand Up @@ -1536,6 +1554,7 @@ def main(self) -> None:
os.environ.get("ORBIT_EXECUTION_MODE", "run"),
int(os.environ.get("ORBIT_LOOP_INDEX", "1")),
)
handler = getattr(handler, "__orbit_graph_wrapper__", handler)
before = context.git_head()
try:
handler(context)
Expand Down
32 changes: 32 additions & 0 deletions backend/tests/test_orbit_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from base64 import b64encode

import orbit_sdk as sdk
import pytest


def test_graph_declarations_export_nodes_and_typed_edges():
Expand Down Expand Up @@ -62,6 +63,37 @@ def test_function_trace_emits_successful_function_evidence(tmp_path, capsys):
assert "collect-source-evidence" in capsys.readouterr().out


def test_graph_step_automatically_traces_its_execution(tmp_path, capsys):
graph = sdk.Graph()

@graph.step("collect-source-evidence")
def collect(ctx) -> None:
ctx.log("Collected source evidence")

collect(context(tmp_path, iteration=1))

output = capsys.readouterr().out
assert "workflow function started: collect-source-evidence" in output
assert "workflow function succeeded: collect-source-evidence" in output
assert '"status": "running"' in output
assert '"status": "succeeded"' in output


def test_graph_step_automatically_traces_failures(tmp_path, capsys):
graph = sdk.Graph()

@graph.step("collect-source-evidence")
def collect(ctx) -> None:
raise RuntimeError("evidence unavailable")

with pytest.raises(RuntimeError, match="evidence unavailable"):
collect(context(tmp_path, iteration=1))

output = capsys.readouterr().out
assert "workflow function failed: collect-source-evidence" in output
assert '"status": "failed"' in output


def context(project, *, iteration: int, run_id: str = "run-123"):
return sdk.RunnerContext(
phase="execute",
Expand Down
Loading