mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-25 05:21:03 -07:00
277 lines
7.7 KiB
TypeScript
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',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|