fix(tests): three false-negative guardrail failures from the 2026-09-10 nightlies - #3202
fix(tests): three false-negative guardrail failures from the 2026-09-10 nightlies#3202apetraru-uipath wants to merge 4 commits into
Conversation
|
Claude finished @apetraru-uipath's task in 1m 12s —— View job PR Review: fix(uipath-review): read the newest review-history entry, not the oldest
SummaryFixes Change-by-Change Review1.
|
| Area | Status | Notes |
|---|---|---|
| Frontmatter | N/A | No skill frontmatter changed |
| E2E Tests | OK | Checker unit tests updated with correct ordering model + new reverse-ordering guard |
| Skill Body | N/A | No SKILL.md changed |
| References & Assets | N/A | No references changed |
| Repo Hygiene | OK | Changes scoped to the two affected files; no secrets, no cross-skill deps |
Issues for Manual Review
None found.
Conclusion
Clean, well-motivated fix. The bug analysis is thorough (real failure, replay validation, tamper check). The code change is minimal — one index flip and consistent messaging. The test corrections are sound: the renamed test now matches the CLI's actual ordering, and the new companion test prevents a different failure mode. LGTM.
rockymadden
left a comment
There was a problem hiding this comment.
What
Three unrelated false negatives from the 2026-09-10 guardrail nightlies, each landing in a different layer.
- 🟢
check_review_history.pyread the oldest entry.history[-1]→history[0], plus docstring and message wording. I confirmed the contract two ways:uip agent review-history add --helpsays "Keeps the newest 25 entries, most recent first", and recordingCthenDagainst a scratch project writes[D, C]. The fix is right, and the old test encoded the wrong model ([older, ENTRY]), which is exactly why it shipped green. - 🟢 Coded BYOG tasks depended on a tenant fixture. A shared
uipshim now serves both discovery verbs. The premise holds:byo-configurations createprobes the connection server-side with no skip flag (skills/uipath-platform/references/guardrails/byo-configurations.md:71), so per-run seeding is out. Serving the second verb is not gold-plating: coded Rule 18 (guardrails.md:490) tells the agent to cross-checkEnabled/ValidConnectionthere before wiring. The mock's payload matches the documented response shape field for field, and the grading claim checks out, both checkers are pure AST overgraph.py. - 🟢
byog-deleterubric asked for more than the prompt. The prompt says "tell me what happens to any agent still pointing at it", and the 1.0 condition demanded remediation on top. Dropping the conjunct is correct.
The PR description is accurate everywhere I could check it. No AI slop.
Overall findings
🔴 major, tests/tasks/uipath-review/ has no CI job, so the new guard never runs
test-helpers.yml runs pytest for uipath-maestro-bpmn, uipath-maestro-flow, uipath-maestro-case, uipath-agents, uipath-planner and uipath-admin/_shared. Not uipath-review. Its three suites (test_check_review_history.py, test_check_review_cli_provenance.py, test_check_guardrail_catalog_evidence.py) run only when someone runs them by hand.
That is the whole failure mode of this bug: #3123 shipped a checker whose test encoded the wrong ordering model, and nothing caught it. test_fails_when_the_newest_entry_is_the_one_that_disagrees is the guard against the next inversion, and today it is a file nobody executes. The "42 passed" in the description is a local result that will not re-run.
Pre-existing gap, not caused by this PR, but this PR is the one that now depends on it. Fix, mirroring the sibling jobs:
pytest-review-check:
runs-on: uipath-ubuntu-latest
name: uipath-review checker unit tests
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.13'
- name: Install pytest
run: pip install pytest
- name: Run pytest
run: pytest tests/tasks/uipath-review/ -vAdd the matching row to docs/REQUIRED-CHECKS.md:158, or required-check contract guard fails.
🟡 minor, the rubric defect is a class, and two siblings still carry it
The reasoning in commit 3 ("a binary rubric paired with a 1.0 threshold is only sound if the judge cannot return an intermediate value") condemns two more criteria in the same directory:
tests/tasks/uipath-platform/guardrails/create_configuration_smoke.yaml:84tests/tasks/uipath-platform/guardrails/probe_abort_smoke.yaml:83
Both are prose "Score 1.0 if / Score 0.0 if" with pass_threshold: 1.0 and no scale statement, so both can return the same 0.9 that failed byog-delete. Either add the scale line to all three now or file the follow-up, but do not leave the next nightly to find them one at a time.
🟡 minor, validated on a driver the nightlies do not use
Commit 2 was validated with --driver tempdir (tests/experiments/default.yaml:13). The nightlies that failed run driver: docker on skills-image (tests/experiments/nightly.yaml:13). The PATH-prepend mechanism is byte-identical to byog_pinning's, which already runs under docker, so I expect it carries. I could not verify it, and neither could the author.
What I confirmed vs reasoned
- Confirmed against the installed CLI: review-history ordering,
byo-configurations listfield names. - Confirmed by reading: both BYOG checkers touch no tenant,
byog_pinning's shim serves only one verb, CI job list, the three judge rubrics. - Reasoned, not confirmed: shim behavior under the docker driver, the "42 passed" count (tests read, not run), and every claim sourced from run artifacts
2026-09-10_04-17-44/04-18-49, which I cannot reach.
Recommendation
✅ Approve. All three fixes are correct, each traced to evidence I could reproduce or read, and scoped tightly. Every finding is additive: one CI job the repo has been missing since before this PR, two sibling rubrics with the same latent defect, and two shim cleanups. None of them argue for holding the merge, and holding it keeps three known-broken tasks in the nightlies.
tl;dr: Three real fixes, all correct, evidence checks out. The catch: tests/tasks/uipath-review/ runs in no CI job, so the regression test this PR just wrote will never execute. Add the pytest job. Two sibling judge rubrics have the same 0.9 defect and are still live. Approve and follow up.
| Return exactly 1.0 or 0.0. This rubric has no partial credit, and the | ||
| criterion's pass_threshold is 1.0 — an intermediate score fails the task. |
There was a problem hiding this comment.
🟡 minor — the second sentence hands the judge a reason to round up.
Return exactly 1.0 or 0.0. This rubric has no partial credit. does the job. Appending "the criterion's pass_threshold is 1.0, an intermediate score fails the task" tells the judge the consequence of its score, and a judge that is genuinely 50/50 now has one option that "fails the task" and one that does not. This PR exists to kill a false negative; that clause trades it for a false positive.
The repo already has the better wording, used in three uipath-admin audit rubrics (audit_login_history_e2e.yaml:82, audit_who_did_x_e2e.yaml:73, audit_scope_ambiguity_smoke.yaml:64). It states the scale and gives a tiebreak procedure instead of a consequence:
Score EXACTLY 1.0 or EXACTLY 0.0 — there is no partial credit and no
intermediate value. If the case feels borderline, decide which of the two
descriptions above fits better and return that score.Suggest swapping to that, both for the neutrality and because a fourth spelling of the same instruction is one more thing to keep in sync.
| # Discovery is mocked, so the task no longer depends on a fixture living on the | ||
| # shared smoke tenant. See _fixtures/ByogMockCli/mocks/uip for why that | ||
| # dependency was untenable and what the shim serves. | ||
| mock_path_dirs: [mocks] |
There was a problem hiding this comment.
🟡 minor — nothing grades the discovery this mock exists to serve.
The low-code sibling pairs its shim with a criterion (byog_pinning.yaml:44, weight 2.0):
- type: command_executed
description: "Agent discovered the BYO entry via uip agent guardrails list (--byo or full list)"
tool_name: "Bash"
command_pattern: 'uip\s+agent\s+guardrails\s+list'
min_count: 1Here there is no such criterion, and the prompt already names byog-smoke-agent-pin, so check_byog_middleware.py passes whether the agent discovered the configuration or copied the string out of the prompt. Two consequences:
- The mock is ungraded infrastructure. If it stops being installed, and the description itself documents one way that happens (the runbook's
-D 'sandbox.mock_path_dirs=["."]'clobber), nothing reports it. The task just drifts back to depending on the agent's mood about verifying. That is the same shape as thebyog_decoratorcarry-forward this PR is fixing: an environment fault wearing a skill result. - Coded Rule 18 goes untested.
guardrails.md:490says to readByoValidatorNamefrom discovery and never from memory, and step 2 says cross-checkEnabled/ValidConnection. The shim now serves both verbs specifically so the agent can do that. Nothing checks that it did.
Suggest adding the byog_pinning criterion to both this task and byog_decorator.yaml, matching on either verb.
|
|
||
| # A help request is a question about flags, not about tenant data. | ||
| if "--help" not in args and "-h" not in args: | ||
| if literal[:3] == ["agent", "guardrails", "list"]: |
There was a problem hiding this comment.
🟡 minor — second copy of this payload, and the twin is now the weaker one.
tests/tasks/uipath-agents/lowcode/guardrails/byog_pinning/mock_template/mocks/uip carries the same BYO_ENTRY and BUILTIN_ENTRY, the same GUIDs, the same validator name. This file adds two things it does not have:
guardrails byo-configurations list, so a low-code agent that follows theByoConfigurationIdcross-reference atlowcode/.../guardrails.md:642still falls through to the real CLI and getsData: [], the exact fixture-outage failure this PR is fixing on the coded half.- the
--helppassthrough, sobyog_pinningstill answers a flag question with a canned payload.
Two copies that already disagree will keep disagreeing, and a CLI field rename now needs both edited. Suggest pointing byog_pinning.yaml at this fixture and deleting its local copy (it already reaches across with ../_fixtures/..., though I have not verified the harness accepts a path that climbs out of lowcode/), or hoisting the shim to a shared tests/tasks/uipath-agents/_fixtures/.
Not blocking, and either way this file is the better of the two.
`check_review_history.py` took `history[-1]`, and its docstring claimed the CLI
"appends". The CLI's own help says the opposite:
uip agent review-history add -- "Keeps the newest 25 entries, most recent first."
So `history[0]` is the newest and `history[-1]` is the oldest. With a single
entry the two coincide, which is why this shipped green; it bites the moment a
reviewer records twice.
Run 2026-09-10_04-18-49, `skill-review-agents-lowcode-guardrail-pii-missing`:
the reviewer recorded a provisional grade C, finished the judgment analysis,
then recorded the corrected final grade D. The file the CLI wrote was
[ {"grade": "D", "runAt": "...04:36:37Z"}, <- newest, matches the report
{"grade": "C", "runAt": "...04:29:48Z"} ] <- oldest
and the checker compared the report's final grade D against the C, failing a
correct artifact. Verified against that exact artifact: FAIL before, PASS after,
and a newest entry that genuinely disagrees still FAILs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`byog_middleware` and `byog_decorator` asked the agent to pin a bring-your-own
guardrail named `byog-smoke-agent-pin`, and relied on that configuration existing
on the shared smoke tenant. Neither task has a `pre_run` that creates it; the
descriptions simply asserted "the tenant already has a BYOG configuration
registered (admin-side)".
The fixture drifted away and the tasks became a test of tenant state:
2026-08-24 tenant held byog-smoke-pii, byog-harmful-content,
cli-harmful-content-1 -- none named byog-smoke-agent-pin
2026-09-10 both discovery calls returned Data: []
uip agent guardrails list --byo -> {"Code":"GuardrailDefinitionsList","Data":[]}
uip guardrails byo-configurations list -> {"Code":"ByoGuardrailConfigurationsList","Data":[]}
The agent then correctly refused to fabricate a validator name and changed
nothing, scoring 0.00 (run 2026-09-10_04-17-44) -- a fixture outage rendered as
a skill regression. `byog_decorator` carried the same defect, hidden because it
was skipped that run and carried forward as a green 1.00.
Seeding per-run is not an option: `byo-configurations create` probes the
connection server-side with no skip flag, and the smoke tenant has no
guardrail-capable connection. That is why the low-code sibling `byog_pinning`
already mocks discovery; this applies the same treatment to the coded half of
the family, from one shared shim.
The shim serves BOTH verbs a coded agent walks -- `agent guardrails list [--byo]`
and `guardrails byo-configurations list` -- where byog_pinning's serves only the
first. Verified necessary: the decorator run hits each exactly once. Without
`--byo`, `list` also returns the built-in pii_detection entry, so the agent must
still disambiguate via IsByo/ByoValidatorName. `--help` and every other verb go
to the real CLI.
Grading is untouched: both checkers are pure AST analysis of graph.py and never
contacted a tenant. Only discovery needed one.
Validated locally with codex + gpt-5.6-terra (the harness that failed),
`--driver tempdir`: byog_middleware SUCCESS 1.000, byog_decorator SUCCESS 1.000.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `llm_judge` criterion required the response to convey two things for 1.0:
that referencing guardrails will break, AND that they need updating or
repointing. The prompt only asks the first — "Remove it, and tell me what
happens to any agent still pointing at it." Nothing asks what to do about them.
Run 2026-09-10_04-17-44 hit exactly that gap. The agent answered precisely:
Any agent still referencing `ByoValidator("byog-legacy-harmful")` (or its
low-code equivalent) will fail its guardrail check at runtime because the
validator name no longer resolves.
The 0.0 condition plainly did not apply, and the 1.0 condition was not fully
met, so the judge returned 0.9 — a value the rubric never defines. Against
`pass_threshold: 1.0` that failed the task at a weighted 0.984, with the other
four criteria all at 1.0.
Two changes: drop the remediation conjunct so the criterion grades what the user
asked (naming a fix stays a bonus, never a deduction), and state the scale
explicitly. A binary rubric paired with a 1.0 threshold is only sound if the
judge cannot return an intermediate value.
Not exercised locally: `experiments/default.yaml` routes the judge through
LiteLLM with `api_base: CODEX_BASE_URL` / `api_key: CODEX_API_KEY`, so every
judge-bearing task ERRORs here for want of those two vars, in either harness.
`coder-eval plan` validates the config; the rubric text is otherwise unverified
until CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the three fixes in this PR, from @rockymadden's review. `tests/tasks/uipath-review/` ran in no CI job (major). test-helpers.yml has pytest jobs for maestro-bpmn, maestro-flow, maestro-case, agents, planner and admin/_shared -- not review. So its three suites, including the ordering guard this PR just wrote, only ever ran by hand. That is the same failure mode as the bug itself: #3123 shipped a checker whose test encoded the wrong ordering model and nothing caught it. Adds `pytest-review-check` mirroring the sibling jobs, plus the `docs/REQUIRED-CHECKS.md` row the contract guard requires. The rubric scale sentence handed the judge a reason to round up. "the criterion's pass_threshold is 1.0 -- an intermediate score fails the task" tells the judge the CONSEQUENCE of its score, so a genuinely borderline judge has one option that "fails the task" and one that does not -- trading this PR's false negative for a false positive. Replaced with the wording three uipath-admin audit rubrics already use, which states the scale and gives a tiebreak procedure instead: "Score EXACTLY 1.0 or EXACTLY 0.0 ... If the case feels borderline, decide which of the two descriptions above fits better." The same latent defect was live in two siblings: create_configuration_smoke and probe_abort_smoke are both prose "Score 1.0 if / Score 0.0 if" with `pass_threshold: 1.0` and no scale statement, so both could return the same 0.9 that failed byog-delete. Both now carry the scale line. Nothing graded the discovery the BYOG mock exists to serve. Both checkers are pure AST over graph.py and the prompt already names `byog-smoke-agent-pin`, so they passed whether the agent discovered the configuration or copied the string out of the prompt -- leaving the mock as ungraded infrastructure, and coded Rule 18 (read ByoValidatorName from discovery, never from memory) untested. Adds the `byog_pinning`-style `command_executed` criterion to byog_middleware and byog_decorator, matching either discovery verb. The shim was a second copy of byog_pinning's payload, and the twin was the weaker one -- it serves only `agent guardrails list`, so a low-code agent following the ByoConfigurationId cross-reference fell through to the real CLI and got `Data: []`, the exact outage this PR fixes on the coded half; it also answers `--help` with a canned payload. Hoisted to `tests/tasks/uipath-agents/_fixtures/ByogMockCli` and pointed all three tasks at it, deleting byog_pinning's local copy. One payload, so a CLI field rename is a one-file edit. Validation: - byog_pinning SUCCESS 1.000 on the shared fixture, all four criteria including its own `uip agent guardrails list` discovery criterion. - Shim verified inside `skills-image:latest` under a PATH prepend: resolves to the shim, both verbs answer, passthrough to the real uip works. A full docker-driver task run is still unverified -- the local image bakes coder_eval 0.8.8 and rejects a 0.12.0 task config -- so the reviewer's open item is closed at the mechanism, not end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88d4021 to
2add28f
Compare
|
Thanks — all findings addressed, and rebased onto 🔴 🟡 The scale sentence handed the judge a reason to round up. This was the sharpest catch in the review. Naming the consequence gives a borderline judge one option that "fails the task" and one that does not — trading this PR's false negative for a false positive. Swapped to the wording the three uipath-admin audit rubrics already use, which gives a tiebreak procedure instead. A fourth spelling would also have been one more thing to keep in sync. 🟡 Two siblings carried the same defect. 🟡 Nothing graded the discovery the mock exists to serve. Correct, and the Rule 18 point is the part that convinced me — the shim serves both verbs specifically so the agent can cross-check 🟡 Second copy of the payload, and the twin was weaker. Hoisted to 🟡 Validated on a driver the nightlies do not use. Partially closed. I verified the shim inside A full docker-driver task run is still unverified: the local One consequence worth flagging: because |
Three unrelated fixes, each traced to a task that scored a false negative in the 2026-09-10 guardrail nightlies. In all three the agent behaved correctly and the harness or the fixture was wrong.
skill-review-agents-lowcode-guardrail-pii-missingskill-agent-guardrail-coded-byog-middlewareskill-platform-guardrails-byog-delete1.
check_review_history.pyread the oldest entryIt graded
history[-1], and its docstring claimed the CLI "appends". The CLI's own help says the opposite:With a single entry the two coincide, which is why this shipped green in #3123. It bites the moment a reviewer records twice. In run
2026-09-10_04-18-49the reviewer recorded a provisional C, finished its analysis, then recorded the corrected D — reasoning about the ordering explicitly:[ {"grade": "D", "runAt": "...04:36:37Z"}, // newest — matches the report {"grade": "C", "runAt": "...04:29:48Z"} ] // oldest — supersededThe report's final grade is D. The checker compared it against the C.
Validated by replaying that exact artifact: pre-fix
FAIL: recorded grade C does not match … D; post-fixPASSon grade D; with the newest entry tampered toB, stillFAIL.tests/tasks/uipath-review/_shared/→ 42 passed.test_last_entry_wins_when_history_has_multiple_entriesencoded the wrong model (it wrote[older, newest]). Corrected, renamed, and paired with a case asserting the reverse ordering still fails — so readinghistory[0]cannot degrade into "find some entry that matches".2. Coded BYOG tasks depended on a tenant fixture that is gone
byog_middlewareandbyog_decoratorask the agent to pinbyog-smoke-agent-pinand relied on it existing on the shared smoke tenant. Neither has apre_runcreating it.The agent correctly refused to fabricate a validator name and changed nothing → 0.00.
byog_decoratorcarries the identical defect, hidden because it was skipped that run and carried forward as a green 1.00.Seeding per-run is not available:
byo-configurations createprobes the connection server-side with no skip flag, and the tenant has no guardrail-capable connection. That is why the low-code siblingbyog_pinningalready mocks discovery. This applies the same treatment to the coded half, from one shared shim under_fixtures/ByogMockCli.The shim serves both verbs a coded agent walks —
agent guardrails list [--byo]andguardrails byo-configurations list— wherebyog_pinning's serves only the first. Verified necessary: the decorator run hits each exactly once. Without--byo,liststill returns the built-inpii_detectionentry so the agent must disambiguate viaIsByo/ByoValidatorName.--helpand every other verb go to the real CLI.Grading is untouched — both checkers are pure AST analysis of
graph.pyand never contacted a tenant. Only discovery needed one.Validated with codex + gpt-5.6-terra (the harness that failed),
--driver tempdir:byog_middlewareSUCCESS 1.000,byog_decoratorSUCCESS 1.000.3.
byog-deletejudge rubric asked for more than the promptThe 1.0 condition was a conjunction: convey that referencing guardrails break and that they need updating or repointing. The prompt only asks the first — "Remove it, and tell me what happens to any agent still pointing at it."
The agent answered precisely:
The 0.0 condition plainly did not apply and the 1.0 condition was not fully met, so the judge returned 0.9 — a value the rubric never defines. Against
pass_threshold: 1.0that failed the task at a weighted 0.984, with the other four criteria all at 1.0.Dropped the remediation conjunct so the criterion grades what the user asked (naming a fix stays a bonus, never a deduction), and stated the scale explicitly — a binary rubric paired with a 1.0 threshold is only sound if the judge cannot return an intermediate value.
Not exercised locally:
experiments/default.yamlroutes the judge through LiteLLM withapi_base: CODEX_BASE_URL/api_key: CODEX_API_KEY, so every judge-bearing task ERRORs here for want of those two vars, in either harness.coder-eval planvalidates the config; the rubric text is otherwise unverified until CI.Two things worth raising separately
byog_decoratorreported a green 1.00 while sitting on a fixture that no longer existed. Consider excluding tenant-dependent tasks from carry-forward, or expiring it after N runs.-D 'sandbox.mock_path_dirs=["."]', and that list replaces rather than merges — so it silently disables the shim on any task that sets its ownmock_path_dirs(byog_pinning, the four platform guardrails tasks, and now these two). Local Codex runs need["mocks","."].The unrelated escalation-smoke failure from the same nightlies is fixed harness-side in UiPath/coder_eval#162.
🤖 Generated with Claude Code