Files
gemini-cli/packages/core/src/tools/trackerTools.test.ts

277 lines
7.7 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Config } from '../config/config.js';
import { MessageBus } from '../confirmation-bus/message-bus.js';
import type { PolicyEngine } from '../policy/policy-engine.js';
import {
TrackerCreateTaskTool,
TrackerListTasksTool,
TrackerUpdateTaskTool,
TrackerVisualizeTool,
TrackerAddDependencyTool,
buildTodosReturnDisplay,
} from './trackerTools.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { TaskStatus, TaskType } from '../services/trackerTypes.js';
import type { TrackerService } from '../services/trackerService.js';
describe('Tracker Tools Integration', () => {
let tempDir: string;
let config: Config;
let messageBus: MessageBus;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tracker-tools-test-'));
config = new Config({
sessionId: 'test-session',
targetDir: tempDir,
cwd: tempDir,
model: 'gemini-3-flash',
debugMode: false,
});
messageBus = new MessageBus(null as unknown as PolicyEngine, false);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
const getSignal = () => new AbortController().signal;
it('creates and lists tasks', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
const createResult = await createTool.buildAndExecute(
{
title: 'Test Task',
description: 'Test Description',
type: TaskType.TASK,
},
getSignal(),
);
expect(createResult.llmContent).toContain('Created task');
const listTool = new TrackerListTasksTool(config, messageBus);
const listResult = await listTool.buildAndExecute({}, getSignal());
expect(listResult.llmContent).toContain('Test Task');
expect(listResult.llmContent).toContain(`(${TaskStatus.OPEN})`);
});
it('updates task status', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
await createTool.buildAndExecute(
{
title: 'Update Me',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
const tasks = await config.getTrackerService().listTasks();
const taskId = tasks[0].id;
const updateTool = new TrackerUpdateTaskTool(config, messageBus);
const updateResult = await updateTool.buildAndExecute(
{
id: taskId,
status: TaskStatus.IN_PROGRESS,
},
getSignal(),
);
expect(updateResult.llmContent).toContain(
`Status: ${TaskStatus.IN_PROGRESS}`,
);
const task = await config.getTrackerService().getTask(taskId);
expect(task?.status).toBe(TaskStatus.IN_PROGRESS);
});
it('adds dependencies and visualizes the graph', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
// Create Parent
await createTool.buildAndExecute(
{
title: 'Parent Task',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
// Create Child
await createTool.buildAndExecute(
{
title: 'Child Task',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
const tasks = await config.getTrackerService().listTasks();
const parentId = tasks.find((t) => t.title === 'Parent Task')!.id;
const childId = tasks.find((t) => t.title === 'Child Task')!.id;
// Add Dependency
const addDepTool = new TrackerAddDependencyTool(config, messageBus);
await addDepTool.buildAndExecute(
{
taskId: parentId,
dependencyId: childId,
},
getSignal(),
);
const updatedParent = await config.getTrackerService().getTask(parentId);
expect(updatedParent?.dependencies).toContain(childId);
// Visualize
const vizTool = new TrackerVisualizeTool(config, messageBus);
const vizResult = await vizTool.buildAndExecute({}, getSignal());
expect(vizResult.llmContent).toContain('Parent Task');
expect(vizResult.llmContent).toContain('Child Task');
expect(vizResult.llmContent).toContain(childId);
});
describe('buildTodosReturnDisplay', () => {
it('returns empty list for no tasks', async () => {
const mockService = {
listTasks: async () => [],
} as unknown as TrackerService;
const result = await buildTodosReturnDisplay(mockService);
expect(result.todos).toEqual([]);
});
it('returns formatted todos', async () => {
const parent = {
id: 'p1',
title: 'Parent',
type: TaskType.TASK,
status: TaskStatus.IN_PROGRESS,
dependencies: [],
};
const child = {
id: 'c1',
title: 'Child',
type: TaskType.EPIC,
status: TaskStatus.OPEN,
parentId: 'p1',
dependencies: [],
};
const closedLeaf = {
id: 'leaf',
title: 'Closed Leaf',
type: TaskType.BUG,
status: TaskStatus.CLOSED,
parentId: 'c1',
dependencies: [],
};
const mockService = {
listTasks: async () => [parent, child, closedLeaf],
} as unknown as TrackerService;
const display = await buildTodosReturnDisplay(mockService);
expect(display.todos).toEqual([
{
description: `task: Parent (p1)`,
status: 'in_progress',
},
{
description: ` epic: Child (c1)`,
status: 'pending',
},
{
description: ` bug: Closed Leaf (leaf)`,
status: 'completed',
},
]);
});
it('sorts tasks by status', async () => {
const t1 = {
id: 't1',
title: 'T1',
type: TaskType.TASK,
status: TaskStatus.CLOSED,
dependencies: [],
};
const t2 = {
id: 't2',
title: 'T2',
type: TaskType.TASK,
status: TaskStatus.OPEN,
dependencies: [],
};
const t3 = {
id: 't3',
title: 'T3',
type: TaskType.TASK,
status: TaskStatus.IN_PROGRESS,
dependencies: [],
};
const t4 = {
id: 't4',
title: 'T4',
type: TaskType.TASK,
status: TaskStatus.BLOCKED,
dependencies: [],
};
const mockService = {
listTasks: async () => [t1, t2, t3, t4],
} as unknown as TrackerService;
const display = await buildTodosReturnDisplay(mockService);
expect(display.todos).toEqual([
{ description: `task: T3 (t3)`, status: 'in_progress' },
{ description: `task: T2 (t2)`, status: 'pending' },
{ description: `task: T4 (t4)`, status: 'blocked' },
{ description: `task: T1 (t1)`, status: 'completed' },
]);
});
it('detects cycles', async () => {
// Since TrackerTask only has a single parentId, a true cycle is unreachable from roots.
// We simulate a database corruption (two tasks with same ID, one root, one child)
// just to exercise the protective cycle detection branch.
const rootP1 = {
id: 'p1',
title: 'Parent',
type: TaskType.TASK,
status: TaskStatus.OPEN,
dependencies: [],
};
const childP1 = { ...rootP1, parentId: 'p1' };
const mockService = {
listTasks: async () => [rootP1, childP1],
} as unknown as TrackerService;
const display = await buildTodosReturnDisplay(mockService);
expect(display.todos).toEqual([
{
description: `task: Parent (p1)`,
status: 'pending',
},
{
description: ` [CYCLE DETECTED: p1]`,
status: 'cancelled',
},
]);
});
});
});