Files
cve-dashboard/.kiro/specs/ivanti-queue-clear-completed-fix/design.md
Jordan Ramos a61d254ff9 Sync .kiro/ from master — v2.2.0 release batch
New specs: archer-template-library, ccp-metrics-view-restructure,
compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date,
compliance-remediation-display-fix, flexible-jira-ticket-creation,
forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix,
multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown

New steering: archer-template-gen.md

Updated: migration-registration-check hook, remediation-plan-history spec,
gitlab-workflow, tech, versioning steering files
2026-06-04 11:27:31 -06:00

11 KiB

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