takt: github-issue-328-tasuku-ritora (#340)

This commit is contained in:
nrs 2026-02-22 21:43:25 +09:00 committed by GitHub
parent 9e68f086d4
commit b309233aeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 7 deletions

View File

@ -70,7 +70,11 @@ vi.mock('../features/interactive/index.js', () => ({
loadRunSessionContext: vi.fn(),
getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })),
formatRunSessionForPrompt: vi.fn(() => ({
runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '',
runTask: '',
runPiece: 'default',
runStatus: '',
runMovementLogs: '',
runReports: '',
})),
runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
findPreviousOrderContent: vi.fn(() => null),
@ -91,6 +95,17 @@ vi.mock('../features/tasks/execute/taskExecution.js', () => ({
executeAndCompleteTask: (...args: unknown[]) => mockExecuteAndCompleteTask(...args),
}));
vi.mock('../shared/i18n/index.js', () => ({
getLabel: vi.fn((key: string) => {
const labels: Record<string, string> = {
'retry.workflowPrompt': 'Select workflow:',
'retry.usePreviousWorkflow': 'Use previous',
'retry.changeWorkflow': 'Change workflow',
};
return labels[key] ?? key;
}),
}));
import { retryFailedTask } from '../features/tasks/list/taskRetryActions.js';
import type { TaskListItem } from '../infra/task/types.js';
import type { PieceConfig } from '../core/models/index.js';
@ -262,4 +277,54 @@ describe('retryFailedTask', () => {
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
});
describe('when previous workflow exists', () => {
beforeEach(() => {
mockFindRunForTask.mockReturnValue('run-123');
});
it('should show workflow selection prompt when runInfo.piece exists', async () => {
const task = makeFailedTask();
await retryFailedTask(task, '/project');
expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
'Select workflow:',
expect.arrayContaining([
expect.objectContaining({ value: 'use_previous' }),
expect.objectContaining({ value: 'change' }),
]),
'use_previous',
);
});
it('should use previous workflow when use_previous is selected', async () => {
const task = makeFailedTask();
mockSelectOptionWithDefault.mockResolvedValue('use_previous');
await retryFailedTask(task, '/project');
expect(mockSelectPiece).not.toHaveBeenCalled();
expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
});
it('should call selectPiece when change is selected', async () => {
const task = makeFailedTask();
mockSelectOptionWithDefault.mockResolvedValue('change');
await retryFailedTask(task, '/project');
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
});
it('should return false when workflow selection is cancelled', async () => {
const task = makeFailedTask();
mockSelectOptionWithDefault.mockResolvedValue(null);
const result = await retryFailedTask(task, '/project');
expect(result).toBe(false);
expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
});
});
});

View File

@ -11,6 +11,7 @@ import { TaskRunner } from '../../../infra/task/index.js';
import { loadPieceByIdentifier, resolvePieceConfigValue, getPieceDescription } from '../../../infra/config/index.js';
import { selectPiece } from '../../pieceSelection/index.js';
import { selectOptionWithDefault } from '../../../shared/prompt/index.js';
import { getLabel } from '../../../shared/i18n/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';
@ -128,12 +129,46 @@ export async function retryFailedTask(
displayFailureInfo(task);
const selectedPiece = await selectPiece(projectDir);
if (!selectedPiece) {
const matchedSlug = findRunForTask(worktreePath, task.content);
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
let selectedPiece: string;
if (runInfo?.piece) {
const usePreviousLabel = getLabel('retry.usePreviousWorkflow');
const changeWorkflowLabel = getLabel('retry.changeWorkflow');
const choice = await selectOptionWithDefault(
getLabel('retry.workflowPrompt'),
[
{ label: `${runInfo.piece} - ${usePreviousLabel}`, value: 'use_previous' },
{ label: changeWorkflowLabel, value: 'change' },
],
'use_previous',
);
if (choice === null) {
info('Cancelled');
return false;
}
if (choice === 'use_previous') {
selectedPiece = runInfo.piece;
} else {
const selected = await selectPiece(projectDir);
if (!selected) {
info('Cancelled');
return false;
}
selectedPiece = selected;
}
} else {
const selected = await selectPiece(projectDir);
if (!selected) {
info('Cancelled');
return false;
}
selectedPiece = selected;
}
const previewCount = resolvePieceConfigValue(projectDir, 'interactivePreviewMovements');
const pieceConfig = loadPieceByIdentifier(selectedPiece, projectDir);
@ -155,8 +190,6 @@ export async function retryFailedTask(
};
// Runs data lives in the worktree (written during previous execution)
const matchedSlug = findRunForTask(worktreePath, task.content);
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
const previousOrderContent = findPreviousOrderContent(worktreePath, matchedSlug);
blankLine();

View File

@ -96,6 +96,9 @@ instruct:
retry:
ui:
intro: "Retry mode - describe additional instructions. Commands: /go (create instruction & run), /retry (rerun previous order), /cancel (exit)"
workflowPrompt: "Select workflow:"
usePreviousWorkflow: "Use previous"
changeWorkflow: "Change workflow"
run:
notifyComplete: "Run complete ({total} tasks)"

View File

@ -96,6 +96,9 @@ instruct:
retry:
ui:
intro: "リトライモード - 追加指示を入力してください。コマンド: /go指示書作成・実行, /retry前回の指示書で再実行, /cancel終了"
workflowPrompt: "ワークフローを選択:"
usePreviousWorkflow: "前回のまま使用"
changeWorkflow: "ワークフローを変更"
run:
notifyComplete: "run完了 ({total} tasks)"