Skip to content

Commit 7d25020

Browse files
committed
test: init flow
1 parent 7588cbe commit 7d25020

9 files changed

Lines changed: 339 additions & 9 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Common utilities for OpenAI Agents testcases."""
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
Simple trace assertion - just check that expected spans exist with required attributes.
3+
Much simpler than the tree-based approach.
4+
"""
5+
6+
import json
7+
from typing import Any
8+
9+
10+
def load_traces(traces_file: str) -> list[dict[str, Any]]:
11+
"""Load traces from a JSONL file."""
12+
traces = []
13+
with open(traces_file, "r", encoding="utf-8") as f:
14+
for line in f:
15+
if line.strip():
16+
traces.append(json.loads(line))
17+
return traces
18+
19+
20+
def load_expected_traces(expected_file: str) -> list[dict[str, Any]]:
21+
"""Load expected trace definitions from a JSON file."""
22+
with open(expected_file, "r", encoding="utf-8") as f:
23+
data = json.load(f)
24+
return data.get("required_spans", [])
25+
26+
27+
def get_attributes(span: dict[str, Any]) -> dict[str, Any]:
28+
"""
29+
Parse attributes from a span.
30+
Supports both formats:
31+
- Old format: 'Attributes' as a JSON string
32+
- New format: 'attributes' as a dict
33+
"""
34+
# New format: attributes is already a dict
35+
if "attributes" in span and isinstance(span["attributes"], dict):
36+
return span["attributes"]
37+
# Old format: Attributes is a JSON string
38+
attributes_str = span.get("Attributes", "{}")
39+
try:
40+
return json.loads(attributes_str)
41+
except json.JSONDecodeError:
42+
return {}
43+
44+
45+
def matches_value(expected_value: Any, actual_value: Any) -> bool:
46+
"""
47+
Check if an actual value matches the expected value.
48+
Supports:
49+
- List of possible values: ["value1", "value2"]
50+
- Wildcard: "*" (any value accepted)
51+
- Exact match: "value"
52+
"""
53+
# Wildcard - accept any value
54+
if expected_value == "*":
55+
return True
56+
# List of possible values
57+
if isinstance(expected_value, list):
58+
return actual_value in expected_value
59+
# Exact match
60+
return expected_value == actual_value
61+
62+
63+
def matches_expected(span: dict[str, Any], expected: dict[str, Any]) -> bool:
64+
"""
65+
Check if a span matches the expected definition.
66+
Supports both formats:
67+
- Old format: 'Name', 'SpanType' fields
68+
- New format: 'name', 'attributes.span_type' fields
69+
"""
70+
# Check name - can be a string or list of possible names
71+
expected_name = expected.get("name")
72+
# Support both old format (Name) and new format (name)
73+
actual_name = span.get("name") or span.get("Name")
74+
if isinstance(expected_name, list):
75+
if actual_name not in expected_name:
76+
return False
77+
elif expected_name != actual_name:
78+
return False
79+
# Check span type if specified
80+
if "span_type" in expected:
81+
# Old format: SpanType field
82+
# New format: attributes.span_type field
83+
actual_span_type = span.get("SpanType")
84+
if not actual_span_type:
85+
actual_attrs = get_attributes(span)
86+
actual_span_type = actual_attrs.get("span_type")
87+
if actual_span_type != expected["span_type"]:
88+
return False
89+
# Check attributes if specified
90+
if "attributes" in expected:
91+
actual_attrs = get_attributes(span)
92+
for key, expected_value in expected["attributes"].items():
93+
if key not in actual_attrs:
94+
return False
95+
# Use flexible value matching
96+
if not matches_value(expected_value, actual_attrs[key]):
97+
return False
98+
return True
99+
100+
101+
def assert_traces(traces_file: str, expected_file: str) -> None:
102+
"""
103+
Assert that all expected traces exist in the traces file.
104+
Args:
105+
traces_file: Path to the traces.jsonl file
106+
expected_file: Path to the expected_traces.json file
107+
Raises:
108+
AssertionError: If any expected trace is not found
109+
"""
110+
traces = load_traces(traces_file)
111+
expected_spans = load_expected_traces(expected_file)
112+
print(f"Loaded {len(traces)} traces from {traces_file}")
113+
print(f"Checking {len(expected_spans)} expected spans...")
114+
missing_spans = []
115+
for expected in expected_spans:
116+
# Find a matching span
117+
found = False
118+
name = expected["name"]
119+
# Handle both string and list of names
120+
name_str = name if isinstance(name, str) else f"[{' | '.join(name)}]"
121+
122+
for span in traces:
123+
if matches_expected(span, expected):
124+
found = True
125+
print(f"✓ Found span: {name_str}")
126+
break
127+
if not found:
128+
missing_spans.append(name_str)
129+
print(f"✗ Missing span: {name_str}")
130+
131+
print("Traces file content:")
132+
with open(traces_file, "r", encoding="utf-8") as f:
133+
print(f.read())
134+
if missing_spans:
135+
print(f"\n=== Dumping raw traces from {traces_file} ===")
136+
with open(traces_file, "r", encoding="utf-8") as f:
137+
print(f.read())
138+
print("\n=== End of traces dump ===\n")
139+
raise AssertionError(
140+
f"Missing expected spans: {', '.join(missing_spans)}\n"
141+
f"Expected {len(expected_spans)} spans, found {len(expected_spans) - len(missing_spans)}"
142+
)
143+
print(f"\n✓ All {len(expected_spans)} expected spans found!")

packages/uipath-openai-agents/testcases/common/validate_output.sh

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ debug_print_uipath_output() {
2222
fi
2323
}
2424

25-
validate_output() {
26-
echo "Printing output file for validation..."
27-
debug_print_uipath_output
28-
29-
echo "Validating output..."
30-
python src/assert.py || { echo "Validation failed!"; exit 1; }
31-
32-
echo "Testcase completed successfully."
25+
# Run assertions from the testcase's src directory
26+
run_assertions() {
27+
echo "Running assertions..."
28+
if [ -f "src/assert.py" ]; then
29+
# Use the Python from the virtual environment
30+
# Prepend the common directory to the python path so it can be resolved
31+
PYTHONPATH="../common:$PYTHONPATH" python src/assert.py
32+
else
33+
echo "assert.py not found in src directory!"
34+
exit 1
35+
fi
3336
}
3437

35-
validate_output
38+
debug_print_uipath_output
39+
run_assertions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# OpenAI Agents Integration Test - Init Flow
2+
3+
This testcase validates the complete init-flow for OpenAI Agents SDK integration with UiPath.
4+
5+
## What it tests
6+
7+
1. **Project Setup**: Creates a new UiPath agent project using `uipath new agent`
8+
2. **Initialization**: Runs `uipath init` to generate configuration files
9+
3. **Packaging**: Packs the agent into a NuGet package
10+
4. **Deployment Execution**: Runs the agent with UiPath platform integration
11+
5. **Local Execution**: Runs the agent locally with tracing enabled
12+
6. **Output Validation**: Validates the agent output structure and status
13+
7. **Trace Validation**: Verifies that expected OpenTelemetry spans are generated
14+
15+
## Files
16+
17+
- **pyproject.toml**: Project dependencies and configuration
18+
- **input.json**: Test input for the agent
19+
- **run.sh**: Main test script that executes all steps
20+
- **expected_traces.json**: Expected OpenTelemetry spans for validation
21+
- **src/assert.py**: Assertion script that validates outputs and traces
22+
23+
## Expected Traces
24+
25+
The test validates that the following spans are generated:
26+
27+
- **Agent workflow**: OpenAI Agents SDK top-level agent span (AGENT kind)
28+
- **response**: OpenAI Responses API call span (LLM kind) - note that OpenAI Agents SDK uses the Responses API, not ChatCompletion
29+
30+
## Running the test
31+
32+
```bash
33+
cd testcases/init-flow
34+
bash run.sh
35+
```
36+
37+
The test requires:
38+
- `CLIENT_ID`: UiPath OAuth client ID
39+
- `CLIENT_SECRET`: UiPath OAuth client secret
40+
- `BASE_URL`: UiPath platform base URL
41+
42+
## Success Criteria
43+
44+
The test passes if:
45+
46+
1. NuGet package (.nupkg) is created successfully
47+
2. Agent executes without errors (status: "successful")
48+
3. Output contains the expected "result" field
49+
4. Local run output contains "Successful execution."
50+
5. All expected OpenTelemetry spans are present in traces.jsonl
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"description": "OpenAI Agents SDK integration - checks key spans exist",
3+
"required_spans": [
4+
{
5+
"name": "Agent workflow",
6+
"attributes": {
7+
"openinference.span.kind": "AGENT"
8+
}
9+
},
10+
{
11+
"name": "response",
12+
"attributes": {
13+
"openinference.span.kind": "LLM",
14+
"llm.system": "openai"
15+
}
16+
}
17+
]
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"messages": "Hello, how are you?"
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[project]
2+
name = "agent"
3+
version = "0.0.1"
4+
description = "OpenAI Agents integration test"
5+
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
6+
dependencies = [
7+
"openai>=1.0.0",
8+
"openai-agents>=0.6.5",
9+
"uipath-openai-agents",
10+
]
11+
requires-python = ">=3.11"
12+
13+
[tool.uv.sources]
14+
uipath-openai-agents = { path = "../../", editable = true }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo "Syncing dependencies..."
5+
uv sync
6+
7+
echo "Backing up pyproject.toml..."
8+
cp pyproject.toml pyproject-overwrite.toml
9+
10+
echo "Creating new UiPath agent..."
11+
uv run uipath new agent
12+
13+
# uipath new overwrites pyproject.toml, so we need to copy it back
14+
echo "Restoring pyproject.toml..."
15+
cp pyproject-overwrite.toml pyproject.toml
16+
uv sync
17+
18+
echo "Authenticating with UiPath..."
19+
uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL"
20+
21+
echo "Initializing UiPath..."
22+
uv run uipath init
23+
24+
echo "Packing agent..."
25+
uv run uipath pack
26+
27+
echo "Input from input.json file"
28+
uv run uipath run agent --file input.json
29+
30+
echo "Running agent again with empty UIPATH_JOB_KEY..."
31+
export UIPATH_JOB_KEY=""
32+
uv run uipath run agent --trace-file .uipath/traces.jsonl --file input.json >> local_run_output.log
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import json
2+
import os
3+
4+
from trace_assert import assert_traces
5+
6+
print("Checking init-flow output...")
7+
8+
# Check NuGet package
9+
uipath_dir = ".uipath"
10+
assert os.path.exists(uipath_dir), "NuGet package directory (.uipath) not found"
11+
12+
nupkg_files = [f for f in os.listdir(uipath_dir) if f.endswith(".nupkg")]
13+
assert nupkg_files, "NuGet package file (.nupkg) not found in .uipath directory"
14+
15+
print(f"NuGet package found: {nupkg_files[0]}")
16+
17+
# Check agent output file
18+
output_file = "__uipath/output.json"
19+
assert os.path.isfile(output_file), "Agent output file not found"
20+
21+
print("Agent output file found")
22+
23+
# Check status and required fields
24+
with open(output_file, "r", encoding="utf-8") as f:
25+
output_data = json.load(f)
26+
27+
# Check status
28+
status = output_data.get("status")
29+
assert status == "successful", f"Agent execution failed with status: {status}"
30+
31+
print("Agent execution status: successful")
32+
33+
# Check required fields for OpenAI agent
34+
assert "output" in output_data, "Missing 'output' field in agent response"
35+
36+
output_content = output_data["output"]
37+
assert "result" in output_content, "Missing 'result' field in output"
38+
39+
result = output_content["result"]
40+
assert result and isinstance(result, (str, dict, list)), (
41+
"Result field is empty or invalid type"
42+
)
43+
44+
print(f"Result field validated: {type(result).__name__}")
45+
46+
# Check local run output
47+
with open("local_run_output.log", "r", encoding="utf-8") as f:
48+
local_run_output = f.read()
49+
50+
# Check if response contains 'Successful execution.'
51+
assert "Successful execution." in local_run_output, (
52+
f"Response does not contain 'Successful execution.'. Actual response: {local_run_output}"
53+
)
54+
55+
print("Local run output validated")
56+
57+
# Check traces
58+
with open(".uipath/traces.jsonl", "r", encoding="utf-8") as f:
59+
local_run_traces = f.read()
60+
print(f"Traces generated: {len(local_run_traces)} bytes")
61+
62+
# Simple trace assertions - just check that expected spans exist
63+
assert_traces(".uipath/traces.jsonl", "expected_traces.json")
64+
65+
print("All validations passed successfully!")

0 commit comments

Comments
 (0)