Sdlc/Minitest/TautologicalAssertion: resolve last lvar assignment and carve out determinism pins #1357

Closed
opened 2026-10-25 21:23:08 +00:00 by jared · 5 comments
Owner

Problem

Rewritten 2026-10-27 from docs/lint-lessons/analyses/sdlc-minitest-tautological-assertion.md.

Sdlc/Minitest/TautologicalAssertion resolves a variable to its first assignment, so first = C.new.next; first = 5; assert_equal first, C.new.next is flagged and first = 5; first = C.new.next; ... is missed. The message tells the author of a determinism test to pin one output, which deletes the property the test states. The earlier carve-out (skip identical X.new(args) calls) would stop the cop from firing on its own must-fire fixture Subject.new(same_args), because a determinism pin and that fixture are the same shape.

Approach

Required work:

  1. In plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/same_subject_calls.rb, resolve the variable to the last assignment that ends before the assertion — check: absent each_descendant(:lvasgn)&.find in plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/same_subject_calls.rb
  2. Do not report the offense when the same test method has a refute_equal of the same method on constructions with different receiver source — check: present refute_equal in plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/*.rb
  3. Change the message: give the known expected value, or, for a determinism property, add a refute_equal with a different input — check: present refute_equal in plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion.rb
  4. Add fixtures to plugins/os-sdlc/tests/cops/tautological_assertion_test.rb: must-fire for a late assignment to the call and for a pin alone; must-pass for a reassigned variable and for a pin plus a sensitivity assertion.

Acceptance criteria

  1. Given first = C.new.next; first = 5; assert_equal first, C.new.next, when the cop runs, then it reports no offense.
  2. Given first = 5; first = C.new.next; assert_equal first, C.new.next, when the cop runs, then it reports one offense.
  3. Given assert_equal S.new(seed: 1).call, S.new(seed: 1).call alone, when the cop runs, then it reports one offense whose message names refute_equal.
  4. Given that line plus refute_equal S.new(seed: 1).call, S.new(seed: 2).call in the same method, when the cop runs, then it reports no offense.
  5. Given the existing Subject.new(same_args) fixture, when the cop runs, then it still reports one offense.

Owner sketch

class RuboCop::Cop::Sdlc::Minitest::TautologicalAssertion::SameSubjectCalls
  def match?(left, right) = ...          # reaching assignment
  def sensitivity_checked?(method_name) = ...
end

Origin

  • Trigger: #1325 zero-false-positive audit of every Sdlc cop.
  • Improvised this session: none
  • Chain: false positives on a reassigned lvar and a determinism pin ← resolver takes the first assignment and the matcher ignores constructor arguments ← DESIGN (same_subject_calls.rb:55)
  • Root candidate: none found; searched tautolog, same_subject, SameSubject, assert_equal
  • Where: plugins/os-sdlc/lib/os_sdlc/cops/same_subject_calls.rb
  • Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl
## Problem Rewritten 2026-10-27 from docs/lint-lessons/analyses/sdlc-minitest-tautological-assertion.md. `Sdlc/Minitest/TautologicalAssertion` resolves a variable to its first assignment, so `first = C.new.next; first = 5; assert_equal first, C.new.next` is flagged and `first = 5; first = C.new.next; ...` is missed. The message tells the author of a determinism test to pin one output, which deletes the property the test states. The earlier carve-out (skip identical `X.new(args)` calls) would stop the cop from firing on its own must-fire fixture `Subject.new(same_args)`, because a determinism pin and that fixture are the same shape. ## Approach Required work: 1. In `plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/same_subject_calls.rb`, resolve the variable to the last assignment that ends before the assertion — check: absent `each_descendant(:lvasgn)&.find` in `plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/same_subject_calls.rb` 2. Do not report the offense when the same test method has a `refute_equal` of the same method on constructions with different receiver source — check: present `refute_equal` in `plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion/*.rb` 3. Change the message: give the known expected value, or, for a determinism property, add a `refute_equal` with a different input — check: present `refute_equal` in `plugins/os-sdlc/lib/os_sdlc/cops/minitest/tautological_assertion.rb` 4. Add fixtures to `plugins/os-sdlc/tests/cops/tautological_assertion_test.rb`: must-fire for a late assignment to the call and for a pin alone; must-pass for a reassigned variable and for a pin plus a sensitivity assertion. ## Acceptance criteria 1. Given `first = C.new.next; first = 5; assert_equal first, C.new.next`, when the cop runs, then it reports no offense. 2. Given `first = 5; first = C.new.next; assert_equal first, C.new.next`, when the cop runs, then it reports one offense. 3. Given `assert_equal S.new(seed: 1).call, S.new(seed: 1).call` alone, when the cop runs, then it reports one offense whose message names `refute_equal`. 4. Given that line plus `refute_equal S.new(seed: 1).call, S.new(seed: 2).call` in the same method, when the cop runs, then it reports no offense. 5. Given the existing `Subject.new(same_args)` fixture, when the cop runs, then it still reports one offense. ## Owner sketch ```ruby class RuboCop::Cop::Sdlc::Minitest::TautologicalAssertion::SameSubjectCalls def match?(left, right) = ... # reaching assignment def sensitivity_checked?(method_name) = ... end ``` ## Origin - Trigger: #1325 zero-false-positive audit of every Sdlc cop. - Improvised this session: none - Chain: false positives on a reassigned lvar and a determinism pin ← resolver takes the first assignment and the matcher ignores constructor arguments ← DESIGN (`same_subject_calls.rb:55`) - Root candidate: none found; searched tautolog, same_subject, SameSubject, assert_equal - Where: plugins/os-sdlc/lib/os_sdlc/cops/same_subject_calls.rb - Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl
Author
Owner

Skeptic DROP (depth-1): proposal to enable stock Lint/UselessAssignment (disabled by DisabledByDefault; 4 unclassified hits in plugins/os-sdlc/tests). Revisit if a hit proves a real defect. Draft: docs/lint-lessons/analyses/sdlc-minitest-tautological-assertion.md question (b).

Skeptic DROP (depth-1): proposal to enable stock Lint/UselessAssignment (disabled by DisabledByDefault; 4 unclassified hits in plugins/os-sdlc/tests). Revisit if a hit proves a real defect. Draft: docs/lint-lessons/analyses/sdlc-minitest-tautological-assertion.md question (b).
Author
Owner

Body rewritten 2026-10-27 from the false-positive process; analysis and lint lesson merged to main (docs/lint-lessons/analyses/, plugins/os-sdlc/lint-lessons/).

Body rewritten 2026-10-27 from the false-positive process; analysis and lint lesson merged to main (docs/lint-lessons/analyses/, plugins/os-sdlc/lint-lessons/).
Author
Owner

Work started on branch ticket-1357 (os-sdlc implement pipeline).

Work started on branch ticket-1357 (os-sdlc implement pipeline).
Author
Owner

Resolution

Done: TautologicalAssertion resolves a local variable to the last assignment before the assertion; a determinism pin is not reported when the same method has refute_equal of that method on constructions with different source; the message names refute_equal; nil guard for assertions outside a def; lesson signal names the carve-out. Merged 49e185f1, pushed.

Evidence: plugins/os-sdlc/tests/cops/tautological_assertion_test.rb 13 runs green (AC1-5 plus same-input refute_equal still fires and block-form fires); ruby plugins/os-sdlc/tests/all.rb 2817 runs 0 failures 69.0s; rubocop clean on changed files; cop sweep over plugins/ 0 hits before and after.

Follow-ups: Pipeline session collision (implementation 200 abandoned by concurrent 201) captured via os-backlog:capture.

## Resolution **Done:** TautologicalAssertion resolves a local variable to the last assignment before the assertion; a determinism pin is not reported when the same method has refute_equal of that method on constructions with different source; the message names refute_equal; nil guard for assertions outside a def; lesson signal names the carve-out. Merged 49e185f1, pushed. **Evidence:** plugins/os-sdlc/tests/cops/tautological_assertion_test.rb 13 runs green (AC1-5 plus same-input refute_equal still fires and block-form fires); ruby plugins/os-sdlc/tests/all.rb 2817 runs 0 failures 69.0s; rubocop clean on changed files; cop sweep over plugins/ 0 hits before and after. **Follow-ups:** Pipeline session collision (implementation 200 abandoned by concurrent 201) captured via os-backlog:capture.
jared closed this issue 2026-10-27 21:44:36 +00:00
Author
Owner

Follow-up: the pipeline session collision is recorded as an occurrence on #1447.

Follow-up: the pipeline session collision is recorded as an occurrence on #1447.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1357
No description provided.