takt: add-task-instruction-doc (#174)
This commit is contained in:
parent
a481346945
commit
6f937b70b5
@ -37,7 +37,9 @@ describe('listTasks non-interactive text output', () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
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'));
|
expect(calls).toContainEqual(expect.stringContaining('Fix the login bug'));
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
});
|
});
|
||||||
@ -77,7 +79,9 @@ describe('listTasks non-interactive text output', () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
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'));
|
expect(calls).toContainEqual(expect.stringContaining('[failed] failed-one'));
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|||||||
109
src/__tests__/listTasksInteractivePendingLabel.test.ts
Normal file
109
src/__tests__/listTasksInteractivePendingLabel.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
import { deletePendingTask, deleteFailedTask } from './taskDeleteActions.js';
|
import { deletePendingTask, deleteFailedTask } from './taskDeleteActions.js';
|
||||||
import { retryFailedTask } from './taskRetryActions.js';
|
import { retryFailedTask } from './taskRetryActions.js';
|
||||||
import { listTasksNonInteractive, type ListNonInteractiveOptions } from './listNonInteractive.js';
|
import { listTasksNonInteractive, type ListNonInteractiveOptions } from './listNonInteractive.js';
|
||||||
|
import { formatTaskStatusLabel } from './taskStatusLabel.js';
|
||||||
|
|
||||||
export type { ListNonInteractiveOptions } from './listNonInteractive.js';
|
export type { ListNonInteractiveOptions } from './listNonInteractive.js';
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ type FailedTaskAction = 'retry' | 'delete';
|
|||||||
* Returns the selected action, or null if cancelled.
|
* Returns the selected action, or null if cancelled.
|
||||||
*/
|
*/
|
||||||
async function showPendingTaskAndPromptAction(task: TaskListItem): Promise<PendingTaskAction | null> {
|
async function showPendingTaskAndPromptAction(task: TaskListItem): Promise<PendingTaskAction | null> {
|
||||||
header(`[${task.kind}] ${task.name}`);
|
header(formatTaskStatusLabel(task));
|
||||||
info(` Created: ${task.createdAt}`);
|
info(` Created: ${task.createdAt}`);
|
||||||
if (task.content) {
|
if (task.content) {
|
||||||
info(` ${task.content}`);
|
info(` ${task.content}`);
|
||||||
@ -72,7 +73,7 @@ async function showPendingTaskAndPromptAction(task: TaskListItem): Promise<Pendi
|
|||||||
* Returns the selected action, or null if cancelled.
|
* Returns the selected action, or null if cancelled.
|
||||||
*/
|
*/
|
||||||
async function showFailedTaskAndPromptAction(task: TaskListItem): Promise<FailedTaskAction | null> {
|
async function showFailedTaskAndPromptAction(task: TaskListItem): Promise<FailedTaskAction | null> {
|
||||||
header(`[${task.kind}] ${task.name}`);
|
header(formatTaskStatusLabel(task));
|
||||||
info(` Failed at: ${task.createdAt}`);
|
info(` Failed at: ${task.createdAt}`);
|
||||||
if (task.content) {
|
if (task.content) {
|
||||||
info(` ${task.content}`);
|
info(` ${task.content}`);
|
||||||
@ -129,12 +130,12 @@ export async function listTasks(
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
...pendingTasks.map((task, idx) => ({
|
...pendingTasks.map((task, idx) => ({
|
||||||
label: `[pending] ${task.name}`,
|
label: formatTaskStatusLabel(task),
|
||||||
value: `pending:${idx}`,
|
value: `pending:${idx}`,
|
||||||
description: task.content,
|
description: task.content,
|
||||||
})),
|
})),
|
||||||
...failedTasks.map((task, idx) => ({
|
...failedTasks.map((task, idx) => ({
|
||||||
label: `[failed] ${task.name}`,
|
label: formatTaskStatusLabel(task),
|
||||||
value: `failed:${idx}`,
|
value: `failed:${idx}`,
|
||||||
description: task.content,
|
description: task.content,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
mergeBranch,
|
mergeBranch,
|
||||||
deleteBranch,
|
deleteBranch,
|
||||||
} from './taskActions.js';
|
} from './taskActions.js';
|
||||||
|
import { formatTaskStatusLabel } from './taskStatusLabel.js';
|
||||||
|
|
||||||
export interface ListNonInteractiveOptions {
|
export interface ListNonInteractiveOptions {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -56,11 +57,11 @@ function printNonInteractiveList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const task of pendingTasks) {
|
for (const task of pendingTasks) {
|
||||||
info(`[pending] ${task.name} - ${task.content}`);
|
info(`${formatTaskStatusLabel(task)} - ${task.content}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const task of failedTasks) {
|
for (const task of failedTasks) {
|
||||||
info(`[failed] ${task.name} - ${task.content}`);
|
info(`${formatTaskStatusLabel(task)} - ${task.content}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/features/tasks/list/taskStatusLabel.ts
Normal file
10
src/features/tasks/list/taskStatusLabel.ts
Normal 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}`;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user