takt: Execute アクションで tasks.yaml への追加をスキップする skipTaskList オプション (#334)
- SelectAndExecuteOptions に skipTaskList フラグを追加 - routing.ts の Execute アクションで skipTaskList: true を設定 - taskRecord の null チェックで条件分岐を統一 - テストを現在の taskResultHandler API に合わせて修正
This commit is contained in:
parent
f307ed80f0
commit
e5902b87ad
165
src/__tests__/selectAndExecute-skipTaskList.test.ts
Normal file
165
src/__tests__/selectAndExecute-skipTaskList.test.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* Tests for skipTaskList option in selectAndExecuteTask
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockAddTask,
|
||||||
|
mockExecuteTask,
|
||||||
|
mockPersistTaskResult,
|
||||||
|
mockPersistTaskError,
|
||||||
|
mockBuildBooleanTaskResult,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
mockAddTask: vi.fn(() => ({
|
||||||
|
name: 'test-task',
|
||||||
|
content: 'test task',
|
||||||
|
filePath: '/project/.takt/tasks.yaml',
|
||||||
|
createdAt: '2026-02-14T00:00:00.000Z',
|
||||||
|
status: 'pending',
|
||||||
|
data: { task: 'test task' },
|
||||||
|
})),
|
||||||
|
mockExecuteTask: vi.fn(),
|
||||||
|
mockPersistTaskResult: vi.fn(),
|
||||||
|
mockPersistTaskError: vi.fn(),
|
||||||
|
mockBuildBooleanTaskResult: vi.fn(() => ({ task: 'mock-result' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
|
resolvePieceConfigValue: vi.fn(),
|
||||||
|
loadPieceByIdentifier: vi.fn(() => ({ name: 'default' })),
|
||||||
|
listPieces: vi.fn(() => ['default']),
|
||||||
|
listPieceEntries: vi.fn(() => []),
|
||||||
|
isPiecePath: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/task/index.js', () => ({
|
||||||
|
createSharedClone: vi.fn(),
|
||||||
|
autoCommitAndPush: vi.fn(),
|
||||||
|
summarizeTaskName: vi.fn(),
|
||||||
|
resolveBaseBranch: vi.fn(() => ({ branch: 'main' })),
|
||||||
|
TaskRunner: vi.fn(() => ({
|
||||||
|
addTask: (...args: unknown[]) => mockAddTask(...args),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn(),
|
||||||
|
withProgress: async <T>(
|
||||||
|
_startMessage: string,
|
||||||
|
_completionMessage: string | ((result: T) => string),
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
): Promise<T> => operation(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/github/index.js', () => ({
|
||||||
|
createPullRequest: vi.fn(),
|
||||||
|
buildPrBody: vi.fn(),
|
||||||
|
pushBranch: vi.fn(),
|
||||||
|
findExistingPr: vi.fn(),
|
||||||
|
commentOnPr: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||||
|
executeTask: (...args: unknown[]) => mockExecuteTask(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/tasks/execute/taskResultHandler.js', () => ({
|
||||||
|
buildBooleanTaskResult: (...args: unknown[]) => mockBuildBooleanTaskResult(...args),
|
||||||
|
persistTaskResult: (...args: unknown[]) => mockPersistTaskResult(...args),
|
||||||
|
persistTaskError: (...args: unknown[]) => mockPersistTaskError(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/pieceSelection/index.js', () => ({
|
||||||
|
selectPiece: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { confirm } from '../shared/prompt/index.js';
|
||||||
|
import { selectAndExecuteTask } from '../features/tasks/execute/selectAndExecute.js';
|
||||||
|
|
||||||
|
const mockConfirm = vi.mocked(confirm);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockExecuteTask.mockResolvedValue(true);
|
||||||
|
// worktree を使わない(confirm で false)
|
||||||
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('skipTaskList option in selectAndExecuteTask', () => {
|
||||||
|
it('skipTaskList: true の場合はタスクリストに追加しない', async () => {
|
||||||
|
await selectAndExecuteTask('/project', 'test task', {
|
||||||
|
piece: 'default',
|
||||||
|
skipTaskList: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddTask).not.toHaveBeenCalled();
|
||||||
|
expect(mockPersistTaskResult).not.toHaveBeenCalled();
|
||||||
|
expect(mockExecuteTask).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skipTaskList: false の場合はタスクリストに追加する', async () => {
|
||||||
|
await selectAndExecuteTask('/project', 'test task', {
|
||||||
|
piece: 'default',
|
||||||
|
skipTaskList: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddTask).toHaveBeenCalled();
|
||||||
|
expect(mockBuildBooleanTaskResult).toHaveBeenCalled();
|
||||||
|
expect(mockPersistTaskResult).toHaveBeenCalled();
|
||||||
|
expect(mockExecuteTask).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skipTaskList 未指定の場合はタスクリストに追加する', async () => {
|
||||||
|
await selectAndExecuteTask('/project', 'test task', {
|
||||||
|
piece: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddTask).toHaveBeenCalled();
|
||||||
|
expect(mockPersistTaskResult).toHaveBeenCalled();
|
||||||
|
expect(mockExecuteTask).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skipTaskList: true でエラー時は persistTaskError を呼ばない', async () => {
|
||||||
|
mockExecuteTask.mockRejectedValue(new Error('Task execution failed'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
selectAndExecuteTask('/project', 'test task', {
|
||||||
|
piece: 'default',
|
||||||
|
skipTaskList: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Task execution failed');
|
||||||
|
|
||||||
|
expect(mockAddTask).not.toHaveBeenCalled();
|
||||||
|
expect(mockPersistTaskError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skipTaskList: false でエラー時は persistTaskError を呼ぶ', async () => {
|
||||||
|
mockExecuteTask.mockRejectedValue(new Error('Task execution failed'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
selectAndExecuteTask('/project', 'test task', {
|
||||||
|
piece: 'default',
|
||||||
|
skipTaskList: false,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Task execution failed');
|
||||||
|
|
||||||
|
expect(mockAddTask).toHaveBeenCalled();
|
||||||
|
expect(mockPersistTaskError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -214,6 +214,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
selectOptions.interactiveUserInput = true;
|
selectOptions.interactiveUserInput = true;
|
||||||
selectOptions.piece = pieceId;
|
selectOptions.piece = pieceId;
|
||||||
selectOptions.interactiveMetadata = { confirmed: true, task: confirmedTask };
|
selectOptions.interactiveMetadata = { confirmed: true, task: confirmedTask };
|
||||||
|
selectOptions.skipTaskList = true;
|
||||||
await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides);
|
await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides);
|
||||||
},
|
},
|
||||||
create_issue: async ({ task: confirmedTask }) => {
|
create_issue: async ({ task: confirmedTask }) => {
|
||||||
|
|||||||
@ -108,7 +108,9 @@ export async function selectAndExecuteTask(
|
|||||||
|
|
||||||
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr, draftPr: shouldDraftPr });
|
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr, draftPr: shouldDraftPr });
|
||||||
const taskRunner = new TaskRunner(cwd);
|
const taskRunner = new TaskRunner(cwd);
|
||||||
const taskRecord = taskRunner.addTask(task, {
|
let taskRecord: Awaited<ReturnType<TaskRunner['addTask']>> | null = null;
|
||||||
|
if (options?.skipTaskList !== true) {
|
||||||
|
taskRecord = taskRunner.addTask(task, {
|
||||||
piece: pieceIdentifier,
|
piece: pieceIdentifier,
|
||||||
...(isWorktree ? { worktree: true } : {}),
|
...(isWorktree ? { worktree: true } : {}),
|
||||||
...(branch ? { branch } : {}),
|
...(branch ? { branch } : {}),
|
||||||
@ -117,6 +119,7 @@ export async function selectAndExecuteTask(
|
|||||||
draft_pr: shouldDraftPr,
|
draft_pr: shouldDraftPr,
|
||||||
...(taskSlug ? { slug: taskSlug } : {}),
|
...(taskSlug ? { slug: taskSlug } : {}),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
const startedAt = new Date().toISOString();
|
const startedAt = new Date().toISOString();
|
||||||
|
|
||||||
let taskSuccess: boolean;
|
let taskSuccess: boolean;
|
||||||
@ -132,9 +135,11 @@ export async function selectAndExecuteTask(
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const completedAt = new Date().toISOString();
|
const completedAt = new Date().toISOString();
|
||||||
|
if (taskRecord) {
|
||||||
persistTaskError(taskRunner, taskRecord, startedAt, completedAt, err, {
|
persistTaskError(taskRunner, taskRecord, startedAt, completedAt, err, {
|
||||||
responsePrefix: 'Task failed: ',
|
responsePrefix: 'Task failed: ',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +165,7 @@ export async function selectAndExecuteTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveSuccess = taskSuccess && !prFailed;
|
const effectiveSuccess = taskSuccess && !prFailed;
|
||||||
|
if (taskRecord) {
|
||||||
const taskResult = buildBooleanTaskResult({
|
const taskResult = buildBooleanTaskResult({
|
||||||
task: taskRecord,
|
task: taskRecord,
|
||||||
taskSuccess: effectiveSuccess,
|
taskSuccess: effectiveSuccess,
|
||||||
@ -171,6 +177,7 @@ export async function selectAndExecuteTask(
|
|||||||
...(isWorktree ? { worktreePath: execCwd } : {}),
|
...(isWorktree ? { worktreePath: execCwd } : {}),
|
||||||
});
|
});
|
||||||
persistTaskResult(taskRunner, taskResult);
|
persistTaskResult(taskRunner, taskResult);
|
||||||
|
}
|
||||||
|
|
||||||
if (!effectiveSuccess) {
|
if (!effectiveSuccess) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -145,4 +145,6 @@ export interface SelectAndExecuteOptions {
|
|||||||
interactiveMetadata?: InteractiveMetadata;
|
interactiveMetadata?: InteractiveMetadata;
|
||||||
/** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */
|
/** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */
|
||||||
issues?: GitHubIssue[];
|
issues?: GitHubIssue[];
|
||||||
|
/** Skip adding task to tasks.yaml */
|
||||||
|
skipTaskList?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user