Skip to content

Lists store hardening: list-scoped reorder + atomic position allocation - #2265

Merged
jaylfc merged 2 commits into
devfrom
exec/tsk-237k2v
Aug 3, 2026
Merged

Lists store hardening: list-scoped reorder + atomic position allocation#2265
jaylfc merged 2 commits into
devfrom
exec/tsk-237k2v

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): Lists store hardening: list-scoped reorder + atomic position allocation

Autonomous build of board card tsk-237k2v.

reorder_entries now requires list_id, scopes UPDATE by list_id,
and returns False (rolling back) when any supplied entry id does
not belong to that list, preventing silent corruption of sibling
lists in the same project.

add_entry allocates position atomically inside the INSERT via
COALESCE((SELECT MAX(position)+1 ...), 0) when position is None,
eliminating the concurrent-duplicate-position race that occurred
when MAX(position)+1 was read in a separate query.

Files:
tests/projects/test_lists_store.py | 72 +++++++++++++++++++++++++++++++++++++
tinyagentos/projects/lists_store.py | 49 +++++++++++++++++--------
2 files changed, 106 insertions(+), 15 deletions(-)

Summary by CodeRabbit

  • Bug Fixes

    • Reordering entries is now limited to the selected list, preventing changes to entries in other lists.
    • Reordering safely rolls back when an invalid entry is provided.
    • Concurrently added entries now receive distinct positions.
  • Improvements

    • Entries added without an explicit position are automatically placed at the end of their list.
    • Reordering now reports whether the operation succeeded.

reorder_entries now requires list_id, scopes UPDATE by list_id,
and returns False (rolling back) when any supplied entry id does
not belong to that list, preventing silent corruption of sibling
lists in the same project.

add_entry allocates position atomically inside the INSERT via
COALESCE((SELECT MAX(position)+1 ...), 0) when position is None,
eliminating the concurrent-duplicate-position race that occurred
when MAX(position)+1 was read in a separate query.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7af22876-bfc3-4a67-be05-6a666ef2f468

📥 Commits

Reviewing files that changed from the base of the PR and between dfc9651 and 4d5e21e.

📒 Files selected for processing (1)
  • tests/projects/test_lists_store.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/projects/test_lists_store.py

📝 Walkthrough

Walkthrough

Changes

The store now assigns omitted entry positions within the target list and project. Reordering requires list_id, rejects entries outside that list, rolls back failed updates, and returns success status. Tests cover concurrency and cross-list protection.

List ordering

Layer / File(s) Summary
Concurrent position assignment
tinyagentos/projects/lists_store.py, tests/projects/test_lists_store.py
add_entry uses list-scoped SQL for omitted positions. Tests verify concurrent inserts receive distinct positions.
List-scoped reordering
tinyagentos/projects/lists_store.py, tests/projects/test_lists_store.py
reorder_entries scopes updates by project and list, rolls back when an update affects no rows, and returns False; successful operations return True. Tests cover the updated call and cross-list rejection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: list-scoped reordering and atomic position allocation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-237k2v

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: The changes introduce a race condition in auto-position assignment, a test that doesn't exercise the new code path, and a breaking API change without full test coverage.

  • tests/projects/test_lists_store.py:276 - test_concurrent_add_entry_distinct_positions patches _get_next_position but the new add_entry implementation bypasses this method entirely (uses SQL subquery), so the test validates the old code path, not the new one

  • tinyagentos/projects/lists_store.py:154-167 - Auto-position INSERT uses SELECT MAX(position) + 1 subquery which has the same race condition as the old _get_next_position; concurrent inserts can still produce duplicate positions

  • tinyagentos/projects/lists_store.py:261-271 - reorder_entries returns False on first missing entry with rollback, but fails fast without reporting which entry failed; no test covers partial failure with mixed valid/invalid entries

  • tinyagentos/projects/lists_store.py:258 - reorder_entries signature changed to require list_id but only one existing test (test_reorder_entries) was updated; other callers (if any) will break

  • tinyagentos/projects/lists_store.py:154-167 - Duplicate list_id, project_id parameters in auto-position INSERT (positions 10,11 and 12,13) is error-prone; consider using named parameters or restructuring
    VERDICT: The changes introduce a race condition in auto-position assignment, a test that doesn't exercise the new code path, and a breaking API change without full test coverage.

  • tests/projects/test_lists_store.py:276 - test_concurrent_add_entry_distinct_positions patches _get_next_position but the new add_entry implementation bypasses this method entirely (uses SQL subquery), so the test validates the old code path, not the new one

  • tinyagentos/projects/lists_store.py:154-167 - Auto-position INSERT uses SELECT MAX(position) + 1 subquery which has the same race condition as the old _get_next_position; concurrent inserts can still produce duplicate positions

  • tinyagentos/projects/lists_store.py:261-271 - reorder_entries returns False on first missing entry with rollback, but fails fast without reporting which entry failed; no test covers partial failure with mixed valid/invalid entries

  • tinyagentos/projects/lists_store.py:258 - reorder_entries signature changed to require list_id but only one existing test (test_reorder_entries) was updated; other callers (if any) will break

  • tinyagentos/projects/lists_store.py:154-167 - Duplicate list_id, project_id parameters in auto-position INSERT (positions 10,11 and 12,13) is error-prone; consider using named parameters or restructuring

Automated first-pass review by the nemotron-ultra-kilo lane. The lead still reviews before merge.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden lists store: list-scoped reorder + atomic position allocation

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Scope reorders by list_id and fail fast on cross-list entry IDs to prevent corruption.
• Allocate implicit positions inside INSERT to avoid concurrent duplicate positions.
• Add regression tests for sibling-list reorder safety and concurrent inserts.
Diagram

graph TD
  T["tests/projects/test_lists_store.py"] --> S["ListsStore"]
  S --> AE["add_entry()"] --> INS["INSERT (atomic position)"] --> DB[("project_list_entries")]
  S --> RE["reorder_entries()"] --> UPD["UPDATE (scoped by list_id)"] --> DB
  RE --> D{"rowcount==0?"} --> RB["ROLLBACK + False"]
  D --> CM["COMMIT + True"]
  subgraph Legend
    direction LR
    _mod["Module/Function"] ~~~ _db[("Database")] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. DB uniqueness constraint + retry on conflict
  • ➕ Hard guarantee against duplicate (project_id, list_id, position) even under races
  • ➕ Makes correctness independent of application-level allocation logic
  • ➖ Requires schema migration and handling constraint violations
  • ➖ Retry loop adds complexity and may need backoff under contention
2. Single-statement reorder with CASE + pre-validation
  • ➕ Can validate all ids belong to list in one query and update in one statement
  • ➕ Fewer round trips; more clearly atomic if wrapped in a transaction
  • ➖ More complex SQL; harder to maintain and test
  • ➖ May hit SQL parameter limits for large reorder batches

Recommendation: The PR’s approach is a solid, minimal hardening: scoping UPDATEs by list_id and using rowcount to trigger rollback prevents silent cross-list corruption, and moving position allocation into the INSERT closes the common read-then-write race window. If position uniqueness is a strict invariant, consider adding a unique constraint (with retry) as an additional safety net, but the current change is a pragmatic improvement without requiring migrations.

Files changed (2) +106 / -15

Bug fix (1) +34 / -15
lists_store.pyMake add_entry position allocation atomic; scope reorder_entries by list_id +34/-15

Make add_entry position allocation atomic; scope reorder_entries by list_id

• Changes add_entry to compute the next position inside the INSERT when position is omitted, avoiding a separate MAX()+1 query race. Updates reorder_entries to require list_id, scope updates by (id, project_id, list_id), and rollback/return False when any update affects zero rows; returns True on successful commit.

tinyagentos/projects/lists_store.py

Tests (1) +72 / -0
test_lists_store.pyAdd regression tests for list-scoped reorder and concurrent adds +72/-0

Add regression tests for list-scoped reorder and concurrent adds

• Updates existing reorder test to pass list_id. Adds a regression test ensuring reordering with a foreign list entry id returns False and does not move the sibling-list entry. Adds a concurrency test to ensure two concurrent add_entry calls without explicit positions persist distinct positions.

tests/projects/test_lists_store.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/projects/test_lists_store.py (1)

280-288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test rollback after a preceding valid update.

This test submits only the sibling-list entry, so no prior update exists to roll back. Submit a first with a changed position, then submit b, and assert that a still has position 0. This verifies the required all-or-nothing behavior.

Proposed test change
     result = await entries_store.reorder_entries(
         project_id="prj-1",
         list_id="lst-A",
-        entries=[{"id": b["id"], "position": 99}],
+        entries=[
+            {"id": a["id"], "position": 1},
+            {"id": b["id"], "position": 99},
+        ],
     )

+    a_after = await entries_store.get_entry(a["id"])
     b_after = await entries_store.get_entry(b["id"])
+    assert a_after["position"] == 0, "prior updates must roll back"
     assert b_after["position"] == 0, "sibling-list entry must not be moved"
     assert result is False, "reorder should signal that id does not belong to list"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/projects/test_lists_store.py` around lines 280 - 288, Update the
reorder_entries test to submit valid entry a with a changed position before
sibling-list entry b in the same request. After the call, assert a remains at
position 0 and b is unchanged, and preserve the assertion that result is False
to verify the operation rolls back all updates when any entry does not belong to
the list.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/projects/test_lists_store.py`:
- Around line 280-288: Update the reorder_entries test to submit valid entry a
with a changed position before sibling-list entry b in the same request. After
the call, assert a remains at position 0 and b is unchanged, and preserve the
assertion that result is False to verify the operation rolls back all updates
when any entry does not belong to the list.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 88085a26-7d64-4d1a-ae1a-e6e739623658

📥 Commits

Reviewing files that changed from the base of the PR and between b09878c and dfc9651.

📒 Files selected for processing (2)
  • tests/projects/test_lists_store.py
  • tinyagentos/projects/lists_store.py

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

VERDICT: Approved with race condition concern in auto-position assignment

  • tinyagentos/projects/lists_store.py:154: Race condition in auto-position SQL — concurrent inserts can get same MAX(position) and produce duplicate positions; test at tests/projects/test_lists_store.py:277 monkeypatches old _get_next_position path, not the new SQL COALESCE path
  • tinyagentos/projects/lists_store.py:154: Consider adding UNIQUE constraint on (list_id, project_id, position) or using SELECT ... FOR UPDATE to serialize
  • tinyagentos/projects/lists_store.py:258: reorder_entries signature changed (added list_id, returns bool) — verify all callers handle new return value
  • tinyagentos/projects/lists_store.py:138-165: Duplicated INSERT SQL — consider refactoring to single statement with conditional position expression

Automated first-pass review by the nemotron-ultra-orB lane. The lead still reviews before merge.

@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Remediation recommended

1. Misleading concurrency test 🐞 Bug ⚙ Maintainability
Description
test_concurrent_add_entry_distinct_positions monkeypatches _get_next_position to widen a race
window, but add_entry no longer calls _get_next_position when position is None, so the patch never
runs and the test’s intent is obscured. The same file also claims implicit positions are assigned
“via _get_next_position”, which is no longer true, making the tests harder to maintain and reason
about.
Code

tests/projects/test_lists_store.py[R299-302]

+        await asyncio.sleep(0)
+        return val
+
+    monkeypatch.setattr(entries_store, "_get_next_position", slow_next_position)
Relevance

●●● Strong

Team often accepts tightening tests when assertions/guards are misleading or vacuous after code
changes.

PR-#1542
PR-#507
PR-#449

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test monkeypatch targets _get_next_position, but the production code path for `position is
None computes the next position directly in the INSERT` statement and does not call
_get_next_position, so the monkeypatch is unused and comments referencing _get_next_position are
inaccurate.

tinyagentos/projects/lists_store.py[141-166]
tests/projects/test_lists_store.py[223-226]
tests/projects/test_lists_store.py[292-324]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`test_concurrent_add_entry_distinct_positions` monkeypatches `ProjectListEntriesStore._get_next_position`, but `add_entry(..., position=None)` now assigns positions inside the SQL `INSERT` and never calls `_get_next_position`. This makes the monkeypatch (and its `asyncio.sleep(0)`) dead code and leaves misleading test comments.

## Issue Context
The store implementation switched implicit position allocation to `COALESCE((SELECT MAX(position)+1 ...), 0)` within the `INSERT`, bypassing `_get_next_position`.

## Fix Focus Areas
- tests/projects/test_lists_store.py[223-226]
- tests/projects/test_lists_store.py[292-324]
- tinyagentos/projects/lists_store.py[141-166]

## Suggested fix
- Update the docstring/comment in `test_add_entries_without_positions_gets_ascending` to describe SQL-based allocation (not `_get_next_position`).
- In `test_concurrent_add_entry_distinct_positions`, either:
 - remove the `_get_next_position` monkeypatch entirely (simplest), or
 - replace it with instrumentation that actually affects the current code path (e.g., patching/observing the DB execute boundary), if you still want a deterministic concurrency regression gate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +299 to +302
await asyncio.sleep(0)
return val

monkeypatch.setattr(entries_store, "_get_next_position", slow_next_position)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Misleading concurrency test 🐞 Bug ⚙ Maintainability

test_concurrent_add_entry_distinct_positions monkeypatches _get_next_position to widen a race
window, but add_entry no longer calls _get_next_position when position is None, so the patch never
runs and the test’s intent is obscured. The same file also claims implicit positions are assigned
“via _get_next_position”, which is no longer true, making the tests harder to maintain and reason
about.
Agent Prompt
## Issue description
`test_concurrent_add_entry_distinct_positions` monkeypatches `ProjectListEntriesStore._get_next_position`, but `add_entry(..., position=None)` now assigns positions inside the SQL `INSERT` and never calls `_get_next_position`. This makes the monkeypatch (and its `asyncio.sleep(0)`) dead code and leaves misleading test comments.

## Issue Context
The store implementation switched implicit position allocation to `COALESCE((SELECT MAX(position)+1 ...), 0)` within the `INSERT`, bypassing `_get_next_position`.

## Fix Focus Areas
- tests/projects/test_lists_store.py[223-226]
- tests/projects/test_lists_store.py[292-324]
- tinyagentos/projects/lists_store.py[141-166]

## Suggested fix
- Update the docstring/comment in `test_add_entries_without_positions_gets_ascending` to describe SQL-based allocation (not `_get_next_position`).
- In `test_concurrent_add_entry_distinct_positions`, either:
  - remove the `_get_next_position` monkeypatch entirely (simplest), or
  - replace it with instrumentation that actually affects the current code path (e.g., patching/observing the DB execute boundary), if you still want a deterministic concurrency regression gate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Reviewed at dfc9651, lead-completed at 4d5e21e.

Verification:

  • Stale-replay check clean: branch is one commit on the current dev tip (merge-base b09878c = Add route tests for the skills API (read endpoints) #2264 merge), diff matches the PR stats exactly. Not a replay.
  • Red-first proven locally: against merge-base store code the new tests fail for the right reasons - the concurrency test reproduces the actual defect (both entries persisted position 0), the sibling-list test moves the wrong list's entry. All 17 pass on the branch.
  • Rollback coverage gap (CodeRabbit's nitpick, adjudicated valid): the sibling test never proved a preceding valid update gets undone. Extended it in 4d5e21e - valid update of lst-A entry first, then the mismatch; red-probed by neutering the rollback (fails), green with it.
  • Wired-in check: reorder_entries has no production caller on dev (store API ahead of the route), so the signature change (list_id param, bool return) breaks nothing; whoever wires the route gets the scoped version. _get_next_position is now production-dead but stays as the regression trap the concurrency test patches.
  • Kilo red is the known rate-limit noise (summary says 'Assistant request was rate limited'), not a finding. CodeRabbit: one nitpick, adopted above.

Auto-merge armed on green.

@jaylfc
jaylfc enabled auto-merge (squash) August 3, 2026 04:23
@jaylfc
jaylfc merged commit c5a8f3d into dev Aug 3, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant