takt 対話モードの save_task を takt add と同じ worktree 設定フローに統一

takt 対話モードで Save Task を選択した際に worktree/branch/auto_pr の
設定プロンプトがスキップされ、takt run で clone なしに実行されて成果物が
消失するバグを修正。promptWorktreeSettings() を共通関数として抽出し、
saveTaskFromInteractive() と addTask() の両方から使用するようにした。
This commit is contained in:
nrslib 2026-02-08 20:26:26 +09:00
parent 8d760c1fc7
commit c0d48df33a
2 changed files with 137 additions and 28 deletions

View File

@ -17,6 +17,11 @@ vi.mock('../shared/ui/index.js', () => ({
blankLine: vi.fn(),
}));
vi.mock('../shared/prompt/index.js', () => ({
confirm: vi.fn(),
promptInput: vi.fn(),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({
@ -28,11 +33,14 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
import { summarizeTaskName } from '../infra/task/summarize.js';
import { success, info } from '../shared/ui/index.js';
import { confirm, promptInput } from '../shared/prompt/index.js';
import { saveTaskFile, saveTaskFromInteractive } from '../features/tasks/add/index.js';
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
const mockSuccess = vi.mocked(success);
const mockInfo = vi.mocked(info);
const mockConfirm = vi.mocked(confirm);
const mockPromptInput = vi.mocked(promptInput);
let testDir: string;
@ -163,16 +171,82 @@ describe('saveTaskFile', () => {
});
describe('saveTaskFromInteractive', () => {
it('should save task and display success message', async () => {
it('should save task with worktree settings when user confirms worktree', async () => {
// Given: user confirms worktree, accepts defaults, confirms auto-PR
mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes
mockPromptInput.mockResolvedValueOnce(''); // Worktree path → auto
mockPromptInput.mockResolvedValueOnce(''); // Branch name → auto
mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes
// When
await saveTaskFromInteractive(testDir, 'Task content');
// Then
expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml');
expect(mockInfo).toHaveBeenCalledWith(expect.stringContaining('Path:'));
const tasksDir = path.join(testDir, '.takt', 'tasks');
const files = fs.readdirSync(tasksDir);
const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8');
expect(content).toContain('worktree: true');
expect(content).toContain('auto_pr: true');
});
it('should save task without worktree settings when user declines worktree', async () => {
// Given: user declines worktree
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
// When
await saveTaskFromInteractive(testDir, 'Task content');
// Then
expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml');
const tasksDir = path.join(testDir, '.takt', 'tasks');
const files = fs.readdirSync(tasksDir);
const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8');
expect(content).not.toContain('worktree:');
expect(content).not.toContain('branch:');
expect(content).not.toContain('auto_pr:');
});
it('should save custom worktree path and branch when specified', async () => {
// Given: user specifies custom path and branch
mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes
mockPromptInput.mockResolvedValueOnce('/custom/path'); // Worktree path
mockPromptInput.mockResolvedValueOnce('feat/branch'); // Branch name
mockConfirm.mockResolvedValueOnce(false); // Auto-create PR? → No
// When
await saveTaskFromInteractive(testDir, 'Task content');
// Then
const tasksDir = path.join(testDir, '.takt', 'tasks');
const files = fs.readdirSync(tasksDir);
const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8');
expect(content).toContain('worktree: /custom/path');
expect(content).toContain('branch: feat/branch');
expect(content).toContain('auto_pr: false');
});
it('should display worktree/branch/auto-PR info when settings are provided', async () => {
// Given
mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes
mockPromptInput.mockResolvedValueOnce('/my/path'); // Worktree path
mockPromptInput.mockResolvedValueOnce('my-branch'); // Branch name
mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes
// When
await saveTaskFromInteractive(testDir, 'Task content');
// Then
expect(mockInfo).toHaveBeenCalledWith(' Worktree: /my/path');
expect(mockInfo).toHaveBeenCalledWith(' Branch: my-branch');
expect(mockInfo).toHaveBeenCalledWith(' Auto-PR: yes');
});
it('should display piece info when specified', async () => {
// Given
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
// When
await saveTaskFromInteractive(testDir, 'Task content', 'review');
@ -181,6 +255,9 @@ describe('saveTaskFromInteractive', () => {
});
it('should include piece in saved YAML', async () => {
// Given
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
// When
await saveTaskFromInteractive(testDir, 'Task content', 'custom');
@ -193,6 +270,9 @@ describe('saveTaskFromInteractive', () => {
});
it('should not display piece info when not specified', async () => {
// Given
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
// When
await saveTaskFromInteractive(testDir, 'Task content');
@ -202,4 +282,18 @@ describe('saveTaskFromInteractive', () => {
);
expect(pieceInfoCalls.length).toBe(0);
});
it('should display auto worktree info when no custom path', async () => {
// Given
mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes
mockPromptInput.mockResolvedValueOnce(''); // Worktree path → auto
mockPromptInput.mockResolvedValueOnce(''); // Branch name → auto
mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes
// When
await saveTaskFromInteractive(testDir, 'Task content');
// Then
expect(mockInfo).toHaveBeenCalledWith(' Worktree: auto');
});
});

View File

@ -87,19 +87,52 @@ export function createIssueFromTask(task: string): void {
}
}
interface WorktreeSettings {
worktree?: boolean | string;
branch?: string;
autoPr?: boolean;
}
async function promptWorktreeSettings(): Promise<WorktreeSettings> {
const useWorktree = await confirm('Create worktree?', true);
if (!useWorktree) {
return {};
}
const customPath = await promptInput('Worktree path (Enter for auto)');
const worktree: boolean | string = customPath || true;
const customBranch = await promptInput('Branch name (Enter for auto)');
const branch = customBranch || undefined;
const autoPr = await confirm('Auto-create PR?', true);
return { worktree, branch, autoPr };
}
/**
* Save a task from interactive mode result.
* Does not prompt for worktree/branch settings.
* Prompts for worktree/branch/auto_pr settings before saving.
*/
export async function saveTaskFromInteractive(
cwd: string,
task: string,
piece?: string,
): Promise<void> {
const filePath = await saveTaskFile(cwd, task, { piece });
const settings = await promptWorktreeSettings();
const filePath = await saveTaskFile(cwd, task, { piece, ...settings });
const filename = path.basename(filePath);
success(`Task created: ${filename}`);
info(` Path: ${filePath}`);
if (settings.worktree) {
info(` Worktree: ${typeof settings.worktree === 'string' ? settings.worktree : 'auto'}`);
}
if (settings.branch) {
info(` Branch: ${settings.branch}`);
}
if (settings.autoPr) {
info(` Auto-PR: yes`);
}
if (piece) info(` Piece: ${piece}`);
}
@ -173,43 +206,25 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
}
// 3. ワークツリー/ブランチ/PR設定
let worktree: boolean | string | undefined;
let branch: string | undefined;
let autoPr: boolean | undefined;
const useWorktree = await confirm('Create worktree?', true);
if (useWorktree) {
const customPath = await promptInput('Worktree path (Enter for auto)');
worktree = customPath || true;
const customBranch = await promptInput('Branch name (Enter for auto)');
if (customBranch) {
branch = customBranch;
}
// PR確認worktreeが有効な場合のみ
autoPr = await confirm('Auto-create PR?', true);
}
const settings = await promptWorktreeSettings();
// YAMLファイル作成
const filePath = await saveTaskFile(cwd, taskContent, {
piece,
issue: issueNumber,
worktree,
branch,
autoPr,
...settings,
});
const filename = path.basename(filePath);
success(`Task created: ${filename}`);
info(` Path: ${filePath}`);
if (worktree) {
info(` Worktree: ${typeof worktree === 'string' ? worktree : 'auto'}`);
if (settings.worktree) {
info(` Worktree: ${typeof settings.worktree === 'string' ? settings.worktree : 'auto'}`);
}
if (branch) {
info(` Branch: ${branch}`);
if (settings.branch) {
info(` Branch: ${settings.branch}`);
}
if (autoPr) {
if (settings.autoPr) {
info(` Auto-PR: yes`);
}
if (piece) {