takt 対話モードの save_task を takt add と同じ worktree 設定フローに統一
takt 対話モードで Save Task を選択した際に worktree/branch/auto_pr の 設定プロンプトがスキップされ、takt run で clone なしに実行されて成果物が 消失するバグを修正。promptWorktreeSettings() を共通関数として抽出し、 saveTaskFromInteractive() と addTask() の両方から使用するようにした。
This commit is contained in:
parent
8d760c1fc7
commit
c0d48df33a
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user