takt/src/__tests__/requeueHelpers.test.ts
2026-03-04 23:07:47 +09:00

164 lines
5.3 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest';
const {
mockDebug,
mockConfirm,
mockGetLabel,
mockSelectPiece,
mockIsPiecePath,
mockLoadAllPiecesWithSources,
} = vi.hoisted(() => ({
mockDebug: vi.fn(),
mockConfirm: vi.fn(),
mockGetLabel: vi.fn((_key: string, _lang?: string, vars?: Record<string, string>) => `Use previous piece "${vars?.piece ?? ''}"?`),
mockSelectPiece: vi.fn(),
mockIsPiecePath: vi.fn(() => false),
mockLoadAllPiecesWithSources: vi.fn(() => new Map<string, unknown>([['default', {}], ['selected-piece', {}]])),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({
debug: (...args: unknown[]) => mockDebug(...args),
info: vi.fn(),
error: vi.fn(),
enter: vi.fn(),
exit: vi.fn(),
}),
}));
vi.mock('../shared/prompt/index.js', () => ({
confirm: (...args: unknown[]) => mockConfirm(...args),
}));
vi.mock('../shared/i18n/index.js', () => ({
getLabel: (...args: unknown[]) => mockGetLabel(...args),
}));
vi.mock('../features/pieceSelection/index.js', () => ({
selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
}));
vi.mock('../infra/config/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
isPiecePath: (...args: unknown[]) => mockIsPiecePath(...args),
loadAllPiecesWithSources: (...args: unknown[]) => mockLoadAllPiecesWithSources(...args),
}));
import { hasDeprecatedProviderConfig, selectPieceWithOptionalReuse } from '../features/tasks/list/requeueHelpers.js';
describe('hasDeprecatedProviderConfig', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('YAML parse エラーを debug 記録しつつ有効な候補で判定を続行する', () => {
const orderContent = [
'```yaml',
'movements: [',
'```',
'',
'```yaml',
'movements:',
' - name: review',
' provider_options:',
' codex:',
' network_access: true',
'```',
].join('\n');
expect(hasDeprecatedProviderConfig(orderContent)).toBe(true);
expect(mockDebug).toHaveBeenCalledTimes(1);
expect(mockDebug).toHaveBeenCalledWith(
'Failed to parse YAML candidate for deprecated provider config detection',
expect.objectContaining({ error: expect.any(String) }),
);
});
it('provider block 新記法のみの piece config は deprecated と判定しない', () => {
const orderContent = [
'movements:',
' - name: review',
' provider:',
' type: codex',
' model: gpt-5.3',
' network_access: true',
].join('\n');
expect(hasDeprecatedProviderConfig(orderContent)).toBe(false);
});
it('provider object と同階層 model の旧記法を deprecated と判定する', () => {
const orderContent = [
'movements:',
' - name: review',
' provider:',
' type: codex',
' network_access: true',
' model: gpt-5.3',
].join('\n');
expect(hasDeprecatedProviderConfig(orderContent)).toBe(true);
});
it('循環参照を含む YAML でもスタックオーバーフローせず判定できる', () => {
const orderContent = [
'movements:',
' - &step',
' name: review',
' provider:',
' type: codex',
' model: gpt-5.3',
' network_access: true',
' self: *step',
].join('\n');
expect(hasDeprecatedProviderConfig(orderContent)).toBe(false);
});
});
describe('selectPieceWithOptionalReuse', () => {
beforeEach(() => {
vi.clearAllMocks();
mockIsPiecePath.mockReturnValue(false);
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([['default', {}], ['selected-piece', {}]]));
mockSelectPiece.mockResolvedValue('selected-piece');
});
it('内部ヘルパーを公開 API に露出しない', async () => {
const requeueHelpersModule = await import('../features/tasks/list/requeueHelpers.js');
expect(Object.prototype.hasOwnProperty.call(requeueHelpersModule, 'resolveReusablePieceName')).toBe(false);
});
it('前回 piece 再利用を確認して Yes ならそのまま返す', async () => {
mockConfirm.mockResolvedValue(true);
const selected = await selectPieceWithOptionalReuse('/project', 'default', 'en');
expect(selected).toBe('default');
expect(mockConfirm).toHaveBeenCalledTimes(1);
expect(mockSelectPiece).not.toHaveBeenCalled();
});
it('前回 piece 再利用を拒否した場合は piece 選択にフォールバックする', async () => {
mockConfirm.mockResolvedValue(false);
const selected = await selectPieceWithOptionalReuse('/project', 'default', 'en');
expect(selected).toBe('selected-piece');
expect(mockConfirm).toHaveBeenCalledTimes(1);
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
});
it('未登録の前回 piece 名は確認せず拒否して piece 選択にフォールバックする', async () => {
mockLoadAllPiecesWithSources.mockReturnValue(new Map<string, unknown>([['default', {}]]));
const selected = await selectPieceWithOptionalReuse('/project', 'tampered-piece', 'en');
expect(selected).toBe('selected-piece');
expect(mockConfirm).not.toHaveBeenCalled();
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
});
});