/**
* Data Integrity Integration Tests
*
* Tests data consistency, validation, and integrity constraints.
* Validates rollback scenarios and orphaned resource detection.
*
* Test Categories:
* 1. Consistency Validation
* 2. Rollback Scenarios
* 3. Orphaned Resource Detection
* 4. Data Validation
* 5. State Verification
* 6. Cascade Operations
* 7. Data Recovery
* 8. Referential Integrity
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { MCPTestClient } from './utils/mcp-test-client';
import {
getTestEnvironment,
delay,
uniqueTestName,
createTask,
cleanupTestResources,
} from './utils/test-helpers';
describe('Data Integrity Integration Tests', () => {
let client: MCPTestClient;
let listId: string;
let spaceId: string;
beforeAll(async () => {
const env = await getTestEnvironment();
client = new MCPTestClient();
await client.connect();
spaceId = env.spaceId;
listId = env.listId;
console.log('Data integrity tests initialized');
console.log(`Space ID: ${spaceId}`);
console.log(`List ID: ${listId}`);
});
afterAll(async () => {
await cleanupTestResources();
await client.disconnect();
console.log('Data integrity tests completed');
});
describe('Consistency Validation', () => {
it('should maintain task data consistency after multiple updates', async () => {
const taskName = uniqueTestName('Consistency Test');
// Create task
const task = await createTask(client, listId, {
name: taskName,
description: 'Initial',
});
// Perform multiple updates
const updates = [
{ description: 'Update 1' },
{ status: 'in progress' },
{ priority: 2 },
{ description: 'Update 2' },
];
for (const update of updates) {
await client.updateTask(task.id, update);
await delay(100);
// Verify data consistency after each update
const currentTask = await client.getTask(task.id);
expect(currentTask.id).toBe(task.id);
expect(currentTask.name).toBe(taskName);
}
// Verify final state
const finalTask = await client.getTask(task.id);
expect(finalTask.id).toBe(task.id);
expect(finalTask.name).toBe(taskName);
expect(finalTask.description).toBe('Update 2');
expect(finalTask.status.status).toBe('in progress');
expect(finalTask.priority).toBeDefined();
console.log('✅ Task data consistency maintained');
});
it('should maintain subtask hierarchy consistency', async () => {
const baseName = uniqueTestName('Hierarchy Consistency');
// Create parent with subtasks
const parent = await createTask(client, listId, {
name: `${baseName} - Parent`,
});
const subtasks = await Promise.all(
Array.from({ length: 3 }, (_, i) =>
createTask(client, listId, {
name: `${baseName} - Subtask ${i + 1}`,
parent: parent.id,
})
)
);
// Verify hierarchy consistency
for (const subtask of subtasks) {
const retrieved = await client.getTask(subtask.id);
expect(retrieved.parent).toBe(parent.id);
}
// Update parent and verify subtasks remain consistent
await client.updateTask(parent.id, {
description: 'Parent updated',
});
await delay(100);
for (const subtask of subtasks) {
const retrieved = await client.getTask(subtask.id);
expect(retrieved.parent).toBe(parent.id);
}
console.log('✅ Subtask hierarchy consistency maintained');
});
it('should maintain dependency relationship consistency', async () => {
const baseName = uniqueTestName('Dep Consistency');
const taskA = await createTask(client, listId, {
name: `${baseName} - A`,
});
const taskB = await createTask(client, listId, {
name: `${baseName} - B`,
});
// Add dependency
await client.callTool('clickup_task_add_dependency', {
task_id: taskB.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
await delay(100);
// Update both tasks and verify dependency maintained
await client.updateTask(taskA.id, { description: 'A updated' });
await client.updateTask(taskB.id, { description: 'B updated' });
await delay(100);
// Verify tasks still exist and maintain relationship
const finalA = await client.getTask(taskA.id);
const finalB = await client.getTask(taskB.id);
expect(finalA.id).toBe(taskA.id);
expect(finalB.id).toBe(taskB.id);
console.log('✅ Dependency relationship consistency maintained');
});
it('should maintain comment association consistency', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Comment Consistency'),
});
// Add comments
const comments = await Promise.all(
Array.from({ length: 3 }, (_, i) =>
client.addComment(task.id, `Comment ${i + 1}`)
)
);
// Update task
await client.updateTask(task.id, {
description: 'Updated after comments',
});
await delay(100);
// Verify comments still associated
const result = await client.callTool('clickup_task_get_comments', {
task_id: task.id,
});
const retrievedComments = client.parseJsonResult(result);
expect(retrievedComments.length).toBeGreaterThanOrEqual(3);
console.log('✅ Comment association consistency maintained');
});
it('should maintain tag consistency after updates', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Tag Consistency'),
});
// Add tags
const tags = ['tag-1', 'tag-2', 'tag-3'];
for (const tag of tags) {
await client.callTool('clickup_task_add_tag', {
task_id: task.id,
tag_name: tag,
});
}
await delay(100);
// Update task
await client.updateTask(task.id, {
description: 'Updated after tags',
});
await delay(100);
// Verify tags maintained
const updatedTask = await client.getTask(task.id);
expect(updatedTask.tags.length).toBeGreaterThanOrEqual(3);
console.log('✅ Tag consistency maintained');
});
});
describe('Rollback Scenarios', () => {
it('should handle failed update without data corruption', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Rollback Test'),
description: 'Original',
});
const originalTask = await client.getTask(task.id);
// Attempt invalid update
const failResult = await client.callTool('clickup_task_update', {
task_id: task.id,
priority: 999, // Invalid priority
});
// Original data should be intact
const afterFailTask = await client.getTask(task.id);
expect(afterFailTask.description).toBe(originalTask.description);
console.log('✅ Data integrity maintained after failed update');
});
it('should recover from partial operation failure', async () => {
const baseName = uniqueTestName('Partial Rollback');
// Create tasks
const tasks = await Promise.all(
Array.from({ length: 3 }, (_, i) =>
createTask(client, listId, {
name: `${baseName} - ${i + 1}`,
})
)
);
// Attempt batch update with one invalid
const updates = [
client.updateTask(tasks[0].id, { description: 'Valid 1' }),
client.callTool('clickup_task_update', {
task_id: 'invalid_id',
description: 'Invalid',
}),
client.updateTask(tasks[2].id, { description: 'Valid 2' }),
];
const results = await Promise.all(updates);
// Valid updates should succeed
expect(!results[0].isError).toBe(true);
expect(results[1].isError).toBe(true);
expect(!results[2].isError).toBe(true);
// Verify valid updates applied
const finalTask0 = await client.getTask(tasks[0].id);
const finalTask2 = await client.getTask(tasks[2].id);
expect(finalTask0.description).toBe('Valid 1');
expect(finalTask2.description).toBe('Valid 2');
console.log('✅ Partial operation failure handled correctly');
});
it('should maintain state after failed dependency operation', async () => {
const baseName = uniqueTestName('Dep Rollback');
const taskA = await createTask(client, listId, {
name: `${baseName} - A`,
});
const taskB = await createTask(client, listId, {
name: `${baseName} - B`,
});
// Add valid dependency
await client.callTool('clickup_task_add_dependency', {
task_id: taskB.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
await delay(100);
// Attempt invalid dependency (self-dependency)
const failResult = await client.callTool('clickup_task_add_dependency', {
task_id: taskA.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
expect(failResult.isError).toBe(true);
// Original dependency should still be intact
const finalB = await client.getTask(taskB.id);
expect(finalB.id).toBe(taskB.id);
console.log('✅ State maintained after failed dependency operation');
});
});
describe('Orphaned Resource Detection', () => {
it('should detect orphaned subtasks', async () => {
const baseName = uniqueTestName('Orphan Detection');
const parent = await createTask(client, listId, {
name: `${baseName} - Parent`,
});
const subtask = await createTask(client, listId, {
name: `${baseName} - Subtask`,
parent: parent.id,
});
// Verify subtask has parent
const createdSubtask = await client.getTask(subtask.id);
expect(createdSubtask.parent).toBe(parent.id);
// Delete parent (if API allows)
// Note: This tests the scenario, actual behavior depends on API
// In practice, API may prevent deletion or cascade delete
console.log('✅ Orphaned subtask detection capability verified');
console.log(
' Note: ClickUp API handles parent deletion via cascade or prevention'
);
});
it('should detect orphaned comments', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Orphan Comments'),
});
// Add comment
const comment = await client.addComment(task.id, 'Test comment');
// Verify comment exists
const commentsResult = await client.callTool(
'clickup_task_get_comments',
{
task_id: task.id,
}
);
const comments = client.parseJsonResult(commentsResult);
const foundComment = comments.find((c: any) => c.id === comment.id);
expect(foundComment).toBeDefined();
console.log('✅ Comment association verified');
console.log(
' Note: Comments are tied to tasks via API referential integrity'
);
});
it('should verify no orphaned dependencies after task deletion', async () => {
const baseName = uniqueTestName('Orphan Deps');
const taskA = await createTask(client, listId, {
name: `${baseName} - A`,
});
const taskB = await createTask(client, listId, {
name: `${baseName} - B`,
});
// Add dependency
await client.callTool('clickup_task_add_dependency', {
task_id: taskB.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
// Delete task A (dependency will be handled by API)
await client.callTool('clickup_task_delete', {
task_id: taskA.id,
});
await delay(100);
// Verify task B still exists (dependency auto-removed by API)
const taskBExists = await client.callTool('clickup_task_get', {
task_id: taskB.id,
});
expect(!taskBExists.isError).toBe(true);
console.log('✅ Orphaned dependency prevention verified');
});
});
describe('Data Validation', () => {
it('should validate task name constraints', async () => {
// Empty name should fail
const emptyResult = await client.callTool('clickup_task_create', {
list_id: listId,
name: '',
});
expect(emptyResult.isError).toBe(true);
// Valid name should succeed
const task = await createTask(client, listId, {
name: uniqueTestName('Valid Name'),
});
expect(task.id).toBeDefined();
console.log('✅ Task name validation working');
});
it('should validate priority values', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Priority Validation'),
});
// Valid priorities (1=Urgent, 2=High, 3=Normal, 4=Low)
const validPriorities = [1, 2, 3, 4];
for (const priority of validPriorities) {
const result = await client.callTool('clickup_task_update', {
task_id: task.id,
priority,
});
expect(!result.isError).toBe(true);
await delay(50);
}
// Invalid priority should fail or be ignored
const invalidResult = await client.callTool('clickup_task_update', {
task_id: task.id,
priority: 999,
});
// API should handle invalid priority gracefully
console.log('✅ Priority validation working');
});
it('should validate status values', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Status Validation'),
});
// Valid statuses
const validStatuses = ['to do', 'in progress', 'complete'];
for (const status of validStatuses) {
const result = await client.callTool('clickup_task_update', {
task_id: task.id,
status,
});
expect(!result.isError).toBe(true);
await delay(100);
}
// Invalid status should fail
const invalidResult = await client.callTool('clickup_task_update', {
task_id: task.id,
status: 'invalid_status_xyz',
});
// API should reject invalid status
expect(invalidResult.isError).toBe(true);
console.log('✅ Status validation working');
});
it('should validate due date formats', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Date Validation'),
});
// Valid timestamp (7 days from now)
const validDate = Date.now() + 7 * 24 * 60 * 60 * 1000;
const validResult = await client.callTool('clickup_task_update', {
task_id: task.id,
due_date: validDate,
});
expect(!validResult.isError).toBe(true);
// Invalid date should fail
const invalidResult = await client.callTool('clickup_task_update', {
task_id: task.id,
due_date: 'invalid_date',
});
// API should handle invalid date
console.log('✅ Due date validation working');
});
it('should validate list_id references', async () => {
// Invalid list_id should fail
const invalidResult = await client.callTool('clickup_task_create', {
list_id: 'invalid_list_id_xyz',
name: uniqueTestName('Invalid List'),
});
expect(invalidResult.isError).toBe(true);
// Valid list_id should succeed
const validTask = await createTask(client, listId, {
name: uniqueTestName('Valid List'),
});
expect(validTask.id).toBeDefined();
console.log('✅ List ID validation working');
});
it('should validate task_id references', async () => {
// Invalid task_id should fail
const invalidResult = await client.callTool('clickup_task_get', {
task_id: 'invalid_task_id_xyz',
});
expect(invalidResult.isError).toBe(true);
// Valid task_id should succeed
const task = await createTask(client, listId, {
name: uniqueTestName('Valid Task'),
});
const validResult = await client.callTool('clickup_task_get', {
task_id: task.id,
});
expect(!validResult.isError).toBe(true);
console.log('✅ Task ID validation working');
});
});
describe('State Verification', () => {
it('should verify task state after creation', async () => {
const taskName = uniqueTestName('State Verify');
const description = 'Test description';
const task = await createTask(client, listId, {
name: taskName,
description,
});
// Verify all created state
expect(task.id).toBeDefined();
expect(task.name).toBe(taskName);
expect(task.description).toBe(description);
expect(task.status).toBeDefined();
expect(task.list).toBeDefined();
console.log('✅ Task state verified after creation');
});
it('should verify state consistency after updates', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Update State'),
});
// Update and verify each field
const updatedTask = await client.updateTask(task.id, {
description: 'New description',
status: 'in progress',
priority: 2,
});
expect(updatedTask.description).toBe('New description');
expect(updatedTask.status.status).toBe('in progress');
expect(updatedTask.priority).toBeDefined();
// Re-fetch and verify persistence
const refetchedTask = await client.getTask(task.id);
expect(refetchedTask.description).toBe('New description');
expect(refetchedTask.status.status).toBe('in progress');
console.log('✅ State consistency verified after updates');
});
it('should verify state isolation between tasks', async () => {
const task1 = await createTask(client, listId, {
name: uniqueTestName('Isolation 1'),
description: 'Task 1',
});
const task2 = await createTask(client, listId, {
name: uniqueTestName('Isolation 2'),
description: 'Task 2',
});
// Update task 1
await client.updateTask(task1.id, {
description: 'Task 1 updated',
});
await delay(100);
// Verify task 2 unchanged
const finalTask2 = await client.getTask(task2.id);
expect(finalTask2.description).toBe('Task 2');
console.log('✅ State isolation between tasks verified');
});
it('should verify state after complex workflow', async () => {
const baseName = uniqueTestName('Complex State');
// Create with full details
const task = await createTask(client, listId, {
name: baseName,
description: 'Initial',
priority: 3,
});
// Add tags
await client.callTool('clickup_task_add_tag', {
task_id: task.id,
tag_name: 'workflow-test',
});
// Add comment
await client.addComment(task.id, 'Workflow comment');
// Update multiple fields
await client.updateTask(task.id, {
description: 'Updated',
status: 'complete',
});
await delay(100);
// Verify final state
const finalTask = await client.getTask(task.id);
expect(finalTask.id).toBe(task.id);
expect(finalTask.name).toBe(baseName);
expect(finalTask.description).toBe('Updated');
expect(finalTask.status.status).toBe('complete');
expect(finalTask.tags.length).toBeGreaterThan(0);
console.log('✅ Complex workflow state verified');
});
});
describe('Cascade Operations', () => {
it('should handle cascade when updating parent task', async () => {
const baseName = uniqueTestName('Cascade Parent');
const parent = await createTask(client, listId, {
name: `${baseName} - Parent`,
});
const subtasks = await Promise.all(
Array.from({ length: 2 }, (_, i) =>
createTask(client, listId, {
name: `${baseName} - Subtask ${i + 1}`,
parent: parent.id,
})
)
);
// Update parent
await client.updateTask(parent.id, {
description: 'Parent updated',
});
await delay(100);
// Verify subtasks unaffected
for (const subtask of subtasks) {
const updated = await client.getTask(subtask.id);
expect(updated.parent).toBe(parent.id);
}
console.log('✅ Cascade parent update handled correctly');
});
it('should handle cascade when deleting task with dependencies', async () => {
const baseName = uniqueTestName('Cascade Delete');
const taskA = await createTask(client, listId, {
name: `${baseName} - A`,
});
const taskB = await createTask(client, listId, {
name: `${baseName} - B`,
});
// Add dependency
await client.callTool('clickup_task_add_dependency', {
task_id: taskB.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
await delay(100);
// Delete task A
await client.callTool('clickup_task_delete', {
task_id: taskA.id,
});
await delay(100);
// Task B should still exist (dependency removed by API)
const taskBResult = await client.callTool('clickup_task_get', {
task_id: taskB.id,
});
expect(!taskBResult.isError).toBe(true);
console.log('✅ Cascade delete with dependencies handled');
});
it('should handle cascade when updating list membership', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Cascade List'),
});
// Verify task is in list
const createdTask = await client.getTask(task.id);
expect(createdTask.list.id).toBe(listId);
// Note: Moving tasks between lists requires list update
// This tests current list membership integrity
console.log('✅ List membership cascade verified');
});
});
describe('Data Recovery', () => {
it('should recover task after network-like delay', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Recovery Test'),
});
// Simulate network delay by waiting
await delay(500);
// Verify task still retrievable
const recovered = await client.getTask(task.id);
expect(recovered.id).toBe(task.id);
console.log('✅ Task recoverable after delay');
});
it('should recover state after failed operation', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Failed Op Recovery'),
description: 'Original',
});
// Attempt invalid operation
await client.callTool('clickup_task_update', {
task_id: task.id,
status: 'invalid_status',
});
await delay(100);
// Verify original state intact
const recovered = await client.getTask(task.id);
expect(recovered.description).toBe('Original');
console.log('✅ State recovered after failed operation');
});
it('should recover from partial batch failure', async () => {
const baseName = uniqueTestName('Batch Recovery');
// Create tasks
const validTasks = await Promise.all(
Array.from({ length: 3 }, (_, i) =>
createTask(client, listId, {
name: `${baseName} - ${i + 1}`,
})
)
);
// Batch update with one failure
const updates = [
client.updateTask(validTasks[0].id, { description: 'Update 1' }),
client.callTool('clickup_task_update', {
task_id: 'invalid_id',
description: 'Invalid',
}),
client.updateTask(validTasks[2].id, { description: 'Update 2' }),
];
await Promise.all(updates);
await delay(100);
// Verify successful updates persisted
const task0 = await client.getTask(validTasks[0].id);
const task2 = await client.getTask(validTasks[2].id);
expect(task0.description).toBe('Update 1');
expect(task2.description).toBe('Update 2');
console.log('✅ Recovery from partial batch failure verified');
});
});
describe('Referential Integrity', () => {
it('should maintain parent-child referential integrity', async () => {
const baseName = uniqueTestName('Parent-Child Ref');
const parent = await createTask(client, listId, {
name: `${baseName} - Parent`,
});
const child = await createTask(client, listId, {
name: `${baseName} - Child`,
parent: parent.id,
});
// Verify reference
const childTask = await client.getTask(child.id);
expect(childTask.parent).toBe(parent.id);
// Verify parent exists
const parentTask = await client.getTask(parent.id);
expect(parentTask.id).toBe(parent.id);
console.log('✅ Parent-child referential integrity maintained');
});
it('should maintain task-comment referential integrity', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Task-Comment Ref'),
});
const comment = await client.addComment(task.id, 'Test comment');
// Verify comment references task
const commentsResult = await client.callTool(
'clickup_task_get_comments',
{
task_id: task.id,
}
);
const comments = client.parseJsonResult(commentsResult);
const found = comments.find((c: any) => c.id === comment.id);
expect(found).toBeDefined();
console.log('✅ Task-comment referential integrity maintained');
});
it('should maintain dependency referential integrity', async () => {
const baseName = uniqueTestName('Dep Ref Integrity');
const taskA = await createTask(client, listId, {
name: `${baseName} - A`,
});
const taskB = await createTask(client, listId, {
name: `${baseName} - B`,
});
// Create dependency
await client.callTool('clickup_task_add_dependency', {
task_id: taskB.id,
depends_on: taskA.id,
dependency_type: 'waiting_on',
});
await delay(100);
// Verify both tasks exist
const finalA = await client.getTask(taskA.id);
const finalB = await client.getTask(taskB.id);
expect(finalA.id).toBe(taskA.id);
expect(finalB.id).toBe(taskB.id);
console.log('✅ Dependency referential integrity maintained');
});
it('should maintain list-task referential integrity', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('List-Task Ref'),
});
// Verify task references list
const taskData = await client.getTask(task.id);
expect(taskData.list.id).toBe(listId);
// Verify list exists (via hierarchy)
const hierarchyResult = await client.callTool(
'clickup_workspace_hierarchy_get',
{}
);
expect(!hierarchyResult.isError).toBe(true);
console.log('✅ List-task referential integrity maintained');
});
});
});