takt: add-task-instruction-doc (#174)

This commit is contained in:
nrs 2026-02-09 20:56:14 +09:00 committed by GitHub
parent a481346945
commit 6f937b70b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 133 additions and 8 deletions

View File

@ -37,7 +37,9 @@ describe('listTasks non-interactive text output', () => {
// Then
const calls = logSpy.mock.calls.map((c) => c[0] as string);
expect(calls).toContainEqual(expect.stringContaining('[pending] my-task'));
expect(calls).toContainEqual(expect.stringContaining('[running] my-task'));
expect(calls).not.toContainEqual(expect.stringContaining('[pending] my-task'));
expect(calls).not.toContainEqual(expect.stringContaining('[pendig] my-task'));
expect(calls).toContainEqual(expect.stringContaining('Fix the login bug'));
logSpy.mockRestore();
});
@ -77,7 +79,9 @@ describe('listTasks non-interactive text output', () => {
// Then
const calls = logSpy.mock.calls.map((c) => c[0] as string);
expect(calls).toContainEqual(expect.stringContaining('[pending] pending-one'));
expect(calls).toContainEqual(expect.stringContaining('[running] pending-one'));
expect(calls).not.toContainEqual(expect.stringContaining('[pending] pending-one'));
expect(calls).not.toContainEqual(expect.stringContaining('[pendig] pending-one'));
expect(calls).toContainEqual(expect.stringContaining('[failed] failed-one'));
logSpy.mockRestore();
});

View File

@ -0,0 +1,109 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { TaskListItem } from '../infra/task/types.js';
const {
mockSelectOption,
mockHeader,
mockInfo,
mockBlankLine,
mockConfirm,
mockListPendingTaskItems,
mockListFailedTasks,
mockDeletePendingTask,
} = vi.hoisted(() => ({
mockSelectOption: vi.fn(),
mockHeader: vi.fn(),
mockInfo: vi.fn(),
mockBlankLine: vi.fn(),
mockConfirm: vi.fn(),
mockListPendingTaskItems: vi.fn(),
mockListFailedTasks: vi.fn(),
mockDeletePendingTask: vi.fn(),
}));
vi.mock('../infra/task/index.js', () => ({
listTaktBranches: vi.fn(() => []),
buildListItems: vi.fn(() => []),
detectDefaultBranch: vi.fn(() => 'main'),
TaskRunner: class {
listPendingTaskItems() {
return mockListPendingTaskItems();
}
listFailedTasks() {
return mockListFailedTasks();
}
},
}));
vi.mock('../shared/prompt/index.js', () => ({
selectOption: mockSelectOption,
confirm: mockConfirm,
}));
vi.mock('../shared/ui/index.js', () => ({
info: mockInfo,
header: mockHeader,
blankLine: mockBlankLine,
}));
vi.mock('../features/tasks/list/taskActions.js', () => ({
showFullDiff: vi.fn(),
showDiffAndPromptAction: vi.fn(),
tryMergeBranch: vi.fn(),
mergeBranch: vi.fn(),
deleteBranch: vi.fn(),
instructBranch: vi.fn(),
}));
vi.mock('../features/tasks/list/taskDeleteActions.js', () => ({
deletePendingTask: mockDeletePendingTask,
deleteFailedTask: vi.fn(),
}));
vi.mock('../features/tasks/list/taskRetryActions.js', () => ({
retryFailedTask: vi.fn(),
}));
import { listTasks } from '../features/tasks/list/index.js';
describe('listTasks interactive pending label regression', () => {
const pendingTask: TaskListItem = {
kind: 'pending',
name: 'my-task',
createdAt: '2026-02-09T00:00:00',
filePath: '/tmp/my-task.md',
content: 'Fix running status label',
};
beforeEach(() => {
vi.clearAllMocks();
mockListPendingTaskItems.mockReturnValue([pendingTask]);
mockListFailedTasks.mockReturnValue([]);
});
it('should show [running] in interactive menu for pending tasks', async () => {
mockSelectOption.mockResolvedValueOnce(null);
await listTasks('/project');
expect(mockSelectOption).toHaveBeenCalledTimes(1);
const menuOptions = mockSelectOption.mock.calls[0]![1] as Array<{ label: string; value: string }>;
expect(menuOptions).toContainEqual(expect.objectContaining({ label: '[running] my-task', value: 'pending:0' }));
expect(menuOptions.some((opt) => opt.label.includes('[pending]'))).toBe(false);
expect(menuOptions.some((opt) => opt.label.includes('[pendig]'))).toBe(false);
});
it('should show [running] header when pending task is selected', async () => {
mockSelectOption
.mockResolvedValueOnce('pending:0')
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);
await listTasks('/project');
expect(mockHeader).toHaveBeenCalledWith('[running] my-task');
const headerTexts = mockHeader.mock.calls.map(([text]) => String(text));
expect(headerTexts.some((text) => text.includes('[pending]'))).toBe(false);
expect(headerTexts.some((text) => text.includes('[pendig]'))).toBe(false);
});
});

View File

@ -30,6 +30,7 @@ import {
import { deletePendingTask, deleteFailedTask } from './taskDeleteActions.js';
import { retryFailedTask } from './taskRetryActions.js';
import { listTasksNonInteractive, type ListNonInteractiveOptions } from './listNonInteractive.js';
import { formatTaskStatusLabel } from './taskStatusLabel.js';
export type { ListNonInteractiveOptions } from './listNonInteractive.js';
@ -54,7 +55,7 @@ type FailedTaskAction = 'retry' | 'delete';
* Returns the selected action, or null if cancelled.
*/
async function showPendingTaskAndPromptAction(task: TaskListItem): Promise<PendingTaskAction | null> {
header(`[${task.kind}] ${task.name}`);
header(formatTaskStatusLabel(task));
info(` Created: ${task.createdAt}`);
if (task.content) {
info(` ${task.content}`);
@ -72,7 +73,7 @@ async function showPendingTaskAndPromptAction(task: TaskListItem): Promise<Pendi
* Returns the selected action, or null if cancelled.
*/
async function showFailedTaskAndPromptAction(task: TaskListItem): Promise<FailedTaskAction | null> {
header(`[${task.kind}] ${task.name}`);
header(formatTaskStatusLabel(task));
info(` Failed at: ${task.createdAt}`);
if (task.content) {
info(` ${task.content}`);
@ -129,12 +130,12 @@ export async function listTasks(
};
}),
...pendingTasks.map((task, idx) => ({
label: `[pending] ${task.name}`,
label: formatTaskStatusLabel(task),
value: `pending:${idx}`,
description: task.content,
})),
...failedTasks.map((task, idx) => ({
label: `[failed] ${task.name}`,
label: formatTaskStatusLabel(task),
value: `failed:${idx}`,
description: task.content,
})),

View File

@ -20,6 +20,7 @@ import {
mergeBranch,
deleteBranch,
} from './taskActions.js';
import { formatTaskStatusLabel } from './taskStatusLabel.js';
export interface ListNonInteractiveOptions {
enabled: boolean;
@ -56,11 +57,11 @@ function printNonInteractiveList(
}
for (const task of pendingTasks) {
info(`[pending] ${task.name} - ${task.content}`);
info(`${formatTaskStatusLabel(task)} - ${task.content}`);
}
for (const task of failedTasks) {
info(`[failed] ${task.name} - ${task.content}`);
info(`${formatTaskStatusLabel(task)} - ${task.content}`);
}
}

View File

@ -0,0 +1,10 @@
import type { TaskListItem } from '../../../infra/task/index.js';
const TASK_STATUS_BY_KIND: Record<TaskListItem['kind'], string> = {
pending: 'running',
failed: 'failed',
};
export function formatTaskStatusLabel(task: TaskListItem): string {
return `[${TASK_STATUS_BY_KIND[task.kind]}] ${task.name}`;
}