279 lines
9.1 KiB
TypeScript
279 lines
9.1 KiB
TypeScript
/**
|
|
* Tests for instruct mode
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { setupRawStdin, restoreStdin, toRawInputs, createMockProvider } from './helpers/stdinSimulator.js';
|
|
|
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
|
loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })),
|
|
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
|
}));
|
|
|
|
vi.mock('../infra/providers/index.js', () => ({
|
|
getProvider: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('../shared/context.js', () => ({
|
|
isQuietMode: vi.fn(() => false),
|
|
}));
|
|
|
|
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
loadPersonaSessions: vi.fn(() => ({})),
|
|
updatePersonaSession: vi.fn(),
|
|
getProjectConfigDir: vi.fn(() => '/tmp'),
|
|
loadSessionState: vi.fn(() => null),
|
|
clearSessionState: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../shared/ui/index.js', () => ({
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
blankLine: vi.fn(),
|
|
StreamDisplay: vi.fn().mockImplementation(() => ({
|
|
createHandler: vi.fn(() => vi.fn()),
|
|
flush: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('../shared/prompt/index.js', () => ({
|
|
selectOption: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../shared/i18n/index.js', () => ({
|
|
getLabel: vi.fn((_key: string, _lang: string) => 'Mock label'),
|
|
getLabelObject: vi.fn(() => ({
|
|
intro: 'Instruct mode intro',
|
|
resume: 'Resuming',
|
|
noConversation: 'No conversation',
|
|
summarizeFailed: 'Summarize failed',
|
|
continuePrompt: 'Continue',
|
|
proposed: 'Proposed task:',
|
|
actionPrompt: 'What to do?',
|
|
actions: {
|
|
execute: 'Execute',
|
|
saveTask: 'Save task',
|
|
continue: 'Continue',
|
|
},
|
|
cancelled: 'Cancelled',
|
|
})),
|
|
}));
|
|
|
|
vi.mock('../shared/prompts/index.js', () => ({
|
|
loadTemplate: vi.fn((_name: string, _lang: string) => 'Mock template content'),
|
|
}));
|
|
|
|
import { getProvider } from '../infra/providers/index.js';
|
|
import { runInstructMode } from '../features/tasks/list/instructMode.js';
|
|
import { selectOption } from '../shared/prompt/index.js';
|
|
import { info } from '../shared/ui/index.js';
|
|
import { loadTemplate } from '../shared/prompts/index.js';
|
|
|
|
const mockGetProvider = vi.mocked(getProvider);
|
|
const mockSelectOption = vi.mocked(selectOption);
|
|
const mockInfo = vi.mocked(info);
|
|
const mockLoadTemplate = vi.mocked(loadTemplate);
|
|
|
|
function setupMockProvider(responses: string[]): void {
|
|
const { provider } = createMockProvider(responses);
|
|
mockGetProvider.mockReturnValue(provider);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockSelectOption.mockResolvedValue('execute');
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreStdin();
|
|
});
|
|
|
|
describe('runInstructMode', () => {
|
|
it('should return action=cancel when user types /cancel', async () => {
|
|
setupRawStdin(toRawInputs(['/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
expect(result.action).toBe('cancel');
|
|
expect(result.task).toBe('');
|
|
});
|
|
|
|
it('should return action=execute with task on /go after conversation', async () => {
|
|
setupRawStdin(toRawInputs(['add more tests', '/go']));
|
|
setupMockProvider(['What kind of tests?', 'Add unit tests for the feature.']);
|
|
|
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
expect(result.action).toBe('execute');
|
|
expect(result.task).toBe('Add unit tests for the feature.');
|
|
});
|
|
|
|
it('should return action=save_task when user selects save task', async () => {
|
|
setupRawStdin(toRawInputs(['describe task', '/go']));
|
|
setupMockProvider(['response', 'Summarized task.']);
|
|
mockSelectOption.mockResolvedValue('save_task');
|
|
|
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
expect(result.action).toBe('save_task');
|
|
expect(result.task).toBe('Summarized task.');
|
|
});
|
|
|
|
it('should continue editing when user selects continue', async () => {
|
|
setupRawStdin(toRawInputs(['describe task', '/go', '/cancel']));
|
|
setupMockProvider(['response', 'Summarized task.']);
|
|
mockSelectOption.mockResolvedValueOnce('continue');
|
|
|
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
expect(result.action).toBe('cancel');
|
|
});
|
|
|
|
it('should reject /go with no prior conversation', async () => {
|
|
setupRawStdin(toRawInputs(['/go', '/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
const result = await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
expect(result.action).toBe('cancel');
|
|
});
|
|
|
|
it('should exclude execute from action selector options', async () => {
|
|
setupRawStdin(toRawInputs(['task', '/go']));
|
|
setupMockProvider(['response', 'Task summary.']);
|
|
mockSelectOption.mockResolvedValue('save_task');
|
|
|
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '');
|
|
|
|
const selectCall = mockSelectOption.mock.calls.find((call) =>
|
|
Array.isArray(call[1])
|
|
);
|
|
expect(selectCall).toBeDefined();
|
|
const options = selectCall![1] as Array<{ value: string }>;
|
|
const values = options.map((o) => o.value);
|
|
expect(values).not.toContain('execute');
|
|
expect(values).toContain('save_task');
|
|
expect(values).toContain('continue');
|
|
expect(values).not.toContain('create_issue');
|
|
});
|
|
|
|
it('should use dedicated instruct system prompt with task context', async () => {
|
|
setupRawStdin(toRawInputs(['/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', 'existing note');
|
|
|
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
|
'score_instruct_system_prompt',
|
|
'en',
|
|
expect.objectContaining({
|
|
taskName: 'my-task',
|
|
taskContent: 'Do something',
|
|
branchName: 'feature-branch',
|
|
branchContext: 'branch context',
|
|
retryNote: 'existing note',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should inject selected run context into system prompt variables', async () => {
|
|
setupRawStdin(toRawInputs(['/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
const runSessionContext = {
|
|
task: 'Previous run task',
|
|
piece: 'default',
|
|
status: 'completed',
|
|
movementLogs: [
|
|
{ step: 'implement', persona: 'coder', status: 'completed', content: 'done' },
|
|
],
|
|
reports: [
|
|
{ filename: '00-plan.md', content: '# Plan' },
|
|
],
|
|
};
|
|
|
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, runSessionContext);
|
|
|
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
|
'score_instruct_system_prompt',
|
|
'en',
|
|
expect.objectContaining({
|
|
hasRunSession: true,
|
|
runTask: 'Previous run task',
|
|
runPiece: 'default',
|
|
runStatus: 'completed',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should inject previousOrderContent into template variables when provided', async () => {
|
|
setupRawStdin(toRawInputs(['/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, undefined, '# Previous Order\nDo the thing');
|
|
|
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
|
'score_instruct_system_prompt',
|
|
'en',
|
|
expect.objectContaining({
|
|
hasOrderContent: true,
|
|
orderContent: '# Previous Order\nDo the thing',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should set hasOrderContent=false when previousOrderContent is null', async () => {
|
|
setupRawStdin(toRawInputs(['/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
await runInstructMode('/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '', undefined, undefined, null);
|
|
|
|
expect(mockLoadTemplate).toHaveBeenCalledWith(
|
|
'score_instruct_system_prompt',
|
|
'en',
|
|
expect.objectContaining({
|
|
hasOrderContent: false,
|
|
orderContent: '',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should return execute with previous order content on /replay when previousOrderContent is set', async () => {
|
|
setupRawStdin(toRawInputs(['/replay']));
|
|
setupMockProvider([]);
|
|
|
|
const previousOrder = '# Previous Order\nDo the thing';
|
|
const result = await runInstructMode(
|
|
'/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '',
|
|
undefined, undefined, previousOrder,
|
|
);
|
|
|
|
expect(result.action).toBe('execute');
|
|
expect(result.task).toBe(previousOrder);
|
|
});
|
|
|
|
it('should show error and continue when /replay is used without previousOrderContent', async () => {
|
|
setupRawStdin(toRawInputs(['/replay', '/cancel']));
|
|
setupMockProvider([]);
|
|
|
|
const result = await runInstructMode(
|
|
'/project', 'branch context', 'feature-branch', 'my-task', 'Do something', '',
|
|
undefined, undefined, null,
|
|
);
|
|
|
|
expect(result.action).toBe('cancel');
|
|
expect(mockInfo).toHaveBeenCalledWith('Mock label');
|
|
});
|
|
});
|