Skip to content

Commit 603e23c

Browse files
mohsen-uipathanonymous
authored andcommitted
test(early-stop): cover second-review items (two-AgentStart, golden corpus, parity)
Add the still-applicable coverage from the second review; no scoring-logic change (any-engagement was already restored by the prior commit): - Golden-corpus regression test pinning observed_label AND score for the canonical skill_triggered trajectories (single-target, target-then-competitor recall/precision, wrong-then-target, negative false-positive, true-negative, file-read), so a future engagement-policy change fails loudly instead of silently shifting the activation suite's P/R/F1. - Multi-turn + file-read parity tests: any-engagement holds across TurnRecords and for the file-read signal (Read of skills/<name>/...), not only the Claude Skill call, so the agent-agnostic parity is asserted for the new branch. - Two-AgentStart watcher test: a retry's second AgentStart does not reset the wall-clock origin (the documented no-op branch in on_event). Also clarifies the EarlyStopInfo.tool_call_index field doc: on a call-latched stop it is the deciding in-flight call index (completed_tool_ends + 1), read as "which call decided", not a count of fully-completed tool calls.
1 parent 0b093b1 commit 603e23c

3 files changed

Lines changed: 172 additions & 1 deletion

File tree

src/coder_eval/models/results.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,12 @@ class EarlyStopInfo(BaseModel):
420420
description="SDK inner-turn count at the stop (watcher counts TurnStartEvents). NOT the "
421421
+ "orchestrator iteration, which is always 1 in single-shot."
422422
)
423-
tool_call_index: int = Field(description="1-based index of the tool call that decided the stop.")
423+
tool_call_index: int = Field(
424+
description="1-based index of the tool call that decided the stop. NOTE: because the "
425+
+ "stop latches on the tool CALL (ToolStartEvent), this is the index of the deciding "
426+
+ "call INCLUDING that in-flight call — i.e. completed_tool_ends + 1 for a call-latched "
427+
+ "stop. Read it as 'which call decided', not as a count of fully-completed tool calls."
428+
)
424429
elapsed_seconds: float = Field(description="Wall-clock seconds from the first agent-start event to the stop.")
425430
turns_remaining_at_stop: int | None = Field(
426431
default=None,

tests/test_early_stop.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,23 @@ def test_tool_call_index_counts_prior_resolved_calls(self) -> None:
11311131
assert watcher.info is not None
11321132
assert watcher.info.tool_call_index == 2
11331133

1134+
def test_second_agent_start_does_not_reset_origin(self) -> None:
1135+
# The wall-clock origin is stamped at the FIRST AgentStartEvent only; a
1136+
# retry's second AgentStart must NOT reset it (the documented no-op branch
1137+
# in on_event). Exercised deterministically via _started_monotonic rather
1138+
# than the time-based elapsed_seconds field.
1139+
watcher = _watcher([_skill_crit("date-teller", "date-teller", stop_when="pass")])
1140+
_feed(watcher, [_agent_start()])
1141+
origin = watcher._started_monotonic
1142+
assert origin is not None
1143+
# A second AgentStart (as on a retry) must leave the origin untouched.
1144+
_feed(watcher, [_agent_start(), _turn_start()])
1145+
assert watcher._started_monotonic == origin
1146+
# The stop that follows anchors elapsed_seconds to that first origin.
1147+
_feed(watcher, [_skill_start("date-teller")])
1148+
assert watcher.info is not None
1149+
assert watcher.info.elapsed_seconds >= 0.0
1150+
11341151

11351152
# --------------------------------------------------------------------------- #
11361153
# Phase 3: Orchestrator wiring

tests/test_skill_triggered.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,155 @@ def test_stacked_recall_and_precision_on_one_trajectory(self) -> None:
158158
assert precision.observed_label == "yes" and precision.score == 0.0 # precision: off-target engaged
159159

160160

161+
def _check_multi(
162+
*, expected_skill: str, skill_name: str, turns: list[list[CommandTelemetry]]
163+
) -> ClassificationCriterionResult:
164+
criterion = SkillTriggeredCriterion(
165+
description="did agent invoke a skill?",
166+
expected_skill=expected_skill,
167+
skill_name=skill_name,
168+
)
169+
checker = SkillTriggeredChecker()
170+
turn_records = [_turn(cmds) for cmds in turns]
171+
result = checker.check(criterion, sandbox=None, turn_records=turn_records) # type: ignore[arg-type]
172+
assert isinstance(result, ClassificationCriterionResult)
173+
return result
174+
175+
176+
class TestSkillTriggeredGoldenCorpus:
177+
"""Regression lock: pins observed_label AND score for the canonical
178+
multi-skill trajectories through ``_check_impl``. Any future change to the
179+
engagement policy (any- vs first-engagement) breaks these, forcing an
180+
explicit acknowledgement of the methodology break — and a re-score/backfill
181+
of historical activation P/R/F1 — before merge.
182+
"""
183+
184+
@pytest.mark.parametrize(
185+
("case", "expected_skill", "skill_name", "commands", "exp_observed", "exp_score"),
186+
[
187+
(
188+
"single-target-recall",
189+
"uipath-admin",
190+
"uipath-admin",
191+
[_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s1")],
192+
"yes",
193+
1.0,
194+
),
195+
(
196+
"target-first-then-competitor-recall",
197+
"uipath-admin",
198+
"uipath-admin",
199+
[
200+
_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s1"),
201+
_cmd("Skill", {"skill": "uipath-platform"}, tool_id="s2"),
202+
],
203+
"yes",
204+
1.0,
205+
),
206+
(
207+
"target-first-then-competitor-precision",
208+
"uipath-admin",
209+
"uipath-platform",
210+
[
211+
_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s1"),
212+
_cmd("Skill", {"skill": "uipath-platform"}, tool_id="s2"),
213+
],
214+
"yes",
215+
0.0,
216+
),
217+
(
218+
"wrong-first-then-target-recall",
219+
"uipath-admin",
220+
"uipath-admin",
221+
[
222+
_cmd("Skill", {"skill": "uipath-platform"}, tool_id="s1"),
223+
_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s2"),
224+
],
225+
"yes",
226+
1.0,
227+
),
228+
(
229+
"negative-target-engaged-false-positive",
230+
"",
231+
"uipath-admin",
232+
[_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s1")],
233+
"yes",
234+
0.0,
235+
),
236+
(
237+
"negative-no-engagement-true-negative",
238+
"",
239+
"uipath-admin",
240+
[_cmd("Read", {"file_path": "notes.txt"})],
241+
"no",
242+
1.0,
243+
),
244+
(
245+
"file-read-target-recall",
246+
"uipath-admin",
247+
"uipath-admin",
248+
[_cmd("Read", {"file_path": "skills/uipath-admin/SKILL.md"})],
249+
"yes",
250+
1.0,
251+
),
252+
],
253+
)
254+
def test_golden_corpus_scores_are_pinned(
255+
self,
256+
case: str,
257+
expected_skill: str,
258+
skill_name: str,
259+
commands: list[CommandTelemetry],
260+
exp_observed: str,
261+
exp_score: float,
262+
) -> None:
263+
result = _check(expected_skill=expected_skill, skill_name=skill_name, commands=commands)
264+
assert result.observed_label == exp_observed, case
265+
assert result.score == exp_score, case
266+
267+
268+
class TestSkillTriggeredMultiTurnParity:
269+
"""Any-engagement holds across TurnRecords and for the file-read signal, so
270+
the agent-agnostic parity CLAUDE.md stresses is asserted for the new branch.
271+
"""
272+
273+
def test_expected_skill_in_later_turn_still_recalls(self) -> None:
274+
# Distractor engaged in turn 1, expected skill in turn 2. Recall must
275+
# credit the GT criterion regardless of which turn the engagement lives in.
276+
result = _check_multi(
277+
expected_skill="uipath-admin",
278+
skill_name="uipath-admin",
279+
turns=[
280+
[_cmd("Skill", {"skill": "uipath-platform"}, tool_id="s1")],
281+
[_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s2")],
282+
],
283+
)
284+
assert result.observed_label == "yes" and result.score == 1.0
285+
286+
def test_off_target_in_earlier_turn_is_precision_miss(self) -> None:
287+
# The competitor engaged in an EARLIER turn than the GT skill still lands
288+
# as a precision miss on its own criterion (no turn-order dependence).
289+
turns = [
290+
[_cmd("Skill", {"skill": "uipath-platform"}, tool_id="s1")],
291+
[_cmd("Skill", {"skill": "uipath-admin"}, tool_id="s2")],
292+
]
293+
precision = _check_multi(expected_skill="uipath-admin", skill_name="uipath-platform", turns=turns)
294+
assert precision.observed_label == "yes" and precision.score == 0.0
295+
296+
def test_file_read_parity_across_turns(self) -> None:
297+
# Off-Claude parity: the agent READS skill files across turns (platform's
298+
# in turn 1, admin's in turn 2). Recall/precision score identically to the
299+
# Skill-tool path, order- and turn-independent.
300+
turns = [
301+
[_cmd("Read", {"file_path": "skills/uipath-platform/SKILL.md"})],
302+
[_cmd("Read", {"file_path": "skills/uipath-admin/reference.md"})],
303+
]
304+
recall = _check_multi(expected_skill="uipath-admin", skill_name="uipath-admin", turns=turns)
305+
precision = _check_multi(expected_skill="uipath-admin", skill_name="uipath-platform", turns=turns)
306+
assert recall.observed_label == "yes" and recall.score == 1.0
307+
assert precision.observed_label == "yes" and precision.score == 0.0
308+
309+
161310
class TestSkillTriggeredCodex:
162311
"""Codex has no ``Skill`` tool — it engages a skill by reading its files via shell.
163312

0 commit comments

Comments
 (0)