takt: cli起動時、ワークフローを選択したあとworktreeをつくる確認して、必要なら作る機能を追加せよ
This commit is contained in:
parent
80626411cf
commit
56c939c9c0
166
src/__tests__/cli-worktree.test.ts
Normal file
166
src/__tests__/cli-worktree.test.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Tests for confirmAndCreateWorktree (CLI worktree confirmation flow)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock dependencies before importing the module under test
|
||||||
|
vi.mock('../prompt/index.js', () => ({
|
||||||
|
confirm: vi.fn(),
|
||||||
|
selectOptionWithDefault: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../task/worktree.js', () => ({
|
||||||
|
createWorktree: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../task/autoCommit.js', () => ({
|
||||||
|
autoCommitWorktree: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/ui.js', () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn(),
|
||||||
|
header: vi.fn(),
|
||||||
|
status: vi.fn(),
|
||||||
|
setLogLevel: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../utils/debug.js', () => ({
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
initDebugLogger: vi.fn(),
|
||||||
|
setVerboseConsole: vi.fn(),
|
||||||
|
getDebugLogFile: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../config/index.js', () => ({
|
||||||
|
initGlobalDirs: vi.fn(),
|
||||||
|
initProjectDirs: vi.fn(),
|
||||||
|
loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })),
|
||||||
|
getEffectiveDebugConfig: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../config/paths.js', () => ({
|
||||||
|
clearAgentSessions: vi.fn(),
|
||||||
|
getCurrentWorkflow: vi.fn(() => 'default'),
|
||||||
|
isVerboseMode: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../commands/index.js', () => ({
|
||||||
|
executeTask: vi.fn(),
|
||||||
|
runAllTasks: vi.fn(),
|
||||||
|
showHelp: vi.fn(),
|
||||||
|
switchWorkflow: vi.fn(),
|
||||||
|
switchConfig: vi.fn(),
|
||||||
|
addTask: vi.fn(),
|
||||||
|
refreshBuiltin: vi.fn(),
|
||||||
|
watchTasks: vi.fn(),
|
||||||
|
reviewTasks: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../config/workflowLoader.js', () => ({
|
||||||
|
listWorkflows: vi.fn(() => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../constants.js', () => ({
|
||||||
|
DEFAULT_WORKFLOW_NAME: 'default',
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { confirm } from '../prompt/index.js';
|
||||||
|
import { createWorktree } from '../task/worktree.js';
|
||||||
|
import { info } from '../utils/ui.js';
|
||||||
|
import { confirmAndCreateWorktree } from '../cli.js';
|
||||||
|
|
||||||
|
const mockConfirm = vi.mocked(confirm);
|
||||||
|
const mockCreateWorktree = vi.mocked(createWorktree);
|
||||||
|
const mockInfo = vi.mocked(info);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('confirmAndCreateWorktree', () => {
|
||||||
|
it('should return original cwd when user declines worktree creation', async () => {
|
||||||
|
// Given: user says "no" to worktree creation
|
||||||
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await confirmAndCreateWorktree('/project', 'fix-auth');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.execCwd).toBe('/project');
|
||||||
|
expect(result.isWorktree).toBe(false);
|
||||||
|
expect(mockCreateWorktree).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create worktree and return worktree path when user confirms', async () => {
|
||||||
|
// Given: user says "yes" to worktree creation
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockCreateWorktree.mockReturnValue({
|
||||||
|
path: '/project/.takt/worktrees/20260128T0504-fix-auth',
|
||||||
|
branch: 'takt/20260128T0504-fix-auth',
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await confirmAndCreateWorktree('/project', 'fix-auth');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.execCwd).toBe('/project/.takt/worktrees/20260128T0504-fix-auth');
|
||||||
|
expect(result.isWorktree).toBe(true);
|
||||||
|
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
||||||
|
worktree: true,
|
||||||
|
taskSlug: 'fix-auth',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display worktree info when created', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockCreateWorktree.mockReturnValue({
|
||||||
|
path: '/project/.takt/worktrees/20260128T0504-my-task',
|
||||||
|
branch: 'takt/20260128T0504-my-task',
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await confirmAndCreateWorktree('/project', 'my-task');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
'Worktree created: /project/.takt/worktrees/20260128T0504-my-task (branch: takt/20260128T0504-my-task)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call confirm with default=false', async () => {
|
||||||
|
// Given
|
||||||
|
mockConfirm.mockResolvedValue(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await confirmAndCreateWorktree('/project', 'task');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockConfirm).toHaveBeenCalledWith('Create worktree?', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass task as taskSlug to createWorktree', async () => {
|
||||||
|
// Given: Japanese task name
|
||||||
|
mockConfirm.mockResolvedValue(true);
|
||||||
|
mockCreateWorktree.mockReturnValue({
|
||||||
|
path: '/project/.takt/worktrees/20260128T0504-task',
|
||||||
|
branch: 'takt/20260128T0504-task',
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
await confirmAndCreateWorktree('/project', '認証機能を追加する');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateWorktree).toHaveBeenCalledWith('/project', {
|
||||||
|
worktree: true,
|
||||||
|
taskSlug: '認証機能を追加する',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/cli.ts
49
src/cli.ts
@ -35,11 +35,41 @@ import {
|
|||||||
reviewTasks,
|
reviewTasks,
|
||||||
} from './commands/index.js';
|
} from './commands/index.js';
|
||||||
import { listWorkflows } from './config/workflowLoader.js';
|
import { listWorkflows } from './config/workflowLoader.js';
|
||||||
import { selectOptionWithDefault } from './prompt/index.js';
|
import { selectOptionWithDefault, confirm } from './prompt/index.js';
|
||||||
|
import { createWorktree } from './task/worktree.js';
|
||||||
|
import { autoCommitWorktree } from './task/autoCommit.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||||
|
|
||||||
const log = createLogger('cli');
|
const log = createLogger('cli');
|
||||||
|
|
||||||
|
export interface WorktreeConfirmationResult {
|
||||||
|
execCwd: string;
|
||||||
|
isWorktree: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask user whether to create a worktree, and create one if confirmed.
|
||||||
|
* Returns the execution directory and whether a worktree was created.
|
||||||
|
*/
|
||||||
|
export async function confirmAndCreateWorktree(
|
||||||
|
cwd: string,
|
||||||
|
task: string,
|
||||||
|
): Promise<WorktreeConfirmationResult> {
|
||||||
|
const useWorktree = await confirm('Create worktree?', false);
|
||||||
|
|
||||||
|
if (!useWorktree) {
|
||||||
|
return { execCwd: cwd, isWorktree: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = createWorktree(cwd, {
|
||||||
|
worktree: true,
|
||||||
|
taskSlug: task,
|
||||||
|
});
|
||||||
|
info(`Worktree created: ${result.path} (branch: ${result.branch})`);
|
||||||
|
|
||||||
|
return { execCwd: result.path, isWorktree: true };
|
||||||
|
}
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
program
|
program
|
||||||
@ -183,8 +213,21 @@ program
|
|||||||
selectedWorkflow = selected;
|
selectedWorkflow = selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Starting task execution', { task, workflow: selectedWorkflow });
|
// Ask whether to create a worktree
|
||||||
const taskSuccess = await executeTask(task, cwd, selectedWorkflow);
|
const { execCwd, isWorktree } = await confirmAndCreateWorktree(cwd, task);
|
||||||
|
|
||||||
|
log.info('Starting task execution', { task, workflow: selectedWorkflow, worktree: isWorktree });
|
||||||
|
const taskSuccess = await executeTask(task, execCwd, selectedWorkflow);
|
||||||
|
|
||||||
|
if (taskSuccess && isWorktree) {
|
||||||
|
const commitResult = autoCommitWorktree(execCwd, task);
|
||||||
|
if (commitResult.success && commitResult.commitHash) {
|
||||||
|
success(`Auto-committed: ${commitResult.commitHash}`);
|
||||||
|
} else if (!commitResult.success) {
|
||||||
|
error(`Auto-commit failed: ${commitResult.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!taskSuccess) {
|
if (!taskSuccess) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user