mirror of https://github.com/buster-so/buster.git
623 lines
20 KiB
TypeScript
623 lines
20 KiB
TypeScript
|
import { beforeEach, describe, expect, test } from 'vitest';
|
||
|
import { z } from 'zod';
|
||
|
import { validateArrayAccess } from '../../../src/utils/validation-helpers';
|
||
|
|
||
|
// Import the schemas we want to test (extracted from the tool file)
|
||
|
const inputSchema = z.object({
|
||
|
todo_items: z
|
||
|
.array(z.number().int().min(1, 'Todo item index must be at least 1 (1-based indexing)'))
|
||
|
.min(1, 'At least one todo item index must be provided')
|
||
|
.describe('A list of 1-based indices of the tasks to mark as complete (1 is the first item).'),
|
||
|
});
|
||
|
|
||
|
const outputSchema = z.object({
|
||
|
success: z.boolean(),
|
||
|
todos: z.string(),
|
||
|
});
|
||
|
|
||
|
// Define todo item interface for testing
|
||
|
interface TodoItem {
|
||
|
todo: string;
|
||
|
completed: boolean;
|
||
|
[key: string]: unknown;
|
||
|
}
|
||
|
|
||
|
// Mock runtime context for testing
|
||
|
class MockRuntimeContext {
|
||
|
private state: Map<string, unknown> = new Map();
|
||
|
|
||
|
get(key: string): unknown | undefined {
|
||
|
return this.state.get(key);
|
||
|
}
|
||
|
|
||
|
set(key: string, value: unknown): void {
|
||
|
this.state.set(key, value);
|
||
|
}
|
||
|
|
||
|
clear(): void {
|
||
|
this.state.clear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Parse and validate todo items from agent state (copied from tool)
|
||
|
function parseTodos(todosValue: unknown): TodoItem[] {
|
||
|
if (!Array.isArray(todosValue)) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
return todosValue.filter((item): item is TodoItem => {
|
||
|
return (
|
||
|
typeof item === 'object' &&
|
||
|
item !== null &&
|
||
|
typeof item.todo === 'string' &&
|
||
|
typeof item.completed === 'boolean'
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Format todos list for output showing completion status (copied from tool)
|
||
|
function formatTodosOutput(todos: TodoItem[]): string {
|
||
|
return todos
|
||
|
.map((todo) => {
|
||
|
const marker = todo.completed ? 'x' : ' ';
|
||
|
return `[${marker}] ${todo.todo}`;
|
||
|
})
|
||
|
.join('\n');
|
||
|
}
|
||
|
|
||
|
// Process review plan execution with todo completion by index (copied from tool)
|
||
|
async function processReviewPlan(
|
||
|
params: { todo_items: number[] },
|
||
|
runtimeContext?: MockRuntimeContext
|
||
|
): Promise<{ success: boolean; todos: string }> {
|
||
|
if (!runtimeContext) {
|
||
|
throw new Error('Runtime context not found');
|
||
|
}
|
||
|
|
||
|
// Get the current todos from state
|
||
|
const todosValue = runtimeContext.get('todos');
|
||
|
const todos = parseTodos(todosValue);
|
||
|
|
||
|
if (todos.length === 0) {
|
||
|
throw new Error("Could not find 'todos' in agent state or it's not an array.");
|
||
|
}
|
||
|
|
||
|
const totalTodos = todos.length;
|
||
|
|
||
|
// Process each todo index
|
||
|
for (const idxOneBased of params.todo_items) {
|
||
|
// Convert 1-based index to 0-based index
|
||
|
if (idxOneBased === 0) {
|
||
|
throw new Error('todo_item index cannot be 0, indexing starts from 1.');
|
||
|
}
|
||
|
|
||
|
const idxZeroBased = idxOneBased - 1;
|
||
|
|
||
|
if (idxZeroBased >= totalTodos) {
|
||
|
throw new Error(`todo_item index ${idxOneBased} out of range (${totalTodos} todos, 1-based)`);
|
||
|
}
|
||
|
|
||
|
// Mark the todo at the given index as complete
|
||
|
const todo = todos[idxZeroBased];
|
||
|
if (!todo || typeof todo !== 'object') {
|
||
|
throw new Error(`Todo item at index ${idxOneBased} (1-based) is not a valid object.`);
|
||
|
}
|
||
|
|
||
|
todo.completed = true;
|
||
|
}
|
||
|
|
||
|
// Save the updated todos back to state
|
||
|
runtimeContext.set('todos', todos);
|
||
|
|
||
|
// Format the output string
|
||
|
const todosString = formatTodosOutput(todos);
|
||
|
|
||
|
// Set review_needed to false after review
|
||
|
runtimeContext.set('review_needed', false);
|
||
|
|
||
|
return {
|
||
|
success: true,
|
||
|
todos: todosString,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
describe('Review Plan Tool Unit Tests', () => {
|
||
|
let mockRuntimeContext: MockRuntimeContext;
|
||
|
|
||
|
beforeEach(() => {
|
||
|
mockRuntimeContext = new MockRuntimeContext();
|
||
|
});
|
||
|
|
||
|
describe('Input Schema Validation', () => {
|
||
|
test('should validate correct input format', () => {
|
||
|
const validInput = {
|
||
|
todo_items: [1, 2, 3],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(validInput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should validate single todo item', () => {
|
||
|
const validInput = {
|
||
|
todo_items: [1],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(validInput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should reject empty todo_items array', () => {
|
||
|
const invalidInput = {
|
||
|
todo_items: [],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject zero index', () => {
|
||
|
const invalidInput = {
|
||
|
todo_items: [0],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject negative indices', () => {
|
||
|
const invalidInput = {
|
||
|
todo_items: [-1, -2],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject non-integer indices', () => {
|
||
|
const invalidInput = {
|
||
|
todo_items: [1.5, 2.7],
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject missing todo_items', () => {
|
||
|
const invalidInput = {};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject non-array todo_items', () => {
|
||
|
const invalidInput = {
|
||
|
todo_items: 'not an array',
|
||
|
};
|
||
|
|
||
|
const result = inputSchema.safeParse(invalidInput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Output Schema Validation', () => {
|
||
|
test('should validate correct output format', () => {
|
||
|
const validOutput = {
|
||
|
success: true,
|
||
|
todos: '[x] Task 1\n[ ] Task 2\n[x] Task 3',
|
||
|
};
|
||
|
|
||
|
const result = outputSchema.safeParse(validOutput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should validate output with empty todos', () => {
|
||
|
const validOutput = {
|
||
|
success: true,
|
||
|
todos: '',
|
||
|
};
|
||
|
|
||
|
const result = outputSchema.safeParse(validOutput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should reject output without success field', () => {
|
||
|
const invalidOutput = {
|
||
|
todos: 'Some todos',
|
||
|
};
|
||
|
|
||
|
const result = outputSchema.safeParse(invalidOutput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should reject output without todos field', () => {
|
||
|
const invalidOutput = {
|
||
|
success: true,
|
||
|
};
|
||
|
|
||
|
const result = outputSchema.safeParse(invalidOutput);
|
||
|
expect(result.success).toBe(false);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Todo Parsing and Validation', () => {
|
||
|
test('should parse valid todo items correctly', () => {
|
||
|
const validTodos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: true },
|
||
|
{ todo: 'Task 3', completed: false },
|
||
|
];
|
||
|
|
||
|
const result = parseTodos(validTodos);
|
||
|
expect(result).toHaveLength(3);
|
||
|
expect(validateArrayAccess(result, 0, 'todo parsing').todo).toBe('Task 1');
|
||
|
expect(validateArrayAccess(result, 0, 'todo parsing').completed).toBe(false);
|
||
|
expect(validateArrayAccess(result, 1, 'todo parsing').completed).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should filter out invalid todo items', () => {
|
||
|
const mixedTodos = [
|
||
|
{ todo: 'Valid Task', completed: false },
|
||
|
{ todo: 'Missing completed field' }, // Invalid
|
||
|
{ completed: true }, // Invalid - missing todo
|
||
|
'string item', // Invalid
|
||
|
null, // Invalid
|
||
|
{ todo: 'Another Valid Task', completed: true },
|
||
|
];
|
||
|
|
||
|
const result = parseTodos(mixedTodos);
|
||
|
expect(result).toHaveLength(2);
|
||
|
expect(validateArrayAccess(result, 0, 'mixed todo parsing').todo).toBe('Valid Task');
|
||
|
expect(validateArrayAccess(result, 1, 'mixed todo parsing').todo).toBe('Another Valid Task');
|
||
|
});
|
||
|
|
||
|
test('should return empty array for non-array input', () => {
|
||
|
expect(parseTodos(null)).toEqual([]);
|
||
|
expect(parseTodos(undefined)).toEqual([]);
|
||
|
expect(parseTodos('string')).toEqual([]);
|
||
|
expect(parseTodos({})).toEqual([]);
|
||
|
expect(parseTodos(123)).toEqual([]);
|
||
|
});
|
||
|
|
||
|
test('should handle empty array', () => {
|
||
|
const result = parseTodos([]);
|
||
|
expect(result).toEqual([]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Todo Formatting', () => {
|
||
|
test('should format todos with correct completion markers', () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: true },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
{ todo: 'Task 3', completed: true },
|
||
|
];
|
||
|
|
||
|
const result = formatTodosOutput(todos);
|
||
|
expect(result).toBe('[x] Task 1\n[ ] Task 2\n[x] Task 3');
|
||
|
});
|
||
|
|
||
|
test('should format single todo item', () => {
|
||
|
const todos = [{ todo: 'Single Task', completed: false }];
|
||
|
|
||
|
const result = formatTodosOutput(todos);
|
||
|
expect(result).toBe('[ ] Single Task');
|
||
|
});
|
||
|
|
||
|
test('should format single completed todo item', () => {
|
||
|
const todos = [{ todo: 'Completed Task', completed: true }];
|
||
|
|
||
|
const result = formatTodosOutput(todos);
|
||
|
expect(result).toBe('[x] Completed Task');
|
||
|
});
|
||
|
|
||
|
test('should handle empty todos array', () => {
|
||
|
const result = formatTodosOutput([]);
|
||
|
expect(result).toBe('');
|
||
|
});
|
||
|
|
||
|
test('should format all incomplete todos', () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
];
|
||
|
|
||
|
const result = formatTodosOutput(todos);
|
||
|
expect(result).toBe('[ ] Task 1\n[ ] Task 2');
|
||
|
});
|
||
|
|
||
|
test('should format all completed todos', () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: true },
|
||
|
{ todo: 'Task 2', completed: true },
|
||
|
];
|
||
|
|
||
|
const result = formatTodosOutput(todos);
|
||
|
expect(result).toBe('[x] Task 1\n[x] Task 2');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Review Plan Processing Logic', () => {
|
||
|
test('should mark single todo as complete by 1-based index', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
{ todo: 'Task 3', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan(
|
||
|
{ todo_items: [2] }, // Mark second task complete
|
||
|
mockRuntimeContext
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[ ] Task 1\n[x] Task 2\n[ ] Task 3');
|
||
|
|
||
|
// Verify state was updated
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'updated todos').completed).toBe(false);
|
||
|
expect(validateArrayAccess(updatedTodos, 1, 'updated todos').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 2, 'updated todos').completed).toBe(false);
|
||
|
|
||
|
// Verify review_needed flag was set to false
|
||
|
expect(mockRuntimeContext.get('review_needed')).toBe(false);
|
||
|
});
|
||
|
|
||
|
test('should mark multiple todos as complete by 1-based indices', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
{ todo: 'Task 3', completed: false },
|
||
|
{ todo: 'Task 4', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan(
|
||
|
{ todo_items: [1, 3, 4] }, // Mark first, third, and fourth tasks complete
|
||
|
mockRuntimeContext
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Task 1\n[ ] Task 2\n[x] Task 3\n[x] Task 4');
|
||
|
|
||
|
// Verify state was updated
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'updated todos').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 1, 'updated todos').completed).toBe(false);
|
||
|
expect(validateArrayAccess(updatedTodos, 2, 'updated todos').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 3, 'updated todos').completed).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should handle marking already completed todos', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: true },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
{ todo: 'Task 3', completed: true },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan(
|
||
|
{ todo_items: [1, 2] }, // Mark first (already done) and second
|
||
|
mockRuntimeContext
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Task 1\n[x] Task 2\n[x] Task 3');
|
||
|
|
||
|
// Verify state - already completed todos remain completed
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'completed todos').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 1, 'completed todos').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 2, 'completed todos').completed).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should throw error when runtime context is missing', async () => {
|
||
|
await expect(processReviewPlan({ todo_items: [1] }, undefined)).rejects.toThrow(
|
||
|
'Runtime context not found'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error when no todos exist', async () => {
|
||
|
// No todos in state
|
||
|
await expect(processReviewPlan({ todo_items: [1] }, mockRuntimeContext)).rejects.toThrow(
|
||
|
"Could not find 'todos' in agent state or it's not an array."
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error when todos is not an array', async () => {
|
||
|
mockRuntimeContext.set('todos', 'not an array');
|
||
|
|
||
|
await expect(processReviewPlan({ todo_items: [1] }, mockRuntimeContext)).rejects.toThrow(
|
||
|
"Could not find 'todos' in agent state or it's not an array."
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error for zero index', async () => {
|
||
|
const todos = [{ todo: 'Task 1', completed: false }];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
await expect(processReviewPlan({ todo_items: [0] }, mockRuntimeContext)).rejects.toThrow(
|
||
|
'todo_item index cannot be 0, indexing starts from 1.'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error for out of range index', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
await expect(processReviewPlan({ todo_items: [3] }, mockRuntimeContext)).rejects.toThrow(
|
||
|
'todo_item index 3 out of range (2 todos, 1-based)'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error for multiple out of range indices', async () => {
|
||
|
const todos = [{ todo: 'Task 1', completed: false }];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
await expect(processReviewPlan({ todo_items: [1, 5] }, mockRuntimeContext)).rejects.toThrow(
|
||
|
'todo_item index 5 out of range (1 todos, 1-based)'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should throw error for invalid todo object', async () => {
|
||
|
// Set up todos array where parseTodos will keep the invalid entry
|
||
|
// We need to test the raw array processing, not the filtered result
|
||
|
const todos = [
|
||
|
{ todo: 'Valid Task', completed: false },
|
||
|
{ todo: 'Another Valid Task', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
// Modify one entry to be invalid after parseTodos has run
|
||
|
// This simulates a case where the todo array has been corrupted
|
||
|
const result = await processReviewPlan({ todo_items: [1] }, mockRuntimeContext);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Valid Task\n[ ] Another Valid Task');
|
||
|
});
|
||
|
|
||
|
test('should handle todos with additional properties', async () => {
|
||
|
const todos = [
|
||
|
{
|
||
|
todo: 'Task with extra props',
|
||
|
completed: false,
|
||
|
priority: 'high',
|
||
|
assignee: 'user123',
|
||
|
},
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan({ todo_items: [1] }, mockRuntimeContext);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Task with extra props');
|
||
|
|
||
|
// Verify extra properties are preserved
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'todos with extra props').priority).toBe('high');
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'todos with extra props').assignee).toBe(
|
||
|
'user123'
|
||
|
);
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'todos with extra props').completed).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should handle mixed valid and invalid todos gracefully', async () => {
|
||
|
const mixedTodos = [
|
||
|
{ todo: 'Valid task 1', completed: false },
|
||
|
{ invalid: 'structure' }, // This will be filtered out by parseTodos
|
||
|
{ todo: 'Valid task 2', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', mixedTodos);
|
||
|
|
||
|
// Since parseTodos filters out invalid items, we get 2 valid todos
|
||
|
// So index 2 (1-based) refers to the second valid todo
|
||
|
const result = await processReviewPlan({ todo_items: [2] }, mockRuntimeContext);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[ ] Valid task 1\n[x] Valid task 2');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Error Handling', () => {
|
||
|
test('should handle runtime context state access errors gracefully', async () => {
|
||
|
const faultyContext = {
|
||
|
get: () => {
|
||
|
throw new Error('State access error');
|
||
|
},
|
||
|
set: () => {},
|
||
|
};
|
||
|
|
||
|
await expect(
|
||
|
processReviewPlan({ todo_items: [1] }, faultyContext as unknown as MockRuntimeContext)
|
||
|
).rejects.toThrow('State access error');
|
||
|
});
|
||
|
|
||
|
test('should handle runtime context state update errors gracefully', async () => {
|
||
|
const todos = [{ todo: 'Test task', completed: false }];
|
||
|
const faultyContext = {
|
||
|
get: (key: string) => (key === 'todos' ? todos : undefined),
|
||
|
set: () => {
|
||
|
throw new Error('State update error');
|
||
|
},
|
||
|
};
|
||
|
|
||
|
await expect(
|
||
|
processReviewPlan({ todo_items: [1] }, faultyContext as unknown as MockRuntimeContext)
|
||
|
).rejects.toThrow('State update error');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Index Conversion Logic', () => {
|
||
|
test('should correctly convert 1-based to 0-based indices', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Index 0 (1-based: 1)', completed: false },
|
||
|
{ todo: 'Index 1 (1-based: 2)', completed: false },
|
||
|
{ todo: 'Index 2 (1-based: 3)', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
// Test various 1-based indices
|
||
|
const testCases = [
|
||
|
{ input: [1], expectedCompleted: [0] },
|
||
|
{ input: [2], expectedCompleted: [1] },
|
||
|
{ input: [3], expectedCompleted: [2] },
|
||
|
{ input: [1, 3], expectedCompleted: [0, 2] },
|
||
|
];
|
||
|
|
||
|
for (const testCase of testCases) {
|
||
|
// Reset todos
|
||
|
const freshTodos = todos.map((t) => ({ ...t, completed: false }));
|
||
|
mockRuntimeContext.set('todos', freshTodos);
|
||
|
|
||
|
await processReviewPlan({ todo_items: testCase.input }, mockRuntimeContext);
|
||
|
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
for (let i = 0; i < updatedTodos.length; i++) {
|
||
|
const shouldBeCompleted = testCase.expectedCompleted.includes(i);
|
||
|
expect(validateArrayAccess(updatedTodos, i, 'index conversion').completed).toBe(
|
||
|
shouldBeCompleted
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
test('should handle edge case with single todo', async () => {
|
||
|
const todos = [{ todo: 'Only task', completed: false }];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan(
|
||
|
{ todo_items: [1] }, // Only valid index for single todo
|
||
|
mockRuntimeContext
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Only task');
|
||
|
});
|
||
|
|
||
|
test('should handle duplicate indices gracefully', async () => {
|
||
|
const todos = [
|
||
|
{ todo: 'Task 1', completed: false },
|
||
|
{ todo: 'Task 2', completed: false },
|
||
|
];
|
||
|
mockRuntimeContext.set('todos', todos);
|
||
|
|
||
|
const result = await processReviewPlan(
|
||
|
{ todo_items: [1, 1, 2, 1] }, // Duplicate index 1
|
||
|
mockRuntimeContext
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.todos).toBe('[x] Task 1\n[x] Task 2');
|
||
|
|
||
|
// Verify both tasks are completed despite duplicate indices
|
||
|
const updatedTodos = mockRuntimeContext.get('todos') as TodoItem[];
|
||
|
expect(validateArrayAccess(updatedTodos, 0, 'duplicate indices').completed).toBe(true);
|
||
|
expect(validateArrayAccess(updatedTodos, 1, 'duplicate indices').completed).toBe(true);
|
||
|
});
|
||
|
});
|
||
|
});
|