Skip to content

Commit a6bcf3a

Browse files
committed
test: add debug-breakpoints integration test for llamaindex workflows
pexpect-based tests that exercise the interactive debugger against the debug-agent sample: single breakpoint, multiple breakpoints, step mode, and quit. run.sh copies agent files from the sample at test time instead of duplicating source files.
1 parent 57ed2bb commit a6bcf3a

5 files changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"ticket_id": "T-12345", "customer_message": "The payment system is broken!", "customer_tier": "premium"}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[project]
2+
name = "debug-breakpoints-test"
3+
version = "0.0.1"
4+
description = "Test case for debug breakpoint functionality with LlamaIndex workflows"
5+
authors = [{ name = "UiPath", email = "test@uipath.com" }]
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"uipath-llamaindex",
9+
"pexpect>=4.9.0",
10+
"pyte>=0.8.0",
11+
"pytest>=8.0.0",
12+
"pytest-asyncio>=0.24.0",
13+
]
14+
15+
[tool.uv.sources]
16+
uipath-llamaindex = { path = "../../", editable = true }
17+
18+
[tool.pytest.ini_options]
19+
asyncio_mode = "auto"
20+
asyncio_default_fixture_loop_scope = "function"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
set -e
3+
4+
SAMPLE_DIR="../../samples/debug-agent"
5+
6+
echo "Copying agent files from debug-agent sample..."
7+
cp "$SAMPLE_DIR/main.py" src/main.py
8+
cp "$SAMPLE_DIR/llama_index.json" llama_index.json
9+
cp "$SAMPLE_DIR/uipath.json" uipath.json
10+
11+
echo "Syncing dependencies..."
12+
uv sync
13+
14+
echo "Initializing the project..."
15+
uv run uipath init
16+
17+
echo "=== Running debug breakpoint tests with pexpect ==="
18+
uv run pytest src/test_debug.py -v -s
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Debug breakpoints tests are run via pytest in test_debug.py
2+
# This file is a placeholder for the testcase runner convention.
3+
4+
print("Debug breakpoints tests completed via pytest.")
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Pexpect-based tests for uipath debug command with LlamaIndex workflows.
3+
4+
Tests the interactive debugger functionality including:
5+
- Single breakpoint
6+
- Multiple breakpoints
7+
- Step mode (s command)
8+
- Quit debugger (q command)
9+
10+
Regression test for: ContextStateError when using wait_for_event in
11+
breakpoint wrapper (the wrapper must use an InternalContext, not the
12+
workflow-level ExternalContext).
13+
"""
14+
15+
import re
16+
import sys
17+
from pathlib import Path
18+
from typing import Optional
19+
20+
import pexpect
21+
import pytest
22+
23+
24+
# ---------------------------------------------------------------------------
25+
# Minimal PromptTest helper (mirrors uipath-langchain-python/testcases/common)
26+
# ---------------------------------------------------------------------------
27+
28+
def strip_ansi(text: str) -> str:
29+
return re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])').sub('', text)
30+
31+
32+
class PromptTest:
33+
def __init__(self, command: str, test_name: str, prompt: str = "> ", timeout: int = 60):
34+
self.command = command
35+
self.test_name = test_name
36+
self.prompt = prompt
37+
self.timeout = timeout
38+
self.child: Optional[pexpect.spawn] = None
39+
self._log_handle = None
40+
self._log_path = Path(f"{test_name}.log")
41+
42+
def start(self):
43+
self.child = pexpect.spawn(self.command, encoding="utf-8", timeout=self.timeout)
44+
self._log_handle = open(self._log_path, "w")
45+
self.child.logfile_read = self._log_handle
46+
47+
def send_command(self, command: str, expect: Optional[str] = None):
48+
self.child.expect(self.prompt)
49+
self.child.sendline(command)
50+
if expect:
51+
self.child.expect(expect)
52+
53+
def expect_eof(self):
54+
self.child.expect(pexpect.EOF, timeout=self.timeout)
55+
56+
def get_output(self) -> str:
57+
if self._log_path.exists():
58+
if self._log_handle:
59+
self._log_handle.flush()
60+
with open(self._log_path, "r", encoding="utf-8") as f:
61+
return strip_ansi(f.read())
62+
return ""
63+
64+
@property
65+
def before(self) -> str:
66+
return self.child.before if self.child else ""
67+
68+
def close(self):
69+
if self._log_handle:
70+
self._log_handle.close()
71+
self._log_handle = None
72+
if self.child:
73+
self.child.close()
74+
self.child = None
75+
76+
77+
# ---------------------------------------------------------------------------
78+
# Test configuration
79+
# ---------------------------------------------------------------------------
80+
81+
COMMAND = "uv run uipath debug agent --file input.json"
82+
PROMPT = r"> "
83+
TIMEOUT = 60
84+
85+
86+
# ---------------------------------------------------------------------------
87+
# Tests
88+
# ---------------------------------------------------------------------------
89+
90+
def test_single_breakpoint():
91+
"""Test setting and hitting a single breakpoint."""
92+
test = PromptTest(command=COMMAND, test_name="debug_single_breakpoint", prompt=PROMPT, timeout=TIMEOUT)
93+
try:
94+
test.start()
95+
96+
test.send_command("b classify_category", expect=r"Breakpoint set at: classify_category")
97+
test.send_command("c", expect=r"BREAKPOINT.*classify_category.*before")
98+
test.send_command("c", expect=r"Debug session completed")
99+
100+
test.expect_eof()
101+
102+
output = test.get_output()
103+
assert "ticket_id" in output, "Expected ticket_id in output"
104+
105+
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF) as e:
106+
print(f"\nERROR: {type(e).__name__}", file=sys.stderr)
107+
print(f"\n--- Output before failure ---\n{test.before}", file=sys.stderr)
108+
pytest.fail(f"Test failed: {e}")
109+
finally:
110+
test.close()
111+
112+
113+
def test_multiple_breakpoints():
114+
"""Test setting and hitting multiple breakpoints."""
115+
test = PromptTest(command=COMMAND, test_name="debug_multiple_breakpoints", prompt=PROMPT, timeout=TIMEOUT)
116+
try:
117+
test.start()
118+
119+
test.send_command("b analyze_sentiment", expect=r"Breakpoint set at: analyze_sentiment")
120+
test.send_command("b determine_priority", expect=r"Breakpoint set at: determine_priority")
121+
test.send_command("c", expect=r"BREAKPOINT.*analyze_sentiment.*before")
122+
test.send_command("c", expect=r"BREAKPOINT.*determine_priority.*before")
123+
test.send_command("c", expect=r"Debug session completed")
124+
125+
test.expect_eof()
126+
127+
output = test.get_output()
128+
breakpoint_count = output.count("BREAKPOINT")
129+
assert breakpoint_count >= 2, f"Expected at least 2 breakpoints hit, got {breakpoint_count}"
130+
131+
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF) as e:
132+
print(f"\nERROR: {type(e).__name__}", file=sys.stderr)
133+
print(f"\n--- Output before failure ---\n{test.before}", file=sys.stderr)
134+
pytest.fail(f"Test failed: {e}")
135+
finally:
136+
test.close()
137+
138+
139+
def test_step_mode():
140+
"""Test step mode - breaks on every node."""
141+
test = PromptTest(command=COMMAND, test_name="debug_step_mode", prompt=PROMPT, timeout=TIMEOUT)
142+
try:
143+
test.start()
144+
145+
# Step through all 9 workflow steps
146+
test.send_command("s", expect=r"BREAKPOINT.*analyze_sentiment.*before")
147+
test.send_command("s", expect=r"BREAKPOINT.*classify_category.*before")
148+
test.send_command("s", expect=r"BREAKPOINT.*check_urgency.*before")
149+
test.send_command("s", expect=r"BREAKPOINT.*determine_priority.*before")
150+
test.send_command("s", expect=r"BREAKPOINT.*check_escalation.*before")
151+
test.send_command("s", expect=r"BREAKPOINT.*route_to_department.*before")
152+
test.send_command("s", expect=r"BREAKPOINT.*assign_queue.*before")
153+
test.send_command("s", expect=r"BREAKPOINT.*generate_response.*before")
154+
test.send_command("s", expect=r"BREAKPOINT.*finalize_ticket.*before")
155+
test.send_command("s", expect=r"Debug session completed")
156+
157+
test.expect_eof()
158+
159+
output = test.get_output()
160+
breakpoint_count = output.count("BREAKPOINT")
161+
assert breakpoint_count >= 9, f"Expected at least 9 breakpoints in step mode, got {breakpoint_count}"
162+
163+
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF) as e:
164+
print(f"\nERROR: {type(e).__name__}", file=sys.stderr)
165+
print(f"\n--- Output before failure ---\n{test.before}", file=sys.stderr)
166+
pytest.fail(f"Test failed: {e}")
167+
finally:
168+
test.close()
169+
170+
171+
def test_quit_debugger():
172+
"""Test quitting the debugger early with 'q' command."""
173+
test = PromptTest(command=COMMAND, test_name="debug_quit", prompt=PROMPT, timeout=TIMEOUT)
174+
try:
175+
test.start()
176+
177+
test.send_command("b check_urgency", expect=r"Breakpoint set at: check_urgency")
178+
test.send_command("c", expect=r"BREAKPOINT.*check_urgency.*before")
179+
test.send_command("q")
180+
181+
test.expect_eof()
182+
183+
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF) as e:
184+
print(f"\nERROR: {type(e).__name__}", file=sys.stderr)
185+
print(f"\n--- Output before failure ---\n{test.before}", file=sys.stderr)
186+
pytest.fail(f"Test failed: {e}")
187+
finally:
188+
test.close()
189+
190+
191+
if __name__ == "__main__":
192+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)