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(),
|
blankLine: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
promptInput: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
...(await importOriginal<Record<string, unknown>>()),
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
@ -28,11 +33,14 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
|
|
||||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||||
import { success, info } from '../shared/ui/index.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';
|
import { saveTaskFile, saveTaskFromInteractive } from '../features/tasks/add/index.js';
|
||||||
|
|
||||||
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
||||||
const mockSuccess = vi.mocked(success);
|
const mockSuccess = vi.mocked(success);
|
||||||
const mockInfo = vi.mocked(info);
|
const mockInfo = vi.mocked(info);
|
||||||
|
const mockConfirm = vi.mocked(confirm);
|
||||||
|
const mockPromptInput = vi.mocked(promptInput);
|
||||||
|
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
|
|
||||||
@ -163,16 +171,82 @@ describe('saveTaskFile', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('saveTaskFromInteractive', () => {
|
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
|
// When
|
||||||
await saveTaskFromInteractive(testDir, 'Task content');
|
await saveTaskFromInteractive(testDir, 'Task content');
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml');
|
expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml');
|
||||||
expect(mockInfo).toHaveBeenCalledWith(expect.stringContaining('Path:'));
|
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 () => {
|
it('should display piece info when specified', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await saveTaskFromInteractive(testDir, 'Task content', 'review');
|
await saveTaskFromInteractive(testDir, 'Task content', 'review');
|
||||||
|
|
||||||
@ -181,6 +255,9 @@ describe('saveTaskFromInteractive', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include piece in saved YAML', async () => {
|
it('should include piece in saved YAML', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await saveTaskFromInteractive(testDir, 'Task content', 'custom');
|
await saveTaskFromInteractive(testDir, 'Task content', 'custom');
|
||||||
|
|
||||||
@ -193,6 +270,9 @@ describe('saveTaskFromInteractive', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not display piece info when not specified', async () => {
|
it('should not display piece info when not specified', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await saveTaskFromInteractive(testDir, 'Task content');
|
await saveTaskFromInteractive(testDir, 'Task content');
|
||||||
|
|
||||||
@ -202,4 +282,18 @@ describe('saveTaskFromInteractive', () => {
|
|||||||
);
|
);
|
||||||
expect(pieceInfoCalls.length).toBe(0);
|
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.
|
* 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(
|
export async function saveTaskFromInteractive(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
task: string,
|
task: string,
|
||||||
piece?: string,
|
piece?: string,
|
||||||
): Promise<void> {
|
): 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);
|
const filename = path.basename(filePath);
|
||||||
success(`Task created: ${filename}`);
|
success(`Task created: ${filename}`);
|
||||||
info(` Path: ${filePath}`);
|
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}`);
|
if (piece) info(` Piece: ${piece}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,43 +206,25 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. ワークツリー/ブランチ/PR設定
|
// 3. ワークツリー/ブランチ/PR設定
|
||||||
let worktree: boolean | string | undefined;
|
const settings = await promptWorktreeSettings();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// YAMLファイル作成
|
// YAMLファイル作成
|
||||||
const filePath = await saveTaskFile(cwd, taskContent, {
|
const filePath = await saveTaskFile(cwd, taskContent, {
|
||||||
piece,
|
piece,
|
||||||
issue: issueNumber,
|
issue: issueNumber,
|
||||||
worktree,
|
...settings,
|
||||||
branch,
|
|
||||||
autoPr,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filename = path.basename(filePath);
|
const filename = path.basename(filePath);
|
||||||
success(`Task created: ${filename}`);
|
success(`Task created: ${filename}`);
|
||||||
info(` Path: ${filePath}`);
|
info(` Path: ${filePath}`);
|
||||||
if (worktree) {
|
if (settings.worktree) {
|
||||||
info(` Worktree: ${typeof worktree === 'string' ? worktree : 'auto'}`);
|
info(` Worktree: ${typeof settings.worktree === 'string' ? settings.worktree : 'auto'}`);
|
||||||
}
|
}
|
||||||
if (branch) {
|
if (settings.branch) {
|
||||||
info(` Branch: ${branch}`);
|
info(` Branch: ${settings.branch}`);
|
||||||
}
|
}
|
||||||
if (autoPr) {
|
if (settings.autoPr) {
|
||||||
info(` Auto-PR: yes`);
|
info(` Auto-PR: yes`);
|
||||||
}
|
}
|
||||||
if (piece) {
|
if (piece) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user