Handle stale out-of-range positions in Work Item task-list toggles
What does this MR do and why?
Atomic task-list toggles already treat an in-range stale line-content mismatch as an unsuccessful toggle. TaskListToggleService returns a falsey result, IssuableBaseService converts it into a stale-edit signal, and the Work Item GraphQL mutation returns a recoverable conflict with the current Work Item state.
After a concurrent whole-description edit removes the original task line, the stale source position indexes a missing line instead. TaskListToggleService currently calls chomp on that nil value, so the exception bypasses the intended conflict handling.
This MR guards the indexed line before comparing its content. A missing line now uses the same unsuccessful-toggle protocol as an in-range stale mismatch.
Before
stale line position
→ line no longer exists
→ NoMethodError
→ intended conflict handling is bypassedAfter
stale line position
→ normal unsuccessful toggle
→ existing stale/edit-conflict handling
→ current Work Item state is returnedImplementation notes
The production change is intentionally limited to:
return unless line && line.chomp == line_sourceIt does not clamp the source position, toggle a nearby line, rescue arbitrary exceptions, or change the matching and precise/imprecise toggle behavior for existing lines.
The regression coverage exercises the service contract directly and the ordinary Work Item GraphQL request/recovery path.
How to test
Added regression coverage for:
- An out-of-range stale source position in TaskListToggleService, which now returns falsey.
- A Work Item GraphQL toggle after the description is shortened, asserting that the description remains unchanged and the conflict payload includes the current description.
The following commands passed in the isolated GDK runtime:
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/services/task_list_toggle_service_spec.rb:221
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/services/task_list_toggle_service_spec.rb:210 spec/services/task_list_toggle_service_spec.rb:221
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/services/task_list_toggle_service_spec.rb
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/requests/api/graphql/mutations/work_items/update_spec.rb:307
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/requests/api/graphql/mutations/work_items/update_spec.rb:229 spec/requests/api/graphql/mutations/work_items/update_spec.rb:276 spec/requests/api/graphql/mutations/work_items/update_spec.rb:296 spec/requests/api/graphql/mutations/work_items/update_spec.rb:312
CI=true RAILS_ENV=test VITE_ENABLED=false mise x -- bin/rspec spec/requests/api/graphql/mutations/work_items/update_spec.rb
CI=true RAILS_ENV=test mise x -- bundle exec rubocop app/services/task_list_toggle_service.rb spec/services/task_list_toggle_service_spec.rb spec/requests/api/graphql/mutations/work_items/update_spec.rb --parallel
git diff --checkResults:
- Focused service spec: 1 example, 0 failures
- Service in-range/out-of-range set: 2 examples, 0 failures
- Full service spec: 26 examples, 0 failures
- Focused Work Item regression: 1 example, 0 failures
- Work Item happy/fallback/in-range/out-of-range set: 4 examples, 0 failures
- Full Work Item update mutation spec: 131 examples, 0 failures
- RuboCop: 3 files inspected, no offenses
- Diff check: clean
Rollout and stable scope
The affected GraphQL feature is experimental, introduced in 19.2, and is default-disabled with GitLab.com derisking. Stable v19.3.1 contains the same unsafe service line.
Risk
Risk is small and localized, but not zero because this changes the service result for one invalid current-state lookup. Existing service coverage constrains valid precise and imprecise positions, matching and mismatching source lines, already-toggled inputs, CRLF content, blockquotes, tables, and multibyte text. The full Work Item mutation spec constrains the surrounding API behavior.
References
Closes #628441
AI assistance
AI tooling was used to assist with investigation and drafting. The contributor independently reviewed the implementation, reproduced the bug locally, verified the regression test and fix, and takes responsibility for the submitted changes.