Skip to content

[QA-2464] feat(bitwarden-testing-tools): review existing Testmo cases in assessing-test-coverage - #307

Open
awiester-bw wants to merge 3 commits into
mainfrom
skill-c-testmo-poc
Open

awiester-bw wants to merge 3 commits into
mainfrom
skill-c-testmo-poc

Conversation

@awiester-bw

Copy link
Copy Markdown
Contributor

🎟️ Tracking

QA-2464. Extends assessing-test-coverage in bitwarden-testing-tools for PDLC Stage 3.

📔 Objective

assessing-test-coverage now reviews the existing Testmo cases for a story and recommends update, retire, or keep for each case the change affects, with the AC or plan item behind it. Given a designing-test-plans plan, it reports planned tests that already exist as cases. It also checks each case's Automation Type against bitwarden/test and lists possible duplicates and cases owned by other teams.

Testmo is read only through Testmo's official MCP server in read-only mode, with setup in the README. A new bundled MCP server, bitwarden-test-automation, adds one tool, find_automated_cases, which finds case IDs in bitwarden/test main through the user's gh login. designing-test-plans now hands its saved plan to assessing-test-coverage when the request also asks about existing tests. assessing-test-coverage runs forked and blocks curl, wget, WebFetch, and .env reads while it runs.

🧪 Testing

Trigger evals with run_real_eval.py, 7 runs per query:

  • claude-opus-4-8: 23/28. Should-trigger 9/14, should-not 14/14. Main scores below threshold on three of the same five queries on this model. All five are listed in evals/README.md, and the baseline was re-recorded.
  • claude-opus-5-5: 27/28, with main at 20/20. The one miss is the duplicate-title query.

Headless runs on PM-39988 and PM-41861: story review with Testmo connected, a wrong Testmo key, no Testmo server, a plan-only request, a run given a plan path, and readiness, plan, and coverage from one request. With no Testmo server, the skill says it isn't connected, points to the README, and finishes the code side.

MCP server: 57 unit tests pass. The CI install and launch steps pass locally on Node 22. Node 24 and 26 run in this PR's CI.

Notes:

  • The marketplace validator fails on a bitwarden-delivery-tools version mismatch that is already on main.
  • This skill doesn't use bitwarden-testmo-tools. That plugin writes through an API key, and this skill only reads.
  • Not in this PR: smoke suite churn, and matching assessing-test-coverage's layer names with the plan's (QA-2708).

@awiester-bw awiester-bw added the ai-review Request a Claude code review label Oct 30, 2026
@awiester-bw
awiester-bw requested a review from a team as a code owner October 30, 2026 22:05
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Claude Code validation

Result: Pass

Re-review after the author's fix commits (b9a77c3, 0df9f6f), with the baseline still pinned at the original merge base 8aa784a. Six of the previous fifteen findings are resolved at the current head, nine persist unchanged, and six new ones surfaced in the original feature commit f538d6d — most of them the same shape as the contradictions already confirmed: a step in the new Testmo procedure mandates output that its own layout has no slot for. Nothing CRITICAL and nothing that weakens security; three claims were dropped after checking them against the Claude Code documentation, including one that had alleged the new deny list fails open.

Not covered: Both plugin-dev:skill-reviewer runs executed without a Bash grant and reconstructed the changeset from the CHANGELOG rather than git diff; every finding they returned was re-verified here against git diff 8aa784a...HEAD and the word counts measured directly. Per the re-run rule, the fix commits themselves were not audited for new findings — only the previous findings were re-checked against them.

Findings
  • ⚠️ IMPORTANT: Finding 1: Step J mandates State, Checkboxes, and "No team set", but the Update layout has no slot for any of them and line 231 forbids adding one (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:167)
  • ⚠️ IMPORTANT: Finding 2: find_automated_cases silently skips every *TestBase.cs, so the skill reports those cases as false integrity mismatches (plugins/bitwarden-testing-tools/mcp/bitwarden-test-automation-mcp-server/src/tools/find-automated-cases.ts:40)
  • ⚠️ IMPORTANT: Finding 3: The skill is 4,444 words inline, past the 3,000-word ceiling, still with no references/ directory (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:43) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 4: The description advertises "lists possible duplicates" but carries no duplicate-shaped trigger, and its own eval records 0/7 (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:3) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 5: Step F mandates a history call for every update candidate, but the Update layout has no field to hold the result (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:131) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 6: The new step 11 only fires on the one request shape the description routes to the sibling skill instead (plugins/bitwarden-testing-tools/skills/designing-test-plans/SKILL.md:79) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 7: The HEADS-UP layout omits the folder, why, and retire note that line 234 requires on every entry (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:262)
  • 🎨 SUGGESTED: Finding 8: POSSIBLE DUPLICATES hardcodes two folder labels, so most pairs step I defines cannot be written down (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:274)
  • 🎨 SUGGESTED: Finding 9: The collapse-to-one-line rule can suppress a populated INTEGRITY MISMATCHES table and a NOT CHECKED section (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:228)
  • 🎨 SUGGESTED: Finding 10: Step I must report "both sets of steps" for a duplicate pair, but step C withheld the fetch that produces them (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:165)
  • 🎨 SUGGESTED: Finding 11: Step C's search fan-out has no cap — an unclear client can reach ~66 calls before any other Testmo request (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:80) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 12: The plan-discovery glob fails silently when ${CLAUDE_PLUGIN_DATA} did not expand, unlike the sibling skill that writes there (plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md:35) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 13: Step 11 invokes a sibling skill with no availability check, unlike the two other sub-skill calls in this file (plugins/bitwarden-testing-tools/skills/designing-test-plans/SKILL.md:79) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 14: The saved plan file gets no pointer to the coverage review produced from it (plugins/bitwarden-testing-tools/skills/designing-test-plans/SKILL.md:79) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 15: Test-only fixture code is compiled into the production build that ships to every plugin user (plugins/bitwarden-testing-tools/mcp/bitwarden-test-automation-mcp-server/tsconfig.json:17) (already raised on the pull request)

Findings 1, 2, 7, 8, 9, and 10 are new this round and are posted as inline comments with reasoning and a fix. The nine marked "already raised" keep their existing unresolved threads and were not re-posted.

Dropped after documentation check: three claims from the component checkers, each contradicted by the official documentation.

  • skills/assessing-test-coverage/SKILL.md:6 — a checker called Bash(curl *) / Bash(wget *) invalid syntax that silently never fires, making the new deny list a security control that fails open. Configure permissions contradicts this directly: "The :* suffix is an equivalent way to write a trailing wildcard, so Bash(ls:*) matches the same commands as Bash(ls *)." The same page uses Bash(curl *) as its own worked example, and notes "The permission dialog writes the space-separated form when you select 'Yes, and don't ask again' for a command prefix." The deny list works as written.
  • skills/assessing-test-coverage/SKILL.md:3 — a checker reported the description as 16 characters under a 1024-character cap, so no trigger phrase could be added without failing frontmatter validation. Skills documents no such cap: "the combined description and when_to_use text is truncated at 1,536 characters in the skill listing to reduce context usage." That is listing truncation, not validation, so Finding 4's fix can be applied as written.
  • skills/assessing-test-coverage/SKILL.md:3 — a checker measured the description against a "~500-character guidance". No such limit is documented; the only figure on the Skills page is the 1,536-character combined truncation quoted above.

Two further layout nits were dropped as not worth a reviewer's time: a missing Folder column on the INTEGRITY MISMATCHES table, and a sentence in step B that references the case Team field before step C fetches it. A .cspell.json finding was dropped as out of scope — it is neither Claude material nor in this review's changed-file list.

Resolved since the last review

All six were fixed in 0df9f6f and b9a77c3, verified at the current head:

  • Tool preload (SKILL.md:23) — mcp__plugin_bitwarden-testing-tools_bitwarden-test-automation__find_automated_cases is now in the exact-name select: list, so step G's only tool is loaded before use.
  • Cross-team notifications (SKILL.md:235) — the word "only" is gone; HEADS-UP FOR OTHER TEAMS now admits "one line per other team on a shared full-review case that needs a change", matching what line 123 requires.
  • Step G scope (SKILL.md:149) — settled on the wider set: "Check every full-review case ID from step C's merged results, fetched or not." No longer contradicts line 133.
  • Unrestricted clone grant (SKILL.md:5) — Bash(git clone:*) removed from allowed-tools, so the new disallowed-tools line is no longer undercut by an arbitrary-URL fetch.
  • No-AC-source runs (SKILL.md:40) — step 5 now skips the Testmo procedure and records "no AC source, Testmo cases not reviewed" under NOT CHECKED instead of running it at full cost for nothing.
  • Testmo key in argv (README.md:54-61) — the setup now single-quotes the header so Claude Code stores the literal ${TESTMO_TOKEN} and expands it at launch, keeping the plaintext key out of the process argument list. The accompanying note explains why the quoting matters.

A seventh, unprompted fix corrected a stale "Skill C" reference to "This skill" at SKILL.md:155.

What passed
  • Manifest and version: 1.5.0 consistent across plugin.json, .claude-plugin/marketplace.json:111, and the root README.md catalog row, with a dated CHANGELOG entry. MINOR is the right bump for a bundled server plus a new skill capability. No agents exist, so no AGENT.md bump was owed.
  • Credentials: no hardcoded secrets in any changed file. The only token-shaped string, ghp_secret123 at src/github/archive.spec.ts:70, is a fixture asserting that classifyGhFailure returns a generic reason rather than echoing gh stderr.
  • MCP configuration: .mcp.json is type: stdio, quotes ${CLAUDE_PLUGIN_ROOT}, carries no env block and therefore no secrets, and is byte-for-byte identical to the already-shipped plugins/bitwarden-atlassian-tools/.mcp.json apart from the server name and directory. No contributor-controlled input reaches the shell.
  • Frontmatter: allowed-tools, disallowed-tools, context, agent, model, and background are all documented SKILL.md fields and all used as documented.
  • Dependency CVEs: the Aikido thread is resolved at head — @modelcontextprotocol/sdk 1.32.0 and zod 4.6.6 are pinned, with body-parser 2.4.0 and fast-uri 3.1.9 in the lockfile.
  • Prompt injection (CWE-1427): nothing in the changed files attempts to direct this review. Both skills do the opposite — assessing-test-coverage/SKILL.md:17 instructs treating Jira, Confluence, PR, CSV, and Testmo text as untrusted data, and the skill bans search_cases because it ships case data to a vendor AI.
  • Script checks: validate-plugin-structure.sh, validate-marketplace.sh, and validate-version-bump.sh run as dedicated workflow steps and report their own results there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 Open source vulnerabilities detected - high severity
Aikido detected 15 vulnerabilities across 5 packages, it includes 5 high, 6 medium and 4 low vulnerabilities.

Details

Remediation:

  • @modelcontextprotocol/sdk — 7 CVEs (high) — fixed in 1.32.0
  • body-parser — 1 CVE (high) — fixed in 2.4.0
  • zod — 2 CVEs (high) — fixed in 4.6.6
  • fast-uri — 4 CVEs (medium) — fixed in 3.1.9
  • raw-body — 1 CVE (low) — fixed in 4.0.0

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The new server pins @modelcontextprotocol/sdk 1.31.0 and zod 4.5.4. Those are the vulnerable versions listed above, and the sibling bitwarden-atlassian-mcp-server in this repo already uses the fixed versions.

Details and fix

plugins/bitwarden-atlassian-tools/mcp/bitwarden-atlassian-mcp-server/package.json already pins @modelcontextprotocol/sdk 1.32.0 and zod 4.6.6. Those are the fix versions Aikido reports, and that server also clears the 7-day minimumReleaseAge gate. This package's package.json (lines 25 and 28) copies the older pins:

"@modelcontextprotocol/sdk": "1.32.0",
"zod": "4.6.6"

After that, run pnpm install again to regenerate pnpm-lock.yaml. The sibling's pnpm-workspace.yaml also has overrides for transitive packages pulled in by the SDK. Check whether body-parser, fast-uri, and raw-body need the same treatment here.

This server only uses stdio, so most of the SDK's HTTP-transport CVEs probably can't be reached in practice. Still, the bundled server ships to every plugin user, and the fix is the same version bump the repo has already made elsewhere.

code-review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved at the current head. package.json now pins @modelcontextprotocol/sdk 1.32.0 and zod 4.6.6, and pnpm-lock.yaml resolves body-parser 2.4.0 and fast-uri 3.1.9. Those match the fix versions listed above and the sibling bitwarden-atlassian-mcp-server. raw-body resolves to 3.0.2, the same version the sibling server ships.

code-review

const platform = detectPlatform(path);
const hits: CodeHit[] = [];

lines.forEach((line, index) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The forEach callback combines match collection, attribute analysis, and hit construction, making the scan flow harder to follow as an unnamed function.

Details

✨ AI Reasoning
​The callback iterates over each source line, gathers matches from three patterns, determines attribute state, and creates a hit for each ID. These are substantial pieces of scanning logic inside an unnamed callback, so a clear name or explanatory comment would improve readability.

🔧 How do I fix it?
Extract complex anonymous functions into named functions with descriptive names, or add explanatory comments for their purpose.

Reply on this comment to give feedback or ignore the issue.
More info

const magic = field(header, 257, 6);
const prefix = magic.startsWith("ustar") ? field(header, 345, 155) : "";
const name = field(header, 0, 100);
const path = pendingPath ?? (prefix === "" ? name : `${prefix}/${name}`);

@aikido-pr-checks aikido-pr-checks Bot Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pendingPath ?? expression contains a ternary fallback, requiring readers to evaluate two conditional selections in one expression.

Suggested change
const path = pendingPath ?? (prefix === "" ? name : `${prefix}/${name}`);
const archivePath = prefix === "" ? name : `${prefix}/${name}`;
const path = pendingPath ?? archivePath;
Details

✨ AI Reasoning
​The expression first selects whether to use pendingPath, then conditionally chooses between name and the prefixed path. Evaluating both selection steps on one line adds avoidable cognitive complexity.

Reply on this comment to give feedback or ignore the issue.
More info

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed the current head, including the latest commits that bump @modelcontextprotocol/sdk and zod to patched versions and address the earlier validate-ai findings in assessing-test-coverage. The bundled bitwarden-test-automation server was checked end to end: commit sha validation before the tarball request, gh spawn and error classification, the streaming tar reader (pax, GNU long-name, and ustar prefix handling), the *.Tests path filter, exact-match case ID scanning, and the TTL-cached index. The .mcp.json launch command and CI workflow match the sibling bitwarden-atlassian-mcp-server pattern, and the workflow uses permissions: {}, pinned actions, and persist-credentials: false. The skill's new allowed-tools entries are read-only MCP tools only. The remaining gh api repos/bitwarden/* grant was already there before this PR. Existing open threads were not duplicated.

Not covered: Skill review did not run. This review path cannot launch plugin-dev:skill-reviewer, so plugins/bitwarden-testing-tools/skills/assessing-test-coverage/SKILL.md and plugins/bitwarden-testing-tools/skills/designing-test-plans/SKILL.md were not checked for description quality, length, or progressive disclosure. performing-multi-agent-code-review covers them where plugin-dev is installed.

Code Review Details

No new findings.

Dependency Changes

Package Change Ecosystem
@modelcontextprotocol/sdk New (1.32.0) npm
zod New (4.6.6) npm
typescript New (7.0.2) npm
@types/node New (24.13.3) npm
vitest New (5.0.2) npm
corepack New (0.36.0) npm

All six are new to this package only. The same versions already ship in bitwarden-atlassian-mcp-server.


## Steps

Before the first read from Jira or Testmo, load the tools by exact name with ToolSearch: `select:mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_issue` for Jira, and `select:mcp__testmo__get_states,mcp__testmo__get_statuses,mcp__testmo__get_repository_folders,mcp__testmo__get_repository_folder,mcp__testmo__get_repository_cases,mcp__testmo__get_repository_case,mcp__testmo__get_case_result_history` for Testmo. Do not use keyword searches for these tools, since they can match unrelated tools. A keyword search that finds nothing does not mean the tools are missing. If an exact-name load finds nothing, try the same load once more before doing anything else, because the plugin's servers can take a moment to start. Report a source as unavailable only when that second load or a call to the tool itself fails, and quote the error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The mandatory exact-name preload omits find_automated_cases, the one tool step G calls.

Details and fix

This line establishes the rule that MCP tools must be loaded by exact name before use, and lists select: entries for the Atlassian tool and all seven mcp__testmo__* tools. It does not list mcp__plugin_bitwarden-testing-tools_bitwarden-test-automation__find_automated_cases, which step G (line 133) then calls.

That tool is the one most likely to be missing at first call. Its server launches via pnpm install --frozen-lockfile --prod and a tsc build (plugins/bitwarden-testing-tools/.mcp.json:8), which is exactly the "the plugin's servers can take a moment to start" case this line warns about. And unlike step A, which has an explicit fallback for Testmo being unreachable, step G only handles the tool returning Code not checked — there is no instruction for the tool not being registered at all. A cold-start miss therefore degrades into a silent skip of the Automation Type check rather than a NOT CHECKED line.

Append the tool to the exact-name select: list:

select:mcp__testmo__get_states,mcp__testmo__get_statuses,mcp__testmo__get_repository_folders,mcp__testmo__get_repository_folder,mcp__testmo__get_repository_cases,mcp__testmo__get_repository_case,mcp__testmo__get_case_result_history,mcp__plugin_bitwarden-testing-tools_bitwarden-test-automation__find_automated_cases

validate-ai

5. Run the Testmo procedure below for every story. If Testmo cannot be read, record why in the Testmo section and continue with the code side.
6. Write the report to `${CLAUDE_PLUGIN_DATA}/coverage-reports/<slug>-<timestamp>-coverage.md` (`<slug>` from the ticket/PR/feature; `<timestamp>` from `date +%Y-%m-%d-%H%M%S`) using the template below.

## Testmo procedure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: This PR takes the skill from 635 to 4,398 words inline, past the 3,000-word ceiling, with no references/ directory.

Details and fix

Measured on the merge base and head:

git show 8aa784a:…/assessing-test-coverage/SKILL.md | wc -w   # 635
wc -w …/assessing-test-coverage/SKILL.md                       # 4398

The "Testmo procedure" (steps A–J, lines 43–184) and the "Testmo section layout" (lines 223–287) are roughly 3,700 new words added inline. All of it loads into context on every invocation of the skill, including the parts that only matter once cases have already been found — the entry layouts, the confidence checklist, the per-section ordering rules.

The growth is entirely introduced by this changeset, so this is not a pre-existing length problem.

Move "Testmo procedure" and "Testmo section layout" into references/testmo-case-review.md and leave a one-line pointer at step 5. The plugin already has this pattern in-tree at plugins/bitwarden-testing-tools/skills/setting-up-regression-runs/references/, so the layout is established.

Reference: https://code.claude.com/docs/en/skills

validate-ai

description: Use when determining what test coverage ALREADY exists for a specific change (a PR, Jira key, Tech Breakdown doc, Testmo CSV, changed paths, or named component). Triggers on "what's already tested", "does this PR have tests", "what coverage exists for", "is this component covered", or "which behaviors have no test today". This is a backward-looking inventory of existing coverage for a concrete change. Do NOT use it to recommend or decide which new tests to add ("should I add integration tests here", "are unit tests enough"), to design a test strategy or plan, to run or fix existing tests, or to explain testing concepts like the test pyramid or which layers a repo uses — those are all out of scope.
argument-hint: "[PR URL | Jira key | Tech Breakdown doc | Testmo CSV]"
allowed-tools: "Read, Write, Grep, Glob, Bash(date:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh api repos/bitwarden/*), Bash(gh search code:*), Bash(git rev-parse:*), Bash(git remote get-url:*), Bash(git -C * rev-parse:*), Bash(git -C * remote get-url:*), Bash(git clone:*), Skill(bitwarden-atlassian-tools:researching-jira-issues)"
description: Use when determining what test coverage ALREADY exists for a specific change (a PR, Jira key, Tech Breakdown doc, Testmo CSV, changed paths, or named component), or which existing Testmo cases and tests it makes obsolete, wrong, or duplicated, including other teams' cases. Use it before writing new tests or Testmo cases. Triggers on "what's already tested", "does this PR have tests", "what coverage exists for", "is this component covered", "which behaviors have no test today", "which Testmo cases does this story make obsolete", or "which tests should be retired or updated for PM-123". A backward-looking review: it recommends update or retire, lists possible duplicates, notes other teams' affected cases, and never writes to Testmo or code. Do NOT use it to decide which new tests to add ("should I add integration tests here", "are unit tests enough"), to design a test strategy or plan, to run or fix existing tests, or to explain testing concepts like the test pyramid or which layers a repo uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The description advertises "lists possible duplicates" but carries no duplicate-shaped trigger, and the PR's own new eval for it records 0/7.

Details and fix

The rewritten description claims the capability — "it recommends update or retire, lists possible duplicates, notes other teams' affected cases" — but every phrase in the Triggers on list is about coverage, obsolescence, or retirement. None is duplicate-shaped.

The PR adds an eval query for exactly this capability and records it failing:

"query": "PM-41861 and PM-42581 have the same title -- did we end up with two sets of Testmo cases for the same thing?",
"trigger_rate": 0.0

plugins/bitwarden-testing-tools/evals/README.md:104 confirms it is also 0/7 on claude-opus-5-5, so it is not a single-model artifact. Unlike the RestoreAsync, passkey, and cipher-key-rotation queries, this one has no "before this change" measurement showing it was already failing — it is new, for a capability this PR is adding.

The plugin's own policy at evals/README.md:92 says to "fix the skill description rather than the eval set" when a query drops below threshold. Here the failure was documented instead of fixed, so a headline capability of the PR will not route.

Add a duplicate trigger phrase to the Triggers on list, for example "do we have two Testmo cases for the same thing", then re-record evals/baseline.json.

validate-ai

- No em dashes anywhere in the output. Use a colon, a comma, or a new sentence instead.
- The Evidence & sources row lists the folders searched and says "searched by case name only."
- Readers may not have a Testmo seat, so each entry stands on its own: link, folder, why, and the note to paste or the proposed text.
- HEADS-UP FOR OTHER TEAMS lists only heads-up cases that need an update or retire under step E. A heads-up case that needs a change is listed there, not under SUGGESTED CHANGES.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The word "only" here silently drops the shared-case cross-team notifications that line 123 exists to produce.

Details and fix

This line says HEADS-UP FOR OTHER TEAMS "lists only heads-up cases that need an update or retire under step E."

"Heads-up case" is defined at line 87 as a case whose Team field names other teams but not the story's team. Line 86 puts a shared case — Team names the story's team and others — in full-review scope instead, so a shared case is by definition not a heads-up case.

But line 123 requires exactly that:

When a full-review case is shared … and gets a retire or update, each other team on the case also gets a heads-up line in the HEADS-UP FOR OTHER TEAMS section.

So line 123 mandates entries that line 235's "only" excludes. A model weighting line 235 produces no cross-team notification for shared cases, which is the single case where another team most needs one. Both lines are new in this PR.

Change line 235 to admit both sources:

- HEADS-UP FOR OTHER TEAMS lists heads-up cases that need an update or retire under step E, plus one line per other team on a shared full-review case that needs a change. A heads-up case that needs a change is listed there, not under SUGGESTED CHANGES.

validate-ai


F. **Read the history of each retire or update candidate.** Call `get_case_result_history` with project ID 1 and the case ID for the last run date and result. Results come back as status IDs. Call `get_statuses` with project ID 1 once per run to turn them into names, such as Passed or Failed. Keep the number of candidates small. Each call is one request.

G. **Check Automation Type against code.** Run this for every full-review case, including ones the story does not change, because a mismatch is a finding either way. Call `find_automated_cases` with the case IDs, up to 50 per call. It reads `bitwarden/test` on its main branch through GitHub, never a local clone. For each ID it returns found or not found, and for each match the file, line, attribute, platform, and whether the test is ignored or has a KnownBug.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Step G opens and closes on two different case sets, so INTEGRITY MISMATCHES will be populated inconsistently between runs.

Details and fix

This line opens with:

Run this for every full-review case, including ones the story does not change, because a mismatch is a finding either way.

Line 149 closes the same step with:

Check only the IDs you fetched in full-review scope.

Those are different sets, because step C at line 82 explicitly says Do not fetch every case in the results. Only a subset is ever fetched, so "every full-review case" and "the IDs you fetched" can never coincide, and which one a run follows decides how much of the Automation Type check actually happens.

Line 133's version is also the cheaper one to honour: find_automated_cases takes case IDs only and accepts up to 50 per call, so it needs nothing from get_repository_case. Fetching is irrelevant to it.

Settle it on line 149 in favour of the wider set:

Report mismatches only, for the team to decide. Do not call them bugs. Check every full-review case ID from step C's merged results, fetched or not. If the tool returns "Code not checked", list the cases as not checked for code, with the reason the tool gave. Never state that a case has no test in code unless the tool returned that result.

validate-ai


The "Coverage now" part only tells the team where the behavior is tested. It is not the reason for the retire, so it may name newer cases. The "why" part may not.

F. **Read the history of each retire or update candidate.** Call `get_case_result_history` with project ID 1 and the case ID for the last run date and result. Results come back as status IDs. Call `get_statuses` with project ID 1 once per run to turn them into names, such as Passed or Failed. Keep the number of candidates small. Each call is one request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Step F mandates a history call for every update candidate, but the Update layout has no field to hold the result and line 231 forbids adding one.

Details and fix

This step requires get_case_result_history plus get_statuses for "each retire or update candidate".

The Retire layout has somewhere to put that:

        Last run: <date and result, or "no results recorded">   # line 248

The Update layout (lines 252–260) has no such field, and line 231 closes off improvising one:

An entry holds only the fields in its layout. No parenthetical remarks, and no comments after an entry.

So every update candidate costs a Testmo request whose result must then be discarded — in a procedure that warns about rate limits at line 183 and tells the model at line 131 to "Keep the number of candidates small. Each call is one request."

Add the field to the Update layout at line 258, matching the Retire layout:

        Automation Type: <type, only when relevant>
        Last run: <date and result, or "no results recorded">
        Now:      <current step or expected result>
        Proposed: <proposed step or expected result>

validate-ai


10. Before writing the plan, check that every behavior AC (scope ACs excluded) appears in at least one test row or in the Unspecified table. Add any missing AC to the Unspecified table with "no test derived" in the Source column. Write the plan using the template below to `${CLAUDE_PLUGIN_DATA}/designing-test-plans/<key>-<timestamp>-test-plan.md`. Get `<timestamp>` by running `date +%Y-%m-%d-%H%M%S` as a single command with nothing else in it (no echo, pipes, or command substitution), then use its output. State the full path when done. If the write fails, say so and return the full plan inline instead of a path.

11. Hand the saved plan to coverage. Only when the user's request also asks about existing tests or Testmo cases, and only after the plan is saved, invoke `Skill(bitwarden-testing-tools:assessing-test-coverage)` with the key and the saved plan's full path. Pass only those two, never the plan's contents. If the plan write failed, do not invoke it, and tell the user the coverage review needs a saved plan. If the call fails, report it and keep the plan path in your answer. Report the coverage result's report path along with the plan path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The new step 11 only fires on the one request shape the description routes to the sibling skill instead.

Details and fix

Step 11 activates only when the request "also asks about existing tests or Testmo cases". But the description at line 3 — unchanged by this PR — tells the router:

Do NOT use it … to inventory existing tests (use assessing-test-coverage)

So "design the tests for PM-123 and tell me which cases we already have" is steered to assessing-test-coverage, which designs no plan. The user lands in the coverage skill with no plan to overlap against, and the handoff this PR adds never runs.

This is a reachability problem the PR introduces: before step 11 existed, the line 3 exclusion was simply correct. Now it suppresses the new feature's only trigger condition.

Amend the exclusion on line 3 so it carves out the combined request:

to inventory existing tests on their own (use assessing-test-coverage; this skill hands the saved plan to it when the same request also asks what already exists)

Note this touches the description, so per plugins/bitwarden-testing-tools/evals/README.md:70-77 the eval needs re-running for both designing-test-plans and assessing-requirement-readiness, with the refreshed baseline.json committed.

validate-ai


```bash
read -s TESTMO_TOKEN # paste your key, then press Enter
claude mcp add testmo "https://testmo.sembi.com/mcp?mode=read-only&INSTANCE-URL=bitwarden.testmo.net" --transport http --scope user --header "Authorization: Bearer $TESTMO_TOKEN"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The documented setup expands the Testmo API key into process argv, where any local process can read it.

Details and fix

$TESTMO_TOKEN is in double quotes, so the shell expands it before claude is executed. The plaintext key becomes an element of the command's argument list, readable by any local process through ps or /proc/<pid>/cmdline for the duration of the call.

The read -s on line 55 is good — it keeps the key out of shell history and off the screen — but it does not cover argv. The README notes the key is saved to ~/.claude.json, which is the at-rest exposure, but not this one.

The repo's own bundled-server pattern avoids it: plugins/bitwarden-atlassian-tools/.mcp.json:11-15 stores literal ${VAR} strings and lets Claude Code expand them at server launch.

Single-quote the header so the literal placeholder is stored and expanded at launch instead:

export TESTMO_TOKEN=...   # from your shell profile, not pasted inline
claude mcp add testmo "https://testmo.sembi.com/mcp?mode=read-only&INSTANCE-URL=bitwarden.testmo.net" --transport http --scope user --header 'Authorization: Bearer ${TESTMO_TOKEN}'

validate-ai


Never search or recommend cases in **Retired Test Cases** or **Need to be deleted**. Skip any result whose folder path starts with either name. Cases in those two folders are not reviewed, even when they are Active.

C. **Find related cases.** Call `get_repository_cases` once per client folder and once for New Release, with `folder_id` set to that folder, `recursive` true, `state_id` set to every state except Retired, `sort` set to `repository_cases:name`, `order` set to `asc`, and `per_page` 25. Make one call per distinctive term from the acceptance criteria, using `name`. Testmo matches the case name only, partially and without regard to case, and does not search case bodies. A case whose name shares no term with the ACs will not show up. Each result includes the case's ID, name, folder ID, state, Team, Automation Type, Needs Review, Remove After Release, and description. Merge the results by case ID. If a search returns more than one page, narrow it with a more specific name before reading more pages. For the folder path of each case you report, call `get_repository_folder` with `expands` set to `folders`. Send only the parameters named in this step. Do not add date filters or any other filter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Step C's search fan-out has no cap — an unclear client can reach ~66 calls before any other Testmo request.

Details and fix

This step says to call get_repository_cases "once per client folder and once for New Release", then "one call per distinctive term from the acceptance criteria".

When the client is unclear, line 74 directs the skill to search all client folders plus New Release — 11 folders. With six distinctive AC terms that is 66 search calls before a single get_repository_folder, get_repository_case, or get_case_result_history runs.

Nothing bounds it. Line 183 tells the skill what to do after it has been rate-limited, which means the budget is spent on breadth-first name searches and the detail reads that produce the actual recommendations are the ones that get cut off.

Cap the fan-out in this step:

Use at most five distinctive terms, chosen from the ACs by specificity, and stop searching after 40 calls, recording the unsearched folders under NOT CHECKED.

validate-ai

allowed-tools: "Read, Write, Grep, Glob, Bash(date:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh api repos/bitwarden/*), Bash(gh search code:*), Bash(git rev-parse:*), Bash(git remote get-url:*), Bash(git -C * rev-parse:*), Bash(git -C * remote get-url:*), Bash(git clone:*), Skill(bitwarden-atlassian-tools:researching-jira-issues)"
description: Use when determining what test coverage ALREADY exists for a specific change (a PR, Jira key, Tech Breakdown doc, Testmo CSV, changed paths, or named component), or which existing Testmo cases and tests it makes obsolete, wrong, or duplicated, including other teams' cases. Use it before writing new tests or Testmo cases. Triggers on "what's already tested", "does this PR have tests", "what coverage exists for", "is this component covered", "which behaviors have no test today", "which Testmo cases does this story make obsolete", or "which tests should be retired or updated for PM-123". A backward-looking review: it recommends update or retire, lists possible duplicates, notes other teams' affected cases, and never writes to Testmo or code. Do NOT use it to decide which new tests to add ("should I add integration tests here", "are unit tests enough"), to design a test strategy or plan, to run or fix existing tests, or to explain testing concepts like the test pyramid or which layers a repo uses.
argument-hint: "[PR URL | Jira key | Tech Breakdown doc | Testmo CSV] [designing-test-plans plan path]"
allowed-tools: "Read, Write, Grep, Glob, Bash(date:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh api repos/bitwarden/*), Bash(gh search code:*), Bash(git rev-parse:*), Bash(git remote get-url:*), Bash(git -C * rev-parse:*), Bash(git -C * remote get-url:*), Bash(git clone:*), Skill(bitwarden-atlassian-tools:researching-jira-issues), mcp__testmo__get_states, mcp__testmo__get_statuses, mcp__testmo__get_repository_folders, mcp__testmo__get_repository_folder, mcp__testmo__get_repository_cases, mcp__testmo__get_repository_case, mcp__testmo__get_case_result_history, mcp__plugin_bitwarden-testing-tools_bitwarden-test-automation__find_automated_cases, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_issue"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Bash(git clone:*) is an unrestricted-URL network fetch that undercuts the containment the new disallowed-tools line establishes.

Details and fix

The new line 6 denies Bash(curl *), Bash(wget *), WebFetch, and .env reads, keeping the skill's reads on sanctioned channels. That is the right direction, and the PR deserves credit for it.

Bash(git clone:*) on this line takes an arbitrary URL and is still allowed. No step in the skill uses it, and gotcha line 179 explicitly tells the skill not to clone and to use gh instead.

To be clear on scope: the grant predates this PR, so it is not a widening introduced here. It is flagged because line 6 is new and states an intent this leftover grant defeats — closing it is what makes the new deny list mean what it says.

Drop it from allowed-tools:

allowed-tools: "Read, Write, Grep, Glob, Bash(date:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh api repos/bitwarden/*), Bash(gh search code:*), Bash(git rev-parse:*), Bash(git remote get-url:*), Bash(git -C * rev-parse:*), Bash(git -C * remote get-url:*), Skill(bitwarden-atlassian-tools:researching-jira-issues), mcp__testmo__get_states, mcp__testmo__get_statuses, mcp__testmo__get_repository_folders, mcp__testmo__get_repository_folder, mcp__testmo__get_repository_cases, mcp__testmo__get_repository_case, mcp__testmo__get_case_result_history, mcp__plugin_bitwarden-testing-tools_bitwarden-test-automation__find_automated_cases, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_issue"

validate-ai


If the user also gives a `designing-test-plans` plan (a file path or pasted content), read it. Keep its acceptance criteria (`AC-1`, `AC-2`, ...), its Tests table (test ID, layer, method), and its Stated scope. Use the AC IDs and plan items as the reasons for every Testmo recommendation.

If no plan path was given and the input names a Jira story, look in `${CLAUDE_PLUGIN_DATA}/designing-test-plans/` for files named exactly `<KEY>-<YYYY-MM-DD-HHMMSS>-test-plan.md`, where `<KEY>` is that story's key. Ignore every other file, including copies with extra text in the name. If any match, use the one with the latest timestamp in its file name as the plan (not the file's modified time), and name the file and its date in the report's Evidence & sources. If none exist, continue without a plan.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The new plan-discovery glob fails silently when ${CLAUDE_PLUGIN_DATA} did not expand, unlike the sibling skill that writes to the same directory.

Details and fix

This step reads ${CLAUDE_PLUGIN_DATA}/designing-test-plans/ with no check that the variable resolved, and its failure mode is "If none exist, continue without a plan" — indistinguishable from a genuinely empty directory.

designing-test-plans/SKILL.md:17 guards the same path explicitly on the write side:

If it still contains ${ or starts with /designing-test-plans, the plugin data variable was empty. Stop with a clear error.

The read side having no equivalent matters because the plan drives the recommendations: step E at line 98 refuses any Update or Retire not backed by an AC or plan item. An unresolved path degrades the whole Testmo section while reporting nothing.

Add a guard before the glob:

If the resolved directory still contains `${` or starts with `/designing-test-plans`, the plugin data variable was empty. Record "plan directory unresolved" under NOT CHECKED rather than continuing as if no plan existed.

validate-ai

4. Record each behavior: layer (unit / integration / E2E), representative test permalink(s), count, source. Behaviors with no test found → gaps.
5. Write the report to `${CLAUDE_PLUGIN_DATA}/coverage-reports/<slug>-<timestamp>-coverage.md` (`<slug>` from the ticket/PR/feature; `<timestamp>` from `date +%Y-%m-%d-%H%M%S`) using the template below.
4. Record each behavior: layer (unit / integration / E2E), representative test permalink(s), count, source. Behaviors with no test found are gaps.
5. Run the Testmo procedure below for every story. If Testmo cannot be read, record why in the Testmo section and continue with the code side.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Step 5 runs the full Testmo procedure for inputs that can produce no recommendation.

Details and fix

This step says to run the Testmo procedure "for every story", but step 1 accepts a PR URL, a Tech Breakdown doc, and a Testmo CSV, and the description at line 3 advertises all four.

For an input that resolves to no Jira story and no plan there are no ACs. Line 98 then forbids every outcome the procedure exists to produce:

Every retire and update names the AC or plan item it rests on. If no AC or plan item supports a recommendation, do not make it.

So steps C through F run their folder searches, case fetches, and history calls at full cost and step E can emit nothing. The report shows "No Testmo cases need changes for this story" (line 228), which reads as a verified negative rather than as "there was nothing to check against".

Add one sentence to this step:

If the input resolves to no story and no plan, skip the Testmo procedure and record "no AC source, Testmo cases not reviewed" under NOT CHECKED.

validate-ai


10. Before writing the plan, check that every behavior AC (scope ACs excluded) appears in at least one test row or in the Unspecified table. Add any missing AC to the Unspecified table with "no test derived" in the Source column. Write the plan using the template below to `${CLAUDE_PLUGIN_DATA}/designing-test-plans/<key>-<timestamp>-test-plan.md`. Get `<timestamp>` by running `date +%Y-%m-%d-%H%M%S` as a single command with nothing else in it (no echo, pipes, or command substitution), then use its output. State the full path when done. If the write fails, say so and return the full plan inline instead of a path.

11. Hand the saved plan to coverage. Only when the user's request also asks about existing tests or Testmo cases, and only after the plan is saved, invoke `Skill(bitwarden-testing-tools:assessing-test-coverage)` with the key and the saved plan's full path. Pass only those two, never the plan's contents. If the plan write failed, do not invoke it, and tell the user the coverage review needs a saved plan. If the call fails, report it and keep the plan path in your answer. Report the coverage result's report path along with the plan path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Step 11 invokes a sibling skill with no availability check, unlike the two other sub-skill calls in this file.

Details and fix

Lines 25–26 guard both other sub-skill invocations with an explicit "if not available, stop and tell the user to install bitwarden-testing-tools". Step 11 has no equivalent — its only guard is "If the call fails", which is ambiguous when the skill is absent rather than erroring.

The failure mode is mild (the plan is already saved by this point) but the user gets no explanation for why the coverage review they asked for never appeared.

Add the same guard the file already uses, before the invocation:

If `Skill(bitwarden-testing-tools:assessing-test-coverage)` is not available, say so, return the plan path, and stop.

validate-ai


10. Before writing the plan, check that every behavior AC (scope ACs excluded) appears in at least one test row or in the Unspecified table. Add any missing AC to the Unspecified table with "no test derived" in the Source column. Write the plan using the template below to `${CLAUDE_PLUGIN_DATA}/designing-test-plans/<key>-<timestamp>-test-plan.md`. Get `<timestamp>` by running `date +%Y-%m-%d-%H%M%S` as a single command with nothing else in it (no echo, pipes, or command substitution), then use its output. State the full path when done. If the write fails, say so and return the full plan inline instead of a path.

11. Hand the saved plan to coverage. Only when the user's request also asks about existing tests or Testmo cases, and only after the plan is saved, invoke `Skill(bitwarden-testing-tools:assessing-test-coverage)` with the key and the saved plan's full path. Pass only those two, never the plan's contents. If the plan write failed, do not invoke it, and tell the user the coverage review needs a saved plan. If the call fails, report it and keep the plan path in your answer. Report the coverage result's report path along with the plan path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The saved plan file gets no pointer to the coverage review produced from it, so the link exists only in the chat reply.

Details and fix

Step 11 says to "Report the coverage result's report path along with the plan path", but only in the answer to the user. The output template header (lines 84–90) gained no corresponding field, so the plan written to ${CLAUDE_PLUGIN_DATA}/designing-test-plans/ is a standalone artifact with no reference to the coverage report written in the same run.

Anyone reopening the plan later — which is the point of saving it to a stable path — cannot find the review that went with it.

Add a line to the template header after Manual tests: (line 90), and have step 11 fill it:

Coverage review: <report path>, or not run.

validate-ai

"declaration": true,
"declarationMap": true,
"sourceMap": true
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Test-only fixture code is compiled into the production build that ships to every plugin user.

Details and fix

exclude lists src/**/*.spec.ts, but src/github/tar-fixtures.ts is not a .spec.ts file. Its own header says it is not used by the server, and it is imported only by spec files — yet it compiles to build/github/tar-fixtures.js and is shipped by the pnpm run build the launch command runs (plugins/bitwarden-testing-tools/.mcp.json:8, which builds with --prod).

Dead code on the runtime surface of a server that otherwise keeps a tight read-only footprint.

Add the fixture pattern to exclude:

"exclude": ["node_modules", "build", "src/**/*.spec.ts", "src/**/*-fixtures.ts"]

validate-ai


I. **Possible duplicates.** A possible duplicate is two cases that test the same behavior, whether or not their names match. Same name is one way to spot one, not the only way. Check every full-review case, and each heads-up case that needs an update or retire under step E, whichever folder it sits in. Report both IDs, both links, and both sets of steps. The team decides. A newer case that covers the same behavior as an older one is a possible duplicate, not a reason to retire the older case. Do not recommend retiring either case on that basis.

J. **Plain notes.** Say these in the case's entry, without judgment:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Step J mandates four plain notes, but the Update layout has no slot for three of them and line 231 forbids improvising one.

Details and fix

This is the same class as the already-raised step F finding, on a different step.

Step J requires these in "the case's entry":

  • Needs Review or Remove After Release is checked
  • The state is not Active
  • Automation Type is Ready to Automate
  • The Team field is empty, say "No team set"

The Retire layout covers two of them: State: (line 247) and Checkboxes: (line 250). It has no empty-team field.

The Update layout (lines 252-260) has Automation Type: and nothing else from that list: no State:, no Checkboxes:, no empty-team field.

Line 231 then closes off the workaround:

An entry holds only the fields in its layout. No parenthetical remarks, and no comments after an entry.

So for an Update entry, three of step J's four notes are mandatory to produce and impossible to place. Step J is literally unsatisfiable there, and "No team set" is unplaceable in either layout.

Add the missing fields to both layouts, each marked conditional so they stay off a normal entry:

        State: <only when not Active, e.g. "Draft, not Active">
        Team: <"No team set", only when the Team field is empty>
        Checkboxes: <Needs Review / Remove After Release, only when checked>

validate-ai

const read = new Date(ctx.now()).toISOString();
const header = [
`Source: ${CODE_REPO}, ${CODE_BRANCH} at ${index.sha} (read ${read}).`,
`Scanned ${index.filesScanned} .cs files under *.Tests folders. ProdSmoke is excluded on purpose.`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The result header names only the ProdSmoke exclusion, but the scanner also skips every *TestBase.cs, so the skill reports those cases as false integrity mismatches.

Details and fix

src/code/scan.ts:91 drops two classes of file, not one:

if (!path.endsWith(".cs") || path.endsWith("TestBase.cs")) {
  return false;
}
return !segments.includes("ProdSmoke");

The header this line builds discloses only the second:

Scanned ${index.filesScanned} .cs files under *.Tests folders. ProdSmoke is excluded on purpose.

That matters because the consuming skill converts a non-result into a reported finding. assessing-test-coverage/SKILL.md:147:

The type is Automated, but the tool finds no match, is a mismatch.

A case whose [Description("N")] lives in a *TestBase.cs therefore lands in the INTEGRITY MISMATCHES table as an automation gap, with nothing in the tool output to tell the reader a whole class of files was never read. The skill is also told at line 150 never to state a case has no test unless the tool returned that result, so it has no way to caveat it.

Name the exclusion in the header:

`Scanned ${index.filesScanned} .cs files under *.Tests folders. ProdSmoke folders and *TestBase.cs files are excluded on purpose.`,

validate-ai

Now: <current step or expected result>
Proposed: <proposed step or expected result>

HEADS-UP FOR OTHER TEAMS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The HEADS-UP layout omits the folder, the why, and the retire note that line 234 requires on every entry, for the readers least able to supply them.

Details and fix

Line 234 states the rule:

Readers may not have a Testmo seat, so each entry stands on its own: link, folder, why, and the note to paste or the proposed text.

The HEADS-UP layout gives a link, a free-text message, and Confidence. No Folder:, no Why:, and no slot for the note.

The gap bites hardest here because of where line 235 routes work:

A heads-up case that needs a change is listed there, not under SUGGESTED CHANGES.

So a heads-up case needing a retire never appears under SUGGESTED CHANGES, and step E at line 125 still requires a retire note for it. The note is produced with nowhere to go, and line 231 bans appending it as a remark. The other team, who by definition did not run the review and are the people line 234 is written for, get the least complete entry in the report.

Add the three fields to the layout:

HEADS-UP FOR OTHER TEAMS
<Owning team>, ready to forward:
  <short message: what changed, and the case to check>
  <case ID> <case name>: <link>
  Folder: <folder path>
  Why: <AC-N or plan item, and what in the case is affected>
  Note to paste: <the retire note or proposed text, exactly as step E gives it>
  Confidence: <High | Low. Trigger N: <one line quoting the words>>

validate-ai

Automation: <plan wants automation | plan is manual>. Case is <manual | automated>.
Outcome: <one of the four outcomes from step H>.

POSSIBLE DUPLICATES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: POSSIBLE DUPLICATES hardcodes exactly two folder labels, so the duplicate pairs step I defines mostly cannot be written down.

Details and fix

The layout fixes one row per folder name:

New Release: <case ID>, <link>. Steps: <steps>.
Client folder: <case ID>, <link>. Steps: <steps>.

Step I at line 165 defines the check much more widely:

Check every full-review case, and each heads-up case that needs an update or retire under step E, whichever folder it sits in.

Three shapes the layout cannot express, all of them in scope for step I: two duplicates sitting in the same client folder, a pair where one case is in another team's heads-up folder, and more than two cases testing the same behavior. The layout also presumes which of the pair is the New Release one, which step I does not.

Line 229 ("Use only the sections listed below") and line 231 ("An entry holds only the fields in its layout") mean the model cannot adapt it on the fly, so the likely outcome is a real duplicate going unreported or being forced into the wrong label.

Replace the two fixed rows with one repeatable row that carries the folder as data:

POSSIBLE DUPLICATES
<folder path>: <case ID> <case name>, <link>. Steps: <steps>.
<folder path>: <case ID> <case name>, <link>. Steps: <steps>.
The team decides.

validate-ai

The Testmo section must be as simple as possible.

- Show only the sections that have entries. No empty headings, no counts of untouched matches, no "keep" list.
- If nothing needs action, the whole Testmo section is one line: "No Testmo cases need changes for this story."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The collapse-to-one-line rule can suppress a populated INTEGRITY MISMATCHES table and a NOT CHECKED section, turning a partial run into a clean bill of health.

Details and fix

This line says:

If nothing needs action, the whole Testmo section is one line: "No Testmo cases need changes for this story."

"Nothing needs action" reads naturally as "no case needs an update or retire", which is the step E outcome. Two other sections can legitimately have entries in exactly that run:

  • Step G at line 133 runs the automation check for every full-review case, "including ones the story does not change, because a mismatch is a finding either way". A run with zero update or retire candidates can still produce integrity mismatches.
  • Line 230 permits a NOT CHECKED section whenever something could not be checked, including a Testmo read failure. Step 5 at line 40 and step A both route failures there.

A model following this line literally collapses both away and asserts the story is clean, which is the one output that is actively misleading: a rate-limited or partially-failed run looks identical to a verified-clean one.

Scope the rule to all three sections:

- If no case needs an update or retire, and there are no integrity mismatches and nothing to record as not checked, the whole Testmo section is one line: "No Testmo cases need changes for this story."

validate-ai


Without a plan, answer only question a, and mention Ready to Automate where seen.

I. **Possible duplicates.** A possible duplicate is two cases that test the same behavior, whether or not their names match. Same name is one way to spot one, not the only way. Check every full-review case, and each heads-up case that needs an update or retire under step E, whichever folder it sits in. Report both IDs, both links, and both sets of steps. The team decides. A newer case that covers the same behavior as an older one is a possible duplicate, not a reason to retire the older case. Do not recommend retiring either case on that basis.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Step I must report "both sets of steps" for a duplicate pair, but step C withheld the fetch that produces them.

Details and fix

This step requires the steps text for both cases in a pair:

Report both IDs, both links, and both sets of steps.

Step C at line 82 decides what was fetched, and deliberately fetches a subset:

Call get_repository_case ... for the cases that look related to the ACs. Do not fetch every case in the results.

A duplicate pair is found by comparing behavior across the full-review set, so one half of a pair routinely sits outside "looks related to the ACs" — and the whole premise of step I is that names need not match, so name-similarity will not have pulled it in either. The steps text is only on the detail record, so for those cases the model either reports a pair with half the required content or quietly drops the pair.

This is the mirror image of the already-raised step G finding, where the wide reading won. Here the narrow fetch rule is correct for cost, so the fix belongs in step I:

Before reporting a candidate pair, call `get_repository_case` for either case whose steps step C did not already fetch. If a fetch fails, report the pair with the steps you have and name the missing one under NOT CHECKED.

validate-ai

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

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant