Skip to content

feat(testing): flag C# typeof-only, construct-only and BDD cannot-fail tests in the audit #6040

Closed

Description

@kyle-sexton

Part of #6038

Problem

The testing:audit C# adapter misses the worst cannot-fail shapes in .NET suites, does not scan step-definition or UnitTest1.cs files, and its generous check assertion token suppresses print-only diagnostic tests.

On main (testing 0.21.3), plugins/testing/skills/audit/adapters/cs-xunit.yaml:

  • :9 files: ['*Test.cs', '*Tests.cs'], so *StepDefinitions.cs (Reqnroll [Given]/[When]/[Then]) and UnitTest1.cs are never enumerated.
  • :11-12 test_start recognizes Fact, Theory, Test, TestMethod, TestCase, DataTestMethod, not Reqnroll step attributes.
  • :18-33 assertion tokens include [Cc]heck, [Ee]nsure, [Vv]alidate, which match method and type names (for example *_CheckConnectionStrings, a ...Checker type), so a test that only prints counts as asserting.
  • :42-46 weak covers Assert.NotNull, so Assert.NotNull(typeof(X)) is a report-only weak-oracle, not a cannot-fail finding.

Evidence

Verified this pass by running main's plugins/testing/skills/audit/scripts/cant-fail-scan.sh --findings (extracted from origin/main into a scratch directory) over a synthetic xUnit fixture:

Shape Result on main
Assert.NotNull(typeof(Widget)); weak-oracle (report-only)
Assert.True(nameof(Widget) == typeof(Widget).Name); not flagged
Assert.True(typeof(ComponentBase).IsAssignableFrom(typeof(Widget))); not flagged
var x = new Widget(); Assert.NotNull(x); Assert.IsType<Widget>(x); not flagged
var x = new Widget(); Assert.NotNull(x); weak-oracle (report-only)
print-only Diagnostics_CheckConnectionStrings (Console.WriteLine only) not flagged
if (cfg is null) return; before the only assertion not flagged
UnitTest1.cs and LoginStepDefinitions.cs with [Then] ... Assert.True(true); not enumerated (1 of 1 files examined was ShapeTests.cs)

The item reported Assert.NotNull(typeof(X)) as "not flagged at all"; on main it is flagged, but only as report-only weak-oracle.

Reported by the producing session from agent reading on a real repo, not rechecked here: logic re-implemented in the test body over literals with no production call; a discarded Result as the only oracle (including through a shared helper); identical-body duplicate tests in one class under different names; inherited architecture-test methods that return before building a rule. False positives it saw (4 of 218 findings, so precision is good): an assertion two hops away in a static helper in another file; an assertion in a fixture or base-class helper inside a DependsOn chain; await task.WaitAsync(timeout) as an implicit no-throw oracle; null-returning or throwing lookups (FindProperty, GetCustomAttribute, smart-enum FromName) read as weak oracles.

Proposed approach

  1. Add cannot-fail rules for C#: typeof-only assertions (typeof(...) and nameof(...) with no other operand, and IsAssignableFrom between two typeof expressions), and construct-then-check-construction (new T() followed only by NotNull/IsType<T> on that variable). Note the construct shape can fail if the constructor throws; classify it as "throw is the only oracle" (below) unless the constructor is known trivial.
  2. Claim *StepDefinitions.cs with Reqnroll step attributes as test starts, and widen globs to UnitTest*.cs (or detect test files by attribute presence).
  3. Narrow check/ensure/validate to call shapes that are assertions (for example a call on an assertion receiver or an Assert*/Should*-named extension), not any identifier containing the word.
  4. Add a third class beside zero-assertion: "throw is the only oracle" (a smoke or operational test that passes if nothing throws; weak, can fail), so users stop deleting tests that guard config and DI wiring.
  5. Report an early return; that precedes every assertion as a finding (the test reports Passed instead of Skipped).

Files: plugins/testing/skills/audit/adapters/cs-xunit.yaml (and cs-nunit.yaml, cs-mstest.yaml which extend it), plugins/testing/skills/audit/scripts/cant-fail-scan.awk, plugins/testing/skills/audit/SKILL.md (rule table), plugins/testing/skills/test-value/SKILL.md (taxonomy keyed to rule ids), plugins/testing/skills/audit/scripts/cant-fail-scan.test.sh.

Acceptance criteria

  • Each "not flagged" or "not enumerated" row in the table above produces a finding on a fixture, and each new rule has a fixture case that fails before the change.
  • Assert.True(true) in a Reqnroll [Then] step in *StepDefinitions.cs is flagged as cannot-fail.
  • A test calling a production method named CheckX() and asserting nothing else is still treated as asserting only if the call is an assertion shape; a print-only test with Check in its name is flagged.
  • Zero-assertion and "throw is the only oracle" are reported under different rule ids.
  • Existing cant-fail-scan.test.sh cases pass; precision on the existing corpus does not drop (check-corpus-grid.sh).

Constraints and gotchas

  • A trial-license guard (catch (Exception ex) when (IsTrialFailure(ex))) is legitimate and must not be flagged.
  • xUnit v2 has no Assert.Skip; the early-return finding's suggested action must not assume one (offer xUnit v3's Assert.Skip or a skip package as options).
  • Hook false-positive fixes need a stay-quiet test that fails before the fix (docs/conventions/hook-precision/README.md); the PostToolUse scan hook runs the same scanner.
  • Version bump and CHANGELOG entry for testing.

Context

Source: local handoff item 20261003-024929-testing-cleanup-and-audit-full-dotnet-support.md, gap 3.

Activity

  1. added
    priority: highSignificant impact, or blocks an imminent release; staff this cycle.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    on Oct 3, 2026
  2. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor

    This was generated by AI during work-loop.

    Approve this for autonomous dispatch, or keep it for a human?

    The work-loop admission gate human-gates feature-shaped C3 work (work-class: scoped, feature-shaped title). The lane removed agent-ready and applied needs-human. To dispatch it, an operator flips it back through /work-items:attend-queue.

    Lane: work-loop, instance melo-worker-1, cycle 1.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Oct 4, 2026
  4. self-assigned this
    on Oct 4, 2026
  5. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor
    No description provided.
  6. added
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    and removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 4, 2026
  7. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor

    This was generated by AI during attend-queue.

    Operator direction (2026-10-04): "go with the recommendations." This records that decision.

    Operator decision: C3 (scoped), feature-shaped, priority high; approved for autonomous dispatch with the decisions below. Every row of this issue's table still reproduces on testing 0.22.6.

    Land as four small PRs, in order:

    • A (bug fix):
      • Stop the test's own declaration line counting as an assertion. Today a method name containing "Check" suppresses zero-assertion (cant-fail-scan.awk:1868-1869 feeding :1694).
      • Check one-line C# bodies for weak-oracle, snapshot-only and inert-assertion (:1707, :1710 skip them today).
    • B: add xUnit Assert.True(true) and typeof/nameof constant oracles to the cs-xunit inert list, as cs-mstest already does for IsTrue(true). No new rule id. typeof(A).IsAssignableFrom(typeof(B)) and IsType<Base> can fail, so they stay out of inert.
    • C: claim Reqnroll [Then] step methods only, never [Given] or [When], plus UnitTest*.cs globs. Regenerate the hook filters (gen-hook-filters.sh).
    • D: a "throw is the only oracle" rule id plus the early-return finding, report-only. It must not change what --check gates. Zero-assertion keeps gating bodies with no call. Update tier_of, the cant-fail-scan.sh case lists, the corpus GRID and the detector-findings README row.

    Decisions:

    • Do not narrow the generous check/ensure/validate token. That choice is documented in the audit SKILL.md Gotchas. Fix the name leak (A) instead.
    • Acceptance criterion 5 is rewritten to "corpus good files stay silent in cant-fail-scan.test.sh". check-corpus-grid.sh checks grid structure, not precision.

    Research: attend-queue slice intake-6040.

  8. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor
    No description provided.
  9. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor

    This was generated by AI during work-loop.

    Resume point (work-loop lane stopping, 2026-10-04). Merged: PR A #6213 (testing 0.22.9) and PR B #6216 (testing 0.22.10). Remaining: PR C (Reqnroll [Then] step methods, UnitTest*.cs globs, hook-filter regeneration) and PR D (a throw-is-the-only-oracle rule id, the early-return finding report-only, tier_of, the case lists, GRID and the README row), as split in the operator decision above. The lane released its lease.

    Open review notes carried from B, not yet decided:

    • MSTest IsNotNull(typeof/nameof) and the Assert.Equal/NotEqual/Same typeof forms were left out of B. MSTest's own inert list replaces the shared one, so MSTest still reports that shape as weak-oracle. Decide: follow-up or drop.
    • The shared inert-assertion:cs remedy text now names typeof/nameof constants on MSTest and NUnit findings too.
    • parity-check's regex dump omits R_INERT, so regex parity does not exercise inert patterns; corpus fixtures cover them.

    Related follow-ups filed: #6211 (the same name-as-assertion leak in Python and Go), #6218 (per-statement oracle judging). For C and D, plan first (medium scope): read B's diff on main and the cant-fail-scan corpus layout before dispatch.

  10. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    MemberAuthor
    No description provided.
  11. kyle-sexton commented on Oct 11, 2026

    @kyle-sexton
    MemberAuthor

    Slice C (the last remaining slice) is up as draft PR #7049 on branch feat/6040-reqnroll-then-test-start.

    • A Reqnroll [Then] step is now a test start in cs-xunit, cs-nunit and cs-mstest; [Given] and [When] are not. The UnitTest*.cs/*StepDefinitions.cs globs and hook filters had already landed (gen-hook-filters.sh --check is clean).
    • Corpus cases: a zero-assertion [Then] under each adapter, an Assert.True(true) [Then], and a silent step file with [Given]/[When]. The four new bad cases fail against main's adapters and pass with the change; cant-fail-scan.test.sh (1292 checks) and affected-tests.sh --base origin/main --run pass.
    • The PR also closes feat(testing): read Reqnroll [Then] steps as test starts in the cannot-fail scan #6385, which tracks the same [Then] slice.

    Left for a human: the carried review note from B (MSTest IsNotNull(typeof/nameof) and the Assert.Equal/NotEqual/Same typeof forms): follow-up or drop.


    Generated by Claude Code

  12. kyle-sexton commented on Oct 11, 2026

    @kyle-sexton
    MemberAuthor

    Handoff note for whoever picks this up next. The overnight Claude run has stopped, and Kyle is moving issue work to Cursor.

    Where a branch is listed it is pushed, so it can be taken over as is. A draft PR shows a red ci-status by design until it is marked ready.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: highSignificant impact, or blocks an imminent release; staff this cycle.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions