Skip to content

fix(tests): make the CLI-verb linter see leaf verbs and negative criteria - #2546

Open
Cellcote wants to merge 1 commit into
mainfrom
fix/cli-verb-leaf-linting
Open

fix(tests): make the CLI-verb linter see leaf verbs and negative criteria#2546
Cellcote wants to merge 1 commit into
mainfrom
fix/cli-verb-leaf-linting

Conversation

@Cellcote

Copy link
Copy Markdown
Contributor

Split out of #2544 (part 2 of 2). Companion to #2545, which fixes the task this
change catches. Merge #2545 first — otherwise this PR's first repo-wide run
reports a High on uipath-process-mining, which is the real bug, not a
regression from this change.

Why

A process-mining task graded on uip pm apps model add-table — a verb that does
not exist (the CLI has uip pm apps data-model add-table; apps model is the
semantic model, with fields / get / update and no add-table child). It
shipped, and it took a nightly score from 1.0 to 0.778, because
check-cli-verbs.py reported the whole set clean. Two defects combined to hide it.

1. \b discarded the leaf verb

On AT_BOUNDARY the walker bailed and called _trim_to_word_boundary,
dropping the token it had just accumulated. But \b after a word character
confirms the token ended — it is the opposite of a partial match.

Since uip <group> <leaf>\b is this repo's standard pattern shape, the checker
was only ever validating the command group. uip\s+pm\s+apps\s+model\s+add-table\b.*
extracted as just pm apps model.

\B keeps the old behaviour: it asserts the token continues, so what was
accumulated really is mid-token.

2. Longest-prefix matching swallowed bogus subcommands

best_match then fell back to pm apps model, a real catalog group, and
reported reachable. Shortening is now refused when the leftover token sits in
subcommand position under a group whose children the catalog knows.

3. Negative criteria were never verb-checked

iter_command_patterns only yielded command_executed. A negative whose verb
does not exist can never fire, so it awards its weight on every run — a dead
assertion that inflates scores. command_executed with max_count: 0 is the
other negative idiom in this repo and gets the same handling.

Negatives report Medium, not High: they inflate scores rather than breaking
the run, and the guard is sometimes deliberate (blocking a verb an agent might
invent).

Guarding against false positives

The prefix-shortening rule is deliberately narrow. Each of these is covered by a
test:

  • Positional arguments after a leaf verbis resources run list <connector> <activity>. is resources run list has no children, so the trailing tokens are
    arguments, not subcommands.
  • Partial kebab tokenscreate-, left by a pattern matching create-raw /
    create-resource. A trailing - means the walker stopped mid-token, so we
    cannot claim the verb is missing.
  • unwalkable_groups — the catalog builder could not enumerate them, so
    absence from the catalog carries no information there.

Also

Pinned encoding="utf-8" on the file reads. The script crashed with
UnicodeDecodeError on Windows under cp1252 when scanning task YAMLs containing
em-dashes.

Verification

  • pytest tests/scripts/test_verb_checkers.py21 passed (14 existing + 7 new).
    The pre-existing test_extract_verb_paths_treats_word_boundary_as_dynamic still
    passes: a \b reached before any complete token is still treated as dynamic.
  • Repo-wide across all task YAMLs, on the current unfixed tree: 1 High, 2
    Medium, 124 Info. The High is exactly the process-mining bug this was written to
    catch; with fix(process-mining): grade the real add-table verb and fix required tags #2545 merged it goes to 0.
  • No CI gate changes behaviour — nothing in .github/ invokes this script.
    /audit-verbs uses --report (exit 0 on Medium, per the contract in
    audit-verbs.md) and /lint-task merges --json findings.

The 2 Medium findings are both in
tests/tasks/uipath-governance/aops-policy/deployment_configure_smoke.yaml
negatives against deployment user add / add-user, which don't exist (the real
verb is configure, which auto-registers). Plausibly intentional guards; left
alone as out of scope, but worth a look from that skill's owner since they
currently award their weight unconditionally.

🤖 Generated with Claude Code

…eria

`scripts/check-cli-verbs.py` reported clean on a task criterion demanding
`uip pm apps model add-table` — a verb that does not exist (the CLI has
`uip pm apps data-model add-table`). Two defects combined to hide it:

- `enumerate_paths` treated `\b` as a partial match and trimmed the token it had
  just accumulated. A `\b` following a word character *confirms* the token ended,
  so this dropped the leaf verb from every `uip <group> <leaf>\b` pattern — the
  repo's standard shape — leaving only the command group verified.
- `classify.best_match` then fell back to the parent group `pm apps model`, a
  real catalog entry, and reported reachable.

Shortening to a shorter prefix is now refused when the leftover token sits in
subcommand position under a group whose children the catalog knows. False
positives are guarded three ways, each covered by a test: positional arguments
after a leaf verb, partial kebab tokens (`create-` from a pattern matching
`create-raw` / `create-resource`), and anything under an `unwalkable_groups`
entry, whose children are unknown.

Also scan `command_not_executed` (and `command_executed` bounded to
`max_count: 0` — the other negative idiom here), which were skipped entirely. A
negative whose verb does not exist can never fire, so it awards its weight
unconditionally. These report Medium, not High: they inflate scores rather than
breaking the run, and the guard is sometimes deliberate.

Pin `encoding="utf-8"` on the file reads — the script crashed on Windows under
cp1252.

Against the current tree this yields 1 High (the process-mining task above,
fixed separately) and 2 Medium (advisory, in uipath-governance).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Cellcote's task in 2m 42s —— View job


PR Review: fix(tests): make the CLI-verb linter see leaf verbs and negative criteria

  • Read review criteria and project conventions
  • Read full diff and changed files
  • Check existing PR conversation
  • Analyze correctness of the \b vs \B fix
  • Analyze the prefix-shortening guard
  • Analyze negative criteria handling
  • Review test coverage
  • Post review findings

Summary

Fixes three interacting defects in scripts/check-cli-verbs.py that combined to let a bogus CLI verb (uip pm apps model add-table) pass the linter undetected: (1) \b was treated as dynamic and discarded the leaf verb, (2) longest-prefix matching fell back to the parent group, and (3) command_not_executed criteria were never checked at all. Adds 7 new tests covering the fixes and their false-positive guard rails. Also pins encoding="utf-8" on file reads to fix Windows crashes.

Change-by-Change Review

1. scripts/check-cli-verbs.py\b handling fix (lines 190–205)

Severity: OK

The core fix is correct and well-reasoned. \b after a literal word character (checked via isalnum()) confirms the token boundary — the accumulated path is the complete verb, not a partial. The isalnum() guard is correct for kebab-case verbs too: add-table ends with e, which is alnum. When \b appears after whitespace (e.g. uip\s+\bfoo\b), isalnum() is False, so it falls through to _trim_to_word_boundary — matching the existing test test_extract_verb_paths_treats_word_boundary_as_dynamic. \B keeps the original bail-out behavior.

2. scripts/check-cli-verbs.py — prefix-shortening guard in best_match (lines 277–293)

Severity: OK

The strict mode in best_match is carefully bounded. The subcommand regex [a-z][a-z0-9]*(-[a-z0-9]+)* correctly rejects partial kebab tokens (trailing -). has_children checks only direct children (depth + 1), which is the right level of precision. The under_unwalkable escape hatch correctly checks all prefixes of the candidate. The guard only fires when all three conditions hold: strict=True, leftover token exists, and the candidate is a known group with children — this is exactly the narrow scope described in the PR body.

One observation: the under_unwalkable check tests the candidate (prefix), not the original verb — this is correct since it asks "do we know this group's children?" rather than "is the target itself unwalkable."

3. scripts/check-cli-verbs.pyload_unwalkable() (lines 73–82)

Severity: Low

The function reads CATALOG_PATH a second time (also read in load_catalog). This is a minor inefficiency — the catalog JSON is small so parsing it twice is negligible. A cleaner approach would be returning the unwalkable set from load_catalog, but this is too minor to block.

4. scripts/check-cli-verbs.py — negative criteria handling (lines 319–338, 385–401)

Severity: OK

iter_command_patterns now yields command_not_executed and detects the max_count: 0 idiom. The is_negative flag correctly uses or (either idiom triggers it). The severity downgrade from High to Medium for negatives is well-justified: a dead negative inflates scores rather than breaking the run, and may be a deliberate guard.

5. scripts/check-cli-verbs.pyencoding="utf-8" (lines 69, 81, 94, 346)

Severity: OK

All four read_text() calls now pin UTF-8. This prevents UnicodeDecodeError on Windows under cp1252 when task YAMLs contain em-dashes.

6. tests/scripts/test_verb_checkers.py — 7 new tests (lines 269–370)

Severity: OK

Excellent coverage of the three fixes and their false-positive guards:

  • test_word_boundary_after_complete_token_keeps_leaf — the \b fix (issue 15)
  • test_bogus_leaf_under_real_group_is_unknown — the strict-mode catch (issue 15)
  • test_positional_args_after_leaf_verb_stay_reachable — false-positive guard (leaf has no children)
  • test_partial_kebab_token_is_not_reported_missing — false-positive guard (trailing -)
  • test_unwalkable_group_never_reports_missing_leaf — false-positive guard (unwalkable)
  • test_negative_criteria_are_scanned_and_reported_mediumcommand_not_executed (issue 16)
  • test_zero_bounded_command_executed_counts_as_negativemax_count: 0 idiom (issue 16)

The tests use focused catalogs that isolate each behavior. The negative-criteria test at line 342–343 has monkeypatch stubs for load_catalog/load_renames/load_unwalkable but then passes explicit arguments to lint_file anyway (line 352) — the stubs are harmless but unnecessary for that call. Not worth changing.

What's Missing

Nothing identified. The PR is well-scoped to the linter and its tests. No skill files, CODEOWNERS, or CI workflows are affected.

Area Ratings

Area Status Notes
Frontmatter N/A No skill files changed
E2E Tests N/A Not a skill change — this modifies a linting script and its unit tests
Skill Body N/A No skill files changed
References & Assets N/A No references or assets changed
Repo Hygiene OK No secrets, no cross-skill refs, changes are tightly scoped

Issues for Manual Review

  • The PR description mentions 2 Medium findings in tests/tasks/uipath-governance/aops-policy/deployment_configure_smoke.yaml (negatives against deployment user add / add-user). These are out of scope for this PR but worth the governance skill owner investigating — they currently award weight unconditionally.
  • The merge ordering note is important: merge fix(process-mining): grade the real add-table verb and fix required tags #2545 first, otherwise the first repo-wide run reports a High on uipath-process-mining (the bug this change is designed to catch).

Conclusion

Clean, well-motivated change that fixes a real scoring regression. The three defects are clearly diagnosed, each fix is narrow and well-tested (7 new tests with good false-positive guard coverage), and the existing 14 tests continue to pass. The encoding="utf-8" fix is a good defensive addition. No issues found that would block merge.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant