Skip to content

Commit 0c28dfe

Browse files
authored
test(uipath-agents): coded anti-pattern + bindings sweep tests (#476)
Anti-pattern smokes — each task seeds a violating project layout and the skill must drive the agent to detect and fix the violation: - skill-agent-coded-antipattern-build-system — pre-seeded pyproject.toml carries [build-system] hatchling. Check verifies the section is removed and [project] survives. - skill-agent-coded-antipattern-module-level-llm — pre-seeded LangGraph main.py constructs UiPathChat() at module level. Check verifies no module-level UiPath* construction remains and the top-level `graph` variable is preserved. - skill-agent-coded-antipattern-wrong-sdk-import — pre-seeded main.py uses `from uipath import UiPath` (raises ImportError). Check verifies the import was switched to `from uipath.platform import UiPath`. Bindings sweep tests (e2e): - skill-agents-bindings-queue-app-index — five binding types in one project (queue, app, index, connection, mcpServer). Asserts one entry per resource with correct keys, value blocks, and metadata. Connection key is bare; queue with no folder_path is also bare (per the bindings reference No-folder edge case). - skill-agents-bindings-asset-subtypes — three asset retrieves annotated str / int / bool. Asserts three asset bindings; per-binding SubType either matches the annotation (stringAsset / integerAsset / booleanAsset) OR is omitted (the bindings reference allows the omit-fallback). Wrong SubType fails — that would mis-place the resource at uipath push time. - skill-agents-bindings-multi-entrypoint — entry-points.json with two entrypoints. Asserts each binding's EntryPointUniqueId references one of the real entrypoints (or no link at all); fabricated UUIDs fail.
1 parent d590ddc commit 0c28dfe

12 files changed

Lines changed: 922 additions & 0 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
task_id: skill-agent-coded-antipattern-build-system
2+
description: >
3+
Negative test for Coded Critical Rule C1 — `pyproject.toml` MUST
4+
NOT carry a `[build-system]` section. The prompt seeds a violating
5+
pyproject (with `hatchling` declared as the build backend), and
6+
the skill must drive the agent to detect the violation and remove
7+
the section before any further work.
8+
tags: [uipath-agents, smoke, coded, lifecycle:edit, feature:antipattern]
9+
max_iterations: 1
10+
11+
agent:
12+
type: claude-code
13+
permission_mode: acceptEdits
14+
allowed_tools: ["Skill", "Bash", "Read", "Write", "Edit", "Glob", "Grep"]
15+
turn_timeout: 1200
16+
17+
sandbox:
18+
driver: tempdir
19+
python: {}
20+
21+
initial_prompt: |
22+
I have an existing UiPath coded agent project under `legacy-port`
23+
that I'm getting ready for deployment. Recreate the project with
24+
these files exactly, then review and fix anything that would
25+
prevent the project from packaging cleanly with `uip codedagent
26+
pack`.
27+
28+
**legacy-port/pyproject.toml**:
29+
```toml
30+
[project]
31+
name = "legacy-port"
32+
version = "0.0.1"
33+
description = "Ported from a legacy Python service"
34+
authors = [{ name = "Test" }]
35+
requires-python = ">=3.11"
36+
dependencies = ["uipath"]
37+
38+
[dependency-groups]
39+
dev = ["uipath-dev"]
40+
41+
[build-system]
42+
requires = ["hatchling>=1.0"]
43+
build-backend = "hatchling.build"
44+
```
45+
46+
**legacy-port/main.py**:
47+
```python
48+
from pydantic import BaseModel
49+
from uipath.tracing import traced
50+
51+
class Input(BaseModel):
52+
message: str
53+
54+
class Output(BaseModel):
55+
echoed: str
56+
57+
@traced()
58+
async def main(input: Input) -> Output:
59+
return Output(echoed=input.message)
60+
```
61+
62+
After recreating those files, review them for any UiPath coded-
63+
agent anti-patterns and fix them in place. Do NOT keep the
64+
pyproject as written if it violates a rule.
65+
66+
Do NOT publish, upload, or deploy. Do NOT pause between planning
67+
and implementation. Complete end-to-end in a single pass.
68+
69+
success_criteria:
70+
- type: file_exists
71+
description: "pyproject.toml exists under legacy-port/"
72+
path: "legacy-port/pyproject.toml"
73+
weight: 1.0
74+
pass_threshold: 1.0
75+
76+
- type: run_command
77+
description: "pyproject.toml has no [build-system] section after the fix"
78+
command: "python3 $TASK_DIR/check_antipattern_build_system.py"
79+
timeout: 30
80+
expected_exit_code: 0
81+
weight: 6.0
82+
pass_threshold: 1.0
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
"""C1 anti-pattern check — `[build-system]` must be gone after the fix."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import re
8+
import sys
9+
from pathlib import Path
10+
11+
ROOT = Path(os.getcwd()) / "legacy-port"
12+
PYPROJECT = ROOT / "pyproject.toml"
13+
14+
15+
def main() -> None:
16+
if not PYPROJECT.is_file():
17+
sys.exit(f"FAIL: missing {PYPROJECT}")
18+
text = PYPROJECT.read_text(encoding="utf-8")
19+
# `[build-system]` must be removed entirely. A line-anchored regex is the
20+
# right precision — substring search would false-flag a comment.
21+
if re.search(r"^\s*\[build-system\]", text, re.M):
22+
sys.exit(
23+
"FAIL: pyproject.toml still has a [build-system] section. "
24+
"Critical Rule C1: UiPath coded agents do not use a build "
25+
"system; remove the section entirely."
26+
)
27+
# Sanity: `[project]` survived the edit.
28+
if not re.search(r"^\s*\[project\]", text, re.M):
29+
sys.exit(
30+
"FAIL: pyproject.toml lost its [project] section while removing "
31+
"[build-system]. Only the build-system block should be removed."
32+
)
33+
print("OK: pyproject.toml has [project] and no [build-system] section")
34+
35+
36+
if __name__ == "__main__":
37+
main()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
task_id: skill-agent-coded-antipattern-module-level-llm
2+
description: >
3+
Negative test for Coded Critical Rule C4 — LLM clients must be
4+
instantiated lazily, never at module level. The prompt seeds a
5+
LangGraph `main.py` with `llm = UiPathChat(...)` at column zero,
6+
and the skill must drive the agent to refactor the construction
7+
inside a node body before `uip codedagent init` is run.
8+
tags: [uipath-agents, smoke, coded, lifecycle:edit, feature:antipattern]
9+
max_iterations: 1
10+
11+
agent:
12+
type: claude-code
13+
permission_mode: acceptEdits
14+
allowed_tools: ["Skill", "Bash", "Read", "Write", "Edit", "Glob", "Grep"]
15+
turn_timeout: 1200
16+
17+
sandbox:
18+
driver: tempdir
19+
python: {}
20+
21+
initial_prompt: |
22+
I'm porting an existing LangGraph agent into UiPath. Recreate the
23+
project under `legacy-classifier` exactly as below, then review
24+
for any UiPath coded-agent anti-patterns and fix them in place.
25+
26+
**legacy-classifier/pyproject.toml**:
27+
```toml
28+
[project]
29+
name = "legacy-classifier"
30+
version = "0.0.1"
31+
description = "Ported classifier"
32+
authors = [{ name = "Test" }]
33+
requires-python = ">=3.11"
34+
dependencies = ["uipath", "uipath-langchain"]
35+
36+
[dependency-groups]
37+
dev = ["uipath-dev"]
38+
```
39+
40+
**legacy-classifier/main.py**:
41+
```python
42+
from langgraph.graph import StateGraph, START, END
43+
from langgraph.types import Command
44+
from uipath_langchain.chat.models import UiPathChat
45+
from pydantic import BaseModel
46+
from typing import TypedDict
47+
48+
llm = UiPathChat(model="gpt-4o-mini-2024-07-18", temperature=0)
49+
50+
class GraphInput(BaseModel):
51+
text: str
52+
53+
class GraphOutput(BaseModel):
54+
category: str
55+
text: str
56+
57+
class GraphState(TypedDict):
58+
text: str
59+
category: str | None
60+
61+
async def classify(state):
62+
result = await llm.ainvoke(f"Classify: {state['text']}")
63+
return Command(update={"category": str(result), "text": state["text"]})
64+
65+
builder = StateGraph(GraphState, input=GraphInput, output=GraphOutput)
66+
builder.add_node("classify", classify)
67+
builder.add_edge(START, "classify")
68+
builder.add_edge("classify", END)
69+
graph = builder.compile()
70+
```
71+
72+
**legacy-classifier/langgraph.json**:
73+
```json
74+
{"graphs": {"agent": "./main.py:graph"}}
75+
```
76+
77+
After recreating those files, review for anti-patterns and fix
78+
them in place. The graph must still compile and `graph` must
79+
remain exported.
80+
81+
Do NOT publish, upload, or deploy. Do NOT pause between planning
82+
and implementation. Complete end-to-end in a single pass.
83+
84+
success_criteria:
85+
- type: file_exists
86+
description: "main.py exists under legacy-classifier/"
87+
path: "legacy-classifier/main.py"
88+
weight: 1.0
89+
pass_threshold: 1.0
90+
91+
- type: run_command
92+
description: "No module-level UiPath* construction; graph still exports `graph`"
93+
command: "python3 $TASK_DIR/check_antipattern_module_level_llm.py"
94+
timeout: 30
95+
expected_exit_code: 0
96+
weight: 6.0
97+
pass_threshold: 1.0
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python3
2+
"""C4 anti-pattern check — module-level UiPath* must be gone after the fix."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import re
8+
import sys
9+
from pathlib import Path
10+
11+
ROOT = Path(os.getcwd()) / "legacy-classifier"
12+
MAIN = ROOT / "main.py"
13+
14+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15+
from _shared.ast_lazy_init_check import find_module_level_llm_clients # noqa: E402
16+
17+
18+
def main() -> None:
19+
if not MAIN.is_file():
20+
sys.exit(f"FAIL: missing {MAIN}")
21+
violations = find_module_level_llm_clients(MAIN)
22+
if violations:
23+
sys.exit(
24+
"FAIL: main.py still has module-level UiPath* construction — "
25+
"Critical Rule C4 violation. Move the LLM client into a node body. "
26+
+ " | ".join(violations)
27+
)
28+
print("OK: main.py has no module-level UiPath* construction")
29+
text = MAIN.read_text(encoding="utf-8")
30+
# The graph variable must survive the refactor — it's what
31+
# `uip codedagent init` looks for via langgraph.json.
32+
if not re.search(r"^\s*graph\s*=\s*", text, re.M):
33+
sys.exit(
34+
"FAIL: main.py no longer exports a top-level `graph =` variable. "
35+
"Refactor must preserve the compiled graph export."
36+
)
37+
print("OK: top-level `graph` variable still exported")
38+
39+
40+
if __name__ == "__main__":
41+
main()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
task_id: skill-agent-coded-antipattern-wrong-sdk-import
2+
description: >
3+
Negative test for Coded Critical Rule C4 (correct SDK import path)
4+
— `from uipath import UiPath` is wrong and raises `ImportError`
5+
at module-import time. The correct path is `from uipath.platform
6+
import UiPath`. The skill must drive the agent to fix the import.
7+
tags: [uipath-agents, smoke, coded, lifecycle:edit, feature:antipattern]
8+
max_iterations: 1
9+
10+
agent:
11+
type: claude-code
12+
permission_mode: acceptEdits
13+
allowed_tools: ["Skill", "Bash", "Read", "Write", "Edit", "Glob", "Grep"]
14+
turn_timeout: 1200
15+
16+
sandbox:
17+
driver: tempdir
18+
python: {}
19+
20+
initial_prompt: |
21+
I have an existing UiPath coded agent that I'm trying to run.
22+
Recreate the project under `bad-import` exactly as below, then
23+
review for any UiPath coded-agent anti-patterns and fix them in
24+
place.
25+
26+
**bad-import/pyproject.toml**:
27+
```toml
28+
[project]
29+
name = "bad-import"
30+
version = "0.0.1"
31+
description = "Has the wrong SDK import path"
32+
authors = [{ name = "Test" }]
33+
requires-python = ">=3.11"
34+
dependencies = ["uipath"]
35+
36+
[dependency-groups]
37+
dev = ["uipath-dev"]
38+
```
39+
40+
**bad-import/main.py**:
41+
```python
42+
from uipath import UiPath # this import path does not exist
43+
from pydantic import BaseModel
44+
45+
class Input(BaseModel):
46+
asset_name: str
47+
48+
class Output(BaseModel):
49+
value: str
50+
51+
async def main(input: Input) -> Output:
52+
sdk = UiPath()
53+
asset = await sdk.assets.retrieve_async(input.asset_name, folder_path="Shared")
54+
return Output(value=str(asset))
55+
```
56+
57+
After recreating those files, review for anti-patterns and fix
58+
them in place. The agent should still call `sdk.assets.retrieve_
59+
async` with the same arguments.
60+
61+
Do NOT publish, upload, or deploy. Do NOT pause between planning
62+
and implementation. Complete end-to-end in a single pass.
63+
64+
success_criteria:
65+
- type: file_exists
66+
description: "main.py exists under bad-import/"
67+
path: "bad-import/main.py"
68+
weight: 1.0
69+
pass_threshold: 1.0
70+
71+
- type: run_command
72+
description: "main.py imports UiPath from uipath.platform; no `from uipath import UiPath`"
73+
command: "python3 $TASK_DIR/check_antipattern_wrong_sdk_import.py"
74+
timeout: 30
75+
expected_exit_code: 0
76+
weight: 6.0
77+
pass_threshold: 1.0
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
"""C4 (correct SDK import path) anti-pattern check.
3+
4+
Asserts the wrong import has been removed and the correct one is in
5+
place. Importantly, both regexes are line-anchored and require the
6+
imported name to be exactly `UiPath` so a member-list import like
7+
`from uipath.platform import UiPath, foo` still passes while
8+
`from uipath import UiPath` is rejected.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import os
14+
import re
15+
import sys
16+
from pathlib import Path
17+
18+
ROOT = Path(os.getcwd()) / "bad-import"
19+
MAIN = ROOT / "main.py"
20+
21+
WRONG = re.compile(r"^\s*from\s+uipath\s+import\s+(?:[^,\n]*,\s*)*UiPath(?:\s*,|$)", re.M)
22+
RIGHT = re.compile(r"^\s*from\s+uipath\.platform\s+import\s+(?:[^,\n]*,\s*)*UiPath\b", re.M)
23+
24+
25+
def main() -> None:
26+
if not MAIN.is_file():
27+
sys.exit(f"FAIL: missing {MAIN}")
28+
text = MAIN.read_text(encoding="utf-8")
29+
if WRONG.search(text):
30+
sys.exit(
31+
"FAIL: main.py still has `from uipath import UiPath`. "
32+
"Critical Rule C4: the correct import is `from uipath.platform "
33+
"import UiPath`."
34+
)
35+
if not RIGHT.search(text):
36+
sys.exit(
37+
"FAIL: main.py does not import UiPath from `uipath.platform`. "
38+
"Add `from uipath.platform import UiPath`."
39+
)
40+
print("OK: main.py imports UiPath from `uipath.platform` (no `from uipath import UiPath`)")
41+
42+
43+
if __name__ == "__main__":
44+
main()

0 commit comments

Comments
 (0)