レビュー画面に元のタスク指示を表示 & タイムスタンプ短縮
- getOriginalInstruction: ブランチ最初のコミットから元の指示を抽出 - /review-tasks の選択肢と詳細表示に元の指示を追加 - タイムスタンプのハイフンを除去 (2026-01-29T0225 → 20260129T0225)
This commit is contained in:
parent
63d6932c01
commit
ea0d04c4fe
97
src/__tests__/getOriginalInstruction.test.ts
Normal file
97
src/__tests__/getOriginalInstruction.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Tests for getOriginalInstruction
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock child_process.execFileSync
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
const mockExecFileSync = vi.mocked(execFileSync);
|
||||
|
||||
import { getOriginalInstruction } from '../task/worktree.js';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getOriginalInstruction', () => {
|
||||
it('should extract instruction from takt-prefixed commit message', () => {
|
||||
mockExecFileSync.mockReturnValue('takt: 認証機能を追加する\ntakt: fix-auth\n');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-fix-auth');
|
||||
|
||||
expect(result).toBe('認証機能を追加する');
|
||||
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||
'git',
|
||||
['log', '--format=%s', '--reverse', 'main..takt/20260128-fix-auth'],
|
||||
expect.objectContaining({ cwd: '/project', encoding: 'utf-8' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return first commit message without takt prefix if not present', () => {
|
||||
mockExecFileSync.mockReturnValue('Initial implementation\n');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-fix-auth');
|
||||
|
||||
expect(result).toBe('Initial implementation');
|
||||
});
|
||||
|
||||
it('should return empty string when no commits on branch', () => {
|
||||
mockExecFileSync.mockReturnValue('');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-fix-auth');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when git command fails', () => {
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error('not a git repository');
|
||||
});
|
||||
|
||||
const result = getOriginalInstruction('/non-existent', 'main', 'takt/20260128-fix-auth');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle multi-line commit messages (use only first line)', () => {
|
||||
mockExecFileSync.mockReturnValue('takt: Fix the login bug\ntakt: follow-up fix\n');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-fix-login');
|
||||
|
||||
expect(result).toBe('Fix the login bug');
|
||||
});
|
||||
|
||||
it('should return empty string when takt prefix has no content', () => {
|
||||
// "takt: \n" trimmed → "takt:", starts with "takt:" → slice + trim → ""
|
||||
mockExecFileSync.mockReturnValue('takt: \n');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-task');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return instruction text when takt prefix has content', () => {
|
||||
mockExecFileSync.mockReturnValue('takt: add search feature\n');
|
||||
|
||||
const result = getOriginalInstruction('/project', 'main', 'takt/20260128-task');
|
||||
|
||||
expect(result).toBe('add search feature');
|
||||
});
|
||||
|
||||
it('should use correct git range with custom default branch', () => {
|
||||
mockExecFileSync.mockReturnValue('takt: Add search feature\n');
|
||||
|
||||
getOriginalInstruction('/project', 'master', 'takt/20260128-add-search');
|
||||
|
||||
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||
'git',
|
||||
['log', '--format=%s', '--reverse', 'master..takt/20260128-add-search'],
|
||||
expect.objectContaining({ cwd: '/project' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -93,7 +93,7 @@ describe('extractTaskSlug', () => {
|
||||
});
|
||||
|
||||
describe('buildReviewItems', () => {
|
||||
it('should build items with correct task slug', () => {
|
||||
it('should build items with correct task slug and originalInstruction', () => {
|
||||
const branches: BranchInfo[] = [
|
||||
{
|
||||
branch: 'takt/20260128-fix-auth',
|
||||
@ -107,6 +107,8 @@ describe('buildReviewItems', () => {
|
||||
expect(items[0]!.info).toBe(branches[0]);
|
||||
// filesChanged will be 0 since we don't have a real git repo
|
||||
expect(items[0]!.filesChanged).toBe(0);
|
||||
// originalInstruction will be empty since git command fails on non-existent repo
|
||||
expect(items[0]!.originalInstruction).toBe('');
|
||||
});
|
||||
|
||||
it('should handle multiple branches', () => {
|
||||
|
||||
@ -78,6 +78,9 @@ async function showDiffAndPromptAction(
|
||||
): Promise<ReviewAction | null> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan(`=== ${item.info.branch} ===`));
|
||||
if (item.originalInstruction) {
|
||||
console.log(chalk.dim(` ${item.originalInstruction}`));
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Show diff stat
|
||||
@ -353,11 +356,17 @@ export async function reviewTasks(cwd: string): Promise<void> {
|
||||
const items = buildReviewItems(cwd, branches, defaultBranch);
|
||||
|
||||
// Build selection options
|
||||
const options = items.map((item, idx) => ({
|
||||
label: item.info.branch,
|
||||
value: String(idx),
|
||||
description: `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`,
|
||||
}));
|
||||
const options = items.map((item, idx) => {
|
||||
const filesSummary = `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`;
|
||||
const description = item.originalInstruction
|
||||
? `${filesSummary} | ${item.originalInstruction}`
|
||||
: filesSummary;
|
||||
return {
|
||||
label: item.info.branch,
|
||||
value: String(idx),
|
||||
description,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await selectOption<string>(
|
||||
'Review Tasks (Branches)',
|
||||
|
||||
@ -43,13 +43,15 @@ export interface BranchReviewItem {
|
||||
info: BranchInfo;
|
||||
filesChanged: number;
|
||||
taskSlug: string;
|
||||
/** Original task instruction extracted from first commit message */
|
||||
originalInstruction: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a timestamp string for paths/branches
|
||||
*/
|
||||
function generateTimestamp(): string {
|
||||
return new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
||||
return new Date().toISOString().replace(/[-:.]/g, '').slice(0, 13);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,6 +315,41 @@ export function extractTaskSlug(branch: string): string {
|
||||
return withoutTimestamp || name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original task instruction from the first commit message on a branch.
|
||||
*
|
||||
* The first commit on a takt branch has the format: "takt: {original instruction}".
|
||||
* This function retrieves the first commit's message and strips the "takt: " prefix.
|
||||
* Returns empty string if extraction fails.
|
||||
*/
|
||||
export function getOriginalInstruction(
|
||||
cwd: string,
|
||||
defaultBranch: string,
|
||||
branch: string,
|
||||
): string {
|
||||
try {
|
||||
// Get the first commit message on the branch (oldest first)
|
||||
const output = execFileSync(
|
||||
'git',
|
||||
['log', '--format=%s', '--reverse', `${defaultBranch}..${branch}`],
|
||||
{ cwd, encoding: 'utf-8', stdio: 'pipe' },
|
||||
).trim();
|
||||
|
||||
if (!output) return '';
|
||||
|
||||
const firstLine = output.split('\n')[0] || '';
|
||||
// Strip "takt: " prefix if present
|
||||
const TAKT_COMMIT_PREFIX = 'takt:';
|
||||
if (firstLine.startsWith(TAKT_COMMIT_PREFIX)) {
|
||||
return firstLine.slice(TAKT_COMMIT_PREFIX.length).trim();
|
||||
}
|
||||
|
||||
return firstLine;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build review items from branch list, enriching with diff stats.
|
||||
*/
|
||||
@ -325,5 +362,6 @@ export function buildReviewItems(
|
||||
info: br,
|
||||
filesChanged: getFilesChanged(projectDir, defaultBranch, br.branch),
|
||||
taskSlug: extractTaskSlug(br.branch),
|
||||
originalInstruction: getOriginalInstruction(projectDir, defaultBranch, br.branch),
|
||||
}));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user