リトライ時のムーブメント選択で失敗箇所にカーソルを初期配置する
selectOption → selectOptionWithDefault に変更し、前回失敗したムーブメントが デフォルト選択されるようにした。Enter一発で失敗箇所から再開できる。
This commit is contained in:
parent
391e56b51a
commit
a8adfdd02a
@ -2,9 +2,10 @@
|
||||
* Tests for prompt module (cursor-based interactive menu)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Readable } from 'node:stream';
|
||||
import chalk from 'chalk';
|
||||
import { setupRawStdin, restoreStdin } from './helpers/stdinSimulator.js';
|
||||
import type { SelectOptionItem, KeyInputResult } from '../shared/prompt/index.js';
|
||||
import {
|
||||
renderMenu,
|
||||
@ -331,6 +332,74 @@ describe('prompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectOptionWithDefault (stdin E2E)', () => {
|
||||
afterEach(() => {
|
||||
restoreStdin();
|
||||
});
|
||||
|
||||
it('should place cursor on default value and confirm it with Enter', async () => {
|
||||
// Enter key only — confirms whatever the cursor is on
|
||||
setupRawStdin(['\r']);
|
||||
|
||||
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||
const options = [
|
||||
{ label: 'plan', value: 'plan' },
|
||||
{ label: 'implement', value: 'implement' },
|
||||
{ label: 'review', value: 'review' },
|
||||
];
|
||||
|
||||
const result = await selectOptionWithDefault('Start from:', options, 'review');
|
||||
|
||||
// If cursor starts at 'review' (index 2), Enter should select it
|
||||
expect(result).toBe('review');
|
||||
});
|
||||
|
||||
it('should place cursor on first item when default is the first option', async () => {
|
||||
setupRawStdin(['\r']);
|
||||
|
||||
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||
const options = [
|
||||
{ label: 'plan', value: 'plan' },
|
||||
{ label: 'implement', value: 'implement' },
|
||||
{ label: 'review', value: 'review' },
|
||||
];
|
||||
|
||||
const result = await selectOptionWithDefault('Start from:', options, 'plan');
|
||||
|
||||
expect(result).toBe('plan');
|
||||
});
|
||||
|
||||
it('should place cursor on middle item when default is the middle option', async () => {
|
||||
setupRawStdin(['\r']);
|
||||
|
||||
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||
const options = [
|
||||
{ label: 'plan', value: 'plan' },
|
||||
{ label: 'implement', value: 'implement' },
|
||||
{ label: 'review', value: 'review' },
|
||||
];
|
||||
|
||||
const result = await selectOptionWithDefault('Start from:', options, 'implement');
|
||||
|
||||
expect(result).toBe('implement');
|
||||
});
|
||||
|
||||
it('should fall back to first item when default value does not exist', async () => {
|
||||
setupRawStdin(['\r']);
|
||||
|
||||
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||
const options = [
|
||||
{ label: 'plan', value: 'plan' },
|
||||
{ label: 'implement', value: 'implement' },
|
||||
];
|
||||
|
||||
const result = await selectOptionWithDefault('Start from:', options, 'nonexistent');
|
||||
|
||||
// defaultValue not found → falls back to index 0
|
||||
expect(result).toBe('plan');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFullWidth', () => {
|
||||
it('should return true for CJK ideographs', () => {
|
||||
expect(isFullWidth('漢'.codePointAt(0)!)).toBe(true);
|
||||
|
||||
@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
const {
|
||||
mockExistsSync,
|
||||
mockSelectPiece,
|
||||
mockSelectOption,
|
||||
mockSelectOptionWithDefault,
|
||||
mockResolvePieceConfigValue,
|
||||
mockLoadPieceByIdentifier,
|
||||
mockGetPieceDescription,
|
||||
@ -15,7 +15,7 @@ const {
|
||||
} = vi.hoisted(() => ({
|
||||
mockExistsSync: vi.fn(() => true),
|
||||
mockSelectPiece: vi.fn(),
|
||||
mockSelectOption: vi.fn(),
|
||||
mockSelectOptionWithDefault: vi.fn(),
|
||||
mockResolvePieceConfigValue: vi.fn(),
|
||||
mockLoadPieceByIdentifier: vi.fn(),
|
||||
mockGetPieceDescription: vi.fn(() => ({
|
||||
@ -41,7 +41,7 @@ vi.mock('../features/pieceSelection/index.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOption: (...args: unknown[]) => mockSelectOption(...args),
|
||||
selectOptionWithDefault: (...args: unknown[]) => mockSelectOptionWithDefault(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
@ -129,7 +129,7 @@ beforeEach(() => {
|
||||
mockSelectPiece.mockResolvedValue('default');
|
||||
mockResolvePieceConfigValue.mockReturnValue(3);
|
||||
mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
|
||||
mockSelectOption.mockResolvedValue('plan');
|
||||
mockSelectOptionWithDefault.mockResolvedValue('plan');
|
||||
mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
|
||||
mockStartReExecution.mockReturnValue({
|
||||
name: 'my-task',
|
||||
@ -158,9 +158,25 @@ describe('retryFailedTask', () => {
|
||||
expect(mockExecuteAndCompleteTask).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass failed movement as default to selectOptionWithDefault', async () => {
|
||||
const task = makeFailedTask(); // failure.movement = 'review'
|
||||
|
||||
await retryFailedTask(task, '/project');
|
||||
|
||||
expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
|
||||
'Start from movement:',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'plan' }),
|
||||
expect.objectContaining({ value: 'implement' }),
|
||||
expect.objectContaining({ value: 'review' }),
|
||||
]),
|
||||
'review',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass non-initial movement as startMovement', async () => {
|
||||
const task = makeFailedTask();
|
||||
mockSelectOption.mockResolvedValue('implement');
|
||||
mockSelectOptionWithDefault.mockResolvedValue('implement');
|
||||
|
||||
await retryFailedTask(task, '/project');
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import type { TaskListItem } from '../../../infra/task/index.js';
|
||||
import { TaskRunner } from '../../../infra/task/index.js';
|
||||
import { loadPieceByIdentifier, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { selectPiece } from '../../pieceSelection/index.js';
|
||||
import { selectOption } from '../../../shared/prompt/index.js';
|
||||
import { selectOptionWithDefault } from '../../../shared/prompt/index.js';
|
||||
import { info, header, blankLine, status } from '../../../shared/ui/index.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import type { PieceConfig } from '../../../core/models/index.js';
|
||||
@ -60,12 +60,12 @@ async function selectStartMovement(
|
||||
const effectiveDefault = defaultIdx >= 0 ? movements[defaultIdx] : movements[0];
|
||||
|
||||
const options = movements.map((name) => ({
|
||||
label: name === effectiveDefault ? `${name} (default)` : name,
|
||||
label: name,
|
||||
value: name,
|
||||
description: name === pieceConfig.initialMovement ? 'Initial movement' : undefined,
|
||||
}));
|
||||
|
||||
return await selectOption<string>('Start from movement:', options);
|
||||
return await selectOptionWithDefault<string>('Start from movement:', options, effectiveDefault ?? movements[0]!);
|
||||
}
|
||||
|
||||
function buildRetryFailureInfo(task: TaskListItem): RetryFailureInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user