Diff-test gate loops when a lib file has only scenario-split tests and no mirrored _test.rb #535

Closed
opened 2026-09-08 17:20:52 +00:00 by jared · 3 comments
Owner

Context

Live run of /os-sdlc:implement #525 (session f3ac4424, 2026-09-08): the programmer changed plugins/os/lib/worktree_cli_finish.rb; the diff-test gate mapped it to plugins/os/tests/worktree_cli_finish_test.rb, which did not exist, and raised LoadError through three programmer-repair rounds. The finish suite is scenario-split (worktree_cli_finish_*_test.rb, 12 files). An entry file was added by hand mid-run as a workaround; that workaround stays, this ticket fixes the gate.
Root cause: CandidateTestPaths#resolve (plugins/os-sdlc/lib/os_sdlc/runner/diffed_test_paths.rb:57-62) returns candidates.first without an existence check whenever there is exactly one candidate. The candidate list is two literal strings (mirrored and unnamespaced, line 50-55); there is no glob anywhere. This is the only lib-to-test mapping site in the runner (code-probe 2026-09-08).
Consumers: gate_commands.rb:32-40 builds one require_relative per path; brief_facts.rb:119-129 splats paths into git diff HEAD --. Both expect a flat list of strings, so the fallback must be flattened inside DiffedTestPaths#paths, never leak arrays out.
VERDICT: CREATE (ticket-skeptic, claude-sonnet-5, 2026-09-08): real observed failure, root cause named, chain ends in ADR-0160's mirrored-path contract.

Tasks

  • CandidateTestPaths#resolve(file) returns an Array: the first existing literal candidate as a one-element array; else every file matching the scenario glob for each candidate (<dir>/<name>_*_test.rb, relative to project_root, sorted); else raise HarnessError naming both the literal candidates and the globs tried.
  • Remove the return candidates.first if candidates.one? shortcut; existence is checked in every case.
  • DiffedTestPaths#paths uses flat_map over test_path_for so the result stays a flat, unique list of strings; gate_commands.rb and brief_facts.rb are unchanged.
  • Tests in plugins/os-sdlc/tests/runner/diffed_test_paths_test.rb using DiffedProjectFixture: add a fixture that writes lib/foo.rb with only test/foo_alpha_test.rb and test/foo_beta_test.rb.

Pseudo-Ruby:

def resolve(file)
  existing = candidates.find { |c| exist?(c) }
  return [existing] if existing

  scenarios = candidates.flat_map { |c| Dir.glob(File.join(project_root, scenario_glob(c))) }
                        .map { |p| p.delete_prefix("#{project_root}/") }.sort.uniq
  return scenarios unless scenarios.empty?

  raise HarnessError, "os-sdlc: no test file found for #{file}; tried: #{(candidates + candidates.map { scenario_glob(_1) }).join(', ')}"
end

def scenario_glob(candidate) = candidate.sub(/_test\.rb\z/, "_*_test.rb")

# DiffedTestPaths
def paths
  mapped = diffed.select { |f| covered?(f) }.flat_map { |f| test_path_for(f) }
  (test_files + mapped).uniq
end

Acceptance criteria

  • A changed lib file whose only tests are foo_alpha_test.rb and foo_beta_test.rb yields both paths, sorted, and the diff-test command requires both.
  • A changed lib file with an existing mirrored foo_test.rb yields exactly that one path, even when scenario files also exist.
  • A changed lib file with no mirrored test and no scenario files raises HarnessError whose message lists the two literal candidates and the two globs.
  • DiffedTestPaths#paths never contains a nested array; existing tests in diffed_test_paths_test.rb and gate_commands_test.rb stay green.

Out of scope

  • Changing the mirrored-path contract in ADR-0160; this adds a fallback under it.
  • Removing the hand-added worktree_cli_finish_test.rb entry file in plugins/os.

Blocking edges

Origin

  • Trigger: /os-sdlc:implement #525, programmer-repair dispatches 18-20
  • Improvised this session: added plugins/os/tests/worktree_cli_finish_test.rb, an entry file that requires the scenario files, on branch ticket-525
  • Chain: repair loop ← gate LoadError ← CandidateTestPaths#resolve returns a single candidate unconditionally (source: plugins/os-sdlc/lib/os_sdlc/runner/diffed_test_paths.rb) ← mirrored-path contract from ADR-0160. End: DESIGN (ADR-0160).
  • Root candidate: this ticket
  • Where: OsSdlc::Runner::DiffedTestPaths::CandidateTestPaths
  • Session: f3ac4424-679e-4c92-9c4d-f6280f413050
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f3ac4424-679e-4c92-9c4d-f6280f413050.jsonl
## Context Live run of `/os-sdlc:implement #525` (session f3ac4424, 2026-09-08): the programmer changed `plugins/os/lib/worktree_cli_finish.rb`; the diff-test gate mapped it to `plugins/os/tests/worktree_cli_finish_test.rb`, which did not exist, and raised LoadError through three programmer-repair rounds. The finish suite is scenario-split (`worktree_cli_finish_*_test.rb`, 12 files). An entry file was added by hand mid-run as a workaround; that workaround stays, this ticket fixes the gate. Root cause: `CandidateTestPaths#resolve` (`plugins/os-sdlc/lib/os_sdlc/runner/diffed_test_paths.rb:57-62`) returns `candidates.first` without an existence check whenever there is exactly one candidate. The candidate list is two literal strings (mirrored and unnamespaced, line 50-55); there is no glob anywhere. This is the only lib-to-test mapping site in the runner (code-probe 2026-09-08). Consumers: `gate_commands.rb:32-40` builds one `require_relative` per path; `brief_facts.rb:119-129` splats paths into `git diff HEAD --`. Both expect a flat list of strings, so the fallback must be flattened inside `DiffedTestPaths#paths`, never leak arrays out. VERDICT: CREATE (ticket-skeptic, claude-sonnet-5, 2026-09-08): real observed failure, root cause named, chain ends in ADR-0160's mirrored-path contract. ## Tasks - [ ] `CandidateTestPaths#resolve(file)` returns an Array: the first existing literal candidate as a one-element array; else every file matching the scenario glob for each candidate (`<dir>/<name>_*_test.rb`, relative to project_root, sorted); else raise `HarnessError` naming both the literal candidates and the globs tried. - [ ] Remove the `return candidates.first if candidates.one?` shortcut; existence is checked in every case. - [ ] `DiffedTestPaths#paths` uses `flat_map` over `test_path_for` so the result stays a flat, unique list of strings; `gate_commands.rb` and `brief_facts.rb` are unchanged. - [ ] Tests in `plugins/os-sdlc/tests/runner/diffed_test_paths_test.rb` using `DiffedProjectFixture`: add a fixture that writes `lib/foo.rb` with only `test/foo_alpha_test.rb` and `test/foo_beta_test.rb`. Pseudo-Ruby: ```ruby def resolve(file) existing = candidates.find { |c| exist?(c) } return [existing] if existing scenarios = candidates.flat_map { |c| Dir.glob(File.join(project_root, scenario_glob(c))) } .map { |p| p.delete_prefix("#{project_root}/") }.sort.uniq return scenarios unless scenarios.empty? raise HarnessError, "os-sdlc: no test file found for #{file}; tried: #{(candidates + candidates.map { scenario_glob(_1) }).join(', ')}" end def scenario_glob(candidate) = candidate.sub(/_test\.rb\z/, "_*_test.rb") # DiffedTestPaths def paths mapped = diffed.select { |f| covered?(f) }.flat_map { |f| test_path_for(f) } (test_files + mapped).uniq end ``` ## Acceptance criteria - [ ] A changed lib file whose only tests are `foo_alpha_test.rb` and `foo_beta_test.rb` yields both paths, sorted, and the diff-test command requires both. - [ ] A changed lib file with an existing mirrored `foo_test.rb` yields exactly that one path, even when scenario files also exist. - [ ] A changed lib file with no mirrored test and no scenario files raises `HarnessError` whose message lists the two literal candidates and the two globs. - [ ] `DiffedTestPaths#paths` never contains a nested array; existing tests in `diffed_test_paths_test.rb` and `gate_commands_test.rb` stay green. ## Out of scope - Changing the mirrored-path contract in ADR-0160; this adds a fallback under it. - Removing the hand-added `worktree_cli_finish_test.rb` entry file in plugins/os. ## Blocking edges - Blocks: #537, #538, #539, #540, #541, #542 (the #534 children land after this). ## Origin - Trigger: `/os-sdlc:implement #525`, programmer-repair dispatches 18-20 - Improvised this session: added `plugins/os/tests/worktree_cli_finish_test.rb`, an entry file that requires the scenario files, on branch ticket-525 - Chain: repair loop ← gate LoadError ← `CandidateTestPaths#resolve` returns a single candidate unconditionally (source: plugins/os-sdlc/lib/os_sdlc/runner/diffed_test_paths.rb) ← mirrored-path contract from ADR-0160. End: `DESIGN` (ADR-0160). - Root candidate: this ticket - Where: OsSdlc::Runner::DiffedTestPaths::CandidateTestPaths - Session: f3ac4424-679e-4c92-9c4d-f6280f413050 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f3ac4424-679e-4c92-9c4d-f6280f413050.jsonl
Author
Owner

Work started via /os-sdlc:implement on branch ticket-535 (session 1a3b7fd0-0319-465b-8414-0ab70560de3d). Deviation: main's brief_facts.rb hot-patched with target: (#534) for the run, to be restored before finish.

Work started via /os-sdlc:implement on branch ticket-535 (session 1a3b7fd0-0319-465b-8414-0ab70560de3d). Deviation: main's brief_facts.rb hot-patched with target: (#534) for the run, to be restored before finish.
Author
Owner

Pipeline run implementation 3 (session 1a3b7fd0-0319-465b-8414-0ab70560de3d, branch ticket-535) ended implementation_failed: bound_exhausted:suite-check/fail after three red suite-checks (dispatches 62, 72, 76). Failed step: suite. Gate detail: OsSdlc::Runner::GateCommandsTest#test_lint_present_and_test_lint_nil_for_a_production_only_diff raises HarnessError "no test file found for lib/foo.rb; tried: test/foo_test.rb; and scenario globs: test/foo_*_test.rb" at diffed_test_paths.rb:72 via gate_commands.rb:33. Cause: that pre-existing test's fixture has a production-only diff with no test file at all; the old code returned the nonexistent path silently (the very behavior that caused the #525 LoadError loop), and the new code raises as this ticket specifies. Resolving it needs a test-side change (fixture gains a test file, or the assertion changes), which programmer-repair is forbidden to make, so the repair loop could not converge. Worktree ticket-535 kept with uncommitted changes to diffed_test_paths.rb and diffed_test_paths_test.rb; diff-test and lint gates were green. Decision needed from the user: keep raise (edit the fixture) or return no paths on no-match.

Pipeline run implementation 3 (session 1a3b7fd0-0319-465b-8414-0ab70560de3d, branch ticket-535) ended implementation_failed: bound_exhausted:suite-check/fail after three red suite-checks (dispatches 62, 72, 76). Failed step: suite. Gate detail: OsSdlc::Runner::GateCommandsTest#test_lint_present_and_test_lint_nil_for_a_production_only_diff raises HarnessError "no test file found for lib/foo.rb; tried: test/foo_test.rb; and scenario globs: test/foo_*_test.rb" at diffed_test_paths.rb:72 via gate_commands.rb:33. Cause: that pre-existing test's fixture has a production-only diff with no test file at all; the old code returned the nonexistent path silently (the very behavior that caused the #525 LoadError loop), and the new code raises as this ticket specifies. Resolving it needs a test-side change (fixture gains a test file, or the assertion changes), which programmer-repair is forbidden to make, so the repair loop could not converge. Worktree ticket-535 kept with uncommitted changes to diffed_test_paths.rb and diffed_test_paths_test.rb; diff-test and lint gates were green. Decision needed from the user: keep raise (edit the fixture) or return no paths on no-match.
Author
Owner

Resolution

Done: CandidateTestPaths#resolve now returns the existing mirrored test, else sorted _*_test.rb scenario matches, else raises HarnessError naming the paths and globs tried. Two gate-commands tests that encoded the old return-a-missing-path contract now give their fixture a test file; a new test asserts the raise. Decision: option A (keep the raise, fix the tests), taken by the agent as the memo default.

Evidence: merged to main 717d8a8 (fix) + 8eb1ee5 (history row) on 2026-09-09; os-sdlc suite 1087 runs, 0 failures, 0 errors; rubocop clean on the three changed files; worktree ticket-535 finished and removed; main's brief hot-patch was already restored.

Follow-ups: see follow-up issue #544: Pipeline cannot converge when a red suite-check needs a test-side edit

## Resolution **Done:** CandidateTestPaths#resolve now returns the existing mirrored test, else sorted <name>_*_test.rb scenario matches, else raises HarnessError naming the paths and globs tried. Two gate-commands tests that encoded the old return-a-missing-path contract now give their fixture a test file; a new test asserts the raise. Decision: option A (keep the raise, fix the tests), taken by the agent as the memo default. **Evidence:** merged to main 717d8a8 (fix) + 8eb1ee5 (history row) on 2026-09-09; os-sdlc suite 1087 runs, 0 failures, 0 errors; rubocop clean on the three changed files; worktree ticket-535 finished and removed; main's brief hot-patch was already restored. **Follow-ups:** see follow-up issue #544: Pipeline cannot converge when a red suite-check needs a test-side edit
jared closed this issue 2026-09-09 11:57:37 +00:00
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#535
No description provided.