# Ivanti Queue Clear Completed Bugfix Design ## Overview The "Clear Completed" endpoint (`DELETE /api/ivanti/todo-queue/completed`) fails with a 500 error when completed queue items have associated rows in the `jira_ticket_queue_items` junction table. The foreign key constraint on `queue_item_id REFERENCES ivanti_todo_queue(id)` lacks `ON DELETE CASCADE`, so PostgreSQL rejects the deletion. The fix wraps the operation in a transaction that deletes junction table references before deleting the queue items themselves. ## Glossary - **Bug_Condition (C)**: The condition that triggers the bug — when one or more completed queue items have associated rows in `jira_ticket_queue_items` - **Property (P)**: The desired behavior — all completed items (with or without junction table links) are deleted atomically, along with their junction table references - **Preservation**: Existing behavior for items without junction table links, empty result sets, and non-complete items must remain unchanged - **`pool`**: The PostgreSQL connection pool exported from `backend/db.js` - **`jira_ticket_queue_items`**: Junction table linking `jira_tickets` to `ivanti_todo_queue` items (created by `add_multi_item_jira_ticket.js` migration) - **`ivanti_todo_queue`**: Main queue table storing user work items with `status` column (`pending`, `complete`) ## Bug Details ### Bug Condition The bug manifests when a user clicks "Clear Completed" and at least one of their completed queue items has a row in `jira_ticket_queue_items` referencing it. The current handler issues a bare `DELETE FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'` which PostgreSQL rejects because child rows exist in the junction table. **Formal Specification:** ``` FUNCTION isBugCondition(input) INPUT: input of type ClearCompletedRequest OUTPUT: boolean completedItems := SELECT id FROM ivanti_todo_queue WHERE user_id = input.userId AND status = 'complete' linkedItems := SELECT queue_item_id FROM jira_ticket_queue_items WHERE queue_item_id IN completedItems RETURN linkedItems.length > 0 END FUNCTION ``` ### Examples - User has 3 completed items, 2 have junction table links → DELETE fails with FK violation, 0 items deleted (bug) - User has 1 completed item with a junction table link → DELETE fails, item remains (bug) - User has 5 completed items, all have junction table links → DELETE fails, all remain (bug) - User has 3 completed items, none have junction table links → DELETE succeeds (no bug, preservation case) ## Expected Behavior ### Preservation Requirements **Unchanged Behaviors:** - Completed items without junction table links are deleted directly and a success response is returned - When no completed items exist, the endpoint returns `{ message: 'Completed items cleared.', deleted: 0 }` - Pending/in-progress items are never touched regardless of junction table links - The `jira_tickets` table is never modified (tickets persist independently of queue items) - Auth middleware (`requireAuth`, `requireGroup`) continues to enforce access control - Response shape remains `{ message: string, deleted: number }` **Scope:** All inputs where `isBugCondition` returns false should be completely unaffected by this fix. This includes: - Users with no completed items - Users with completed items that have no junction table references - Any request targeting non-complete statuses - Mouse/UI interactions unrelated to the "Clear Completed" button ## Hypothesized Root Cause Based on the bug description and code inspection, the root cause is confirmed: 1. **Missing cascade handling in application code**: The `DELETE FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'` query does not account for the FK constraint added by the `add_multi_item_jira_ticket.js` migration. The junction table was added after the original endpoint was written. 2. **FK constraint without ON DELETE CASCADE**: The migration creates `queue_item_id INTEGER NOT NULL REFERENCES ivanti_todo_queue(id)` without specifying `ON DELETE CASCADE`. This is a deliberate design choice (junction table links should be explicitly managed), but the delete endpoint was never updated to handle it. 3. **No transaction wrapping**: The current handler uses a single query without a transaction. Even if it attempted to delete junction rows first, without a transaction there would be a race condition window. ## Correctness Properties Property 1: Bug Condition - Clear Completed With Junction Table Links _For any_ set of completed queue items belonging to a user where at least one item has associated rows in `jira_ticket_queue_items`, the fixed clear completed operation SHALL delete all associated `jira_ticket_queue_items` rows first, then delete all completed queue items for that user, atomically within a transaction, and return a success response with the correct deleted count. **Validates: Requirements 2.1, 2.2** Property 2: Preservation - Clear Completed Without Junction Table Links _For any_ set of completed queue items belonging to a user where NO items have associated rows in `jira_ticket_queue_items` (or where no completed items exist at all), the fixed clear completed operation SHALL produce the same result as the original function — deleting all completed items and returning a success response with the correct deleted count. **Validates: Requirements 3.1, 3.2, 3.3** ## Fix Implementation ### Changes Required **File**: `backend/routes/ivantiTodoQueue.js` **Function**: `router.delete('/completed', ...)` handler **Specific Changes**: 1. **Acquire a dedicated client from the pool**: Replace `pool.query(...)` with `pool.connect()` to get a client that supports transactions. 2. **Begin a transaction**: Issue `BEGIN` before any data-modifying queries. 3. **Select completed item IDs**: Query `SELECT id FROM ivanti_todo_queue WHERE user_id = $1 AND status = 'complete'` to get the set of IDs to delete. 4. **Early exit for empty set**: If no completed items exist, commit and return `{ deleted: 0 }` immediately. 5. **Delete junction table references**: Issue `DELETE FROM jira_ticket_queue_items WHERE queue_item_id = ANY($1::int[])` with the collected IDs. 6. **Delete queue items by ID**: Issue `DELETE FROM ivanti_todo_queue WHERE id = ANY($1::int[])` using the same ID set (more precise than re-filtering by status). 7. **Commit transaction**: Issue `COMMIT` on success. 8. **Rollback on error**: Wrap in try/catch, issue `ROLLBACK` on any failure, then return 500. 9. **Release client**: Always release the client back to the pool in a `finally` block. ## Testing Strategy ### Validation Approach The testing strategy follows a two-phase approach: first, surface counterexamples that demonstrate the bug on unfixed code, then verify the fix works correctly and preserves existing behavior. ### Exploratory Bug Condition Checking **Goal**: Surface counterexamples that demonstrate the bug BEFORE implementing the fix. Confirm the FK violation root cause. **Test Plan**: Mock the `pool.connect()` / `pool.query()` pattern to simulate the FK constraint violation. Write tests that attempt to clear completed items when junction table references exist and assert the operation fails on unfixed code. **Test Cases**: 1. **Single linked item**: One completed item with a junction table reference — DELETE fails (will fail on unfixed code) 2. **Mixed linked/unlinked items**: Some completed items have links, some don't — DELETE fails for all (will fail on unfixed code) 3. **All items linked**: Every completed item has junction table references — DELETE fails (will fail on unfixed code) 4. **Multiple links per item**: One completed item with multiple junction table rows — DELETE fails (will fail on unfixed code) **Expected Counterexamples**: - The simple DELETE query throws a FK violation error - The catch block returns 500 and no items are deleted - Possible cause confirmed: missing junction table cleanup before parent row deletion ### Fix Checking **Goal**: Verify that for all inputs where the bug condition holds, the fixed function produces the expected behavior. **Pseudocode:** ``` FOR ALL input WHERE isBugCondition(input) DO result := clearCompleted_fixed(input) ASSERT result.status = 200 ASSERT result.body.deleted = count(completedItems) ASSERT jira_ticket_queue_items has no rows for deleted IDs ASSERT ivanti_todo_queue has no completed rows for user ASSERT pending items unchanged ASSERT jira_tickets table unchanged END FOR ``` ### Preservation Checking **Goal**: Verify that for all inputs where the bug condition does NOT hold, the fixed function produces the same result as the original function. **Pseudocode:** ``` FOR ALL input WHERE NOT isBugCondition(input) DO ASSERT clearCompleted_original(input) = clearCompleted_fixed(input) END FOR ``` **Testing Approach**: Property-based testing is recommended for preservation checking because: - It generates many test cases automatically across the input domain - It catches edge cases (empty sets, single items, large batches) - It provides strong guarantees that behavior is unchanged for all non-buggy inputs **Test Plan**: Observe behavior on UNFIXED code for cases without junction table links (these succeed today), then write property-based tests capturing that behavior. **Test Cases**: 1. **No completed items**: Verify returns `{ deleted: 0 }` — same as before 2. **Completed items without links**: Verify all are deleted and count is correct — same as before 3. **Pending items untouched**: Verify non-complete items are never affected — same as before 4. **Response shape preserved**: Verify `{ message, deleted }` structure unchanged ### Unit Tests - Mock `pool.connect()` and verify correct query sequence within transaction (BEGIN → SELECT → DELETE junction → DELETE queue → COMMIT) - Verify ROLLBACK is called on any query failure - Verify client is always released in finally block - Test edge case: empty completed set triggers early COMMIT and returns `{ deleted: 0 }` ### Property-Based Tests - Generate random sets of queue items (mix of pending/complete, with/without junction links) and verify the transaction deletes exactly the right rows - Generate random configurations without junction links and verify identical behavior to original code - Generate random user IDs and verify isolation (one user's clear doesn't affect another's items) ### Integration Tests - End-to-end test with actual FK constraints: insert queue items + junction rows, call endpoint, verify both tables cleaned up - Verify atomicity: if the junction DELETE succeeds but queue DELETE fails, nothing is committed - Verify the endpoint still works for the simple case (no junction rows) **Test file**: `backend/__tests__/ivanti-queue-clear-completed-fix.test.js`