takt/src/__tests__/options-builder.test.ts

213 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { OptionsBuilder } from '../core/piece/engine/OptionsBuilder.js';
import type { PieceMovement } from '../core/models/types.js';
import type { PieceEngineOptions } from '../core/piece/types.js';
function createMovement(overrides: Partial<PieceMovement> = {}): PieceMovement {
return {
name: 'reviewers',
personaDisplayName: 'Reviewers',
instructionTemplate: 'review',
passPreviousResponse: false,
...overrides,
};
}
function createBuilder(step: PieceMovement, engineOverrides: Partial<PieceEngineOptions> = {}): OptionsBuilder {
const engineOptions: PieceEngineOptions = {
projectCwd: '/project',
provider: 'codex',
providerProfiles: {
codex: {
defaultPermissionMode: 'full',
},
},
...engineOverrides,
};
return new OptionsBuilder(
engineOptions,
() => '/project',
() => '/project',
() => undefined,
() => '.takt/runs/sample/reports',
() => 'ja',
() => [{ name: step.name }],
() => 'default',
() => 'test piece',
);
}
describe('OptionsBuilder.buildBaseOptions', () => {
it('resolves permission mode using provider profiles', () => {
const step = createMovement();
const builder = createBuilder(step);
const options = builder.buildBaseOptions(step);
expect(options.permissionMode).toBe('full');
});
it('applies movement requiredPermissionMode as minimum floor', () => {
const step = createMovement({ requiredPermissionMode: 'full' });
const builder = createBuilder(step);
const options = builder.buildBaseOptions(step);
expect(options.permissionMode).toBe('full');
});
it('uses readonly when provider is not configured', () => {
const step = createMovement();
const builder = createBuilder(step, {
provider: undefined,
providerProfiles: undefined,
});
const options = builder.buildBaseOptions(step);
expect(options.permissionMode).toBe('readonly');
});
it('merges provider options with precedence: global < movement < project', () => {
const step = createMovement({
providerOptions: {
codex: { networkAccess: false },
claude: { sandbox: { excludedCommands: ['./gradlew'] } },
},
});
const builder = createBuilder(step, {
providerOptionsSource: 'project',
providerOptions: {
codex: { networkAccess: true },
claude: { sandbox: { allowUnsandboxedCommands: true } },
opencode: { networkAccess: true },
},
});
const options = builder.buildBaseOptions(step);
expect(options.providerOptions).toEqual({
codex: { networkAccess: true },
opencode: { networkAccess: true },
claude: {
sandbox: {
allowUnsandboxedCommands: true,
excludedCommands: ['./gradlew'],
},
},
});
});
it('falls back to global/project provider options when movement has none', () => {
const step = createMovement();
const builder = createBuilder(step, {
providerOptions: {
codex: { networkAccess: false },
},
});
const options = builder.buildBaseOptions(step);
expect(options.providerOptions).toEqual({
codex: { networkAccess: false },
});
});
});
describe('OptionsBuilder.resolveStepProviderModel', () => {
it('should return engine-level provider and model when step has no overrides', () => {
const step = createMovement();
const builder = createBuilder(step, { provider: 'claude', model: 'sonnet' });
const result = builder.resolveStepProviderModel(step);
expect(result.provider).toBe('claude');
expect(result.model).toBe('sonnet');
});
it('should prioritize persona providers over engine-level provider', () => {
const step = createMovement({ personaDisplayName: 'coder' });
const builder = createBuilder(step, {
provider: 'claude',
model: 'sonnet',
personaProviders: { coder: { provider: 'codex', model: 'o3-mini' } },
});
const result = builder.resolveStepProviderModel(step);
expect(result.provider).toBe('codex');
expect(result.model).toBe('o3-mini');
});
it('should prioritize step-level provider over engine-level provider', () => {
const step = createMovement({ provider: 'opencode' as 'opencode' });
const builder = createBuilder(step, { provider: 'claude' });
const result = builder.resolveStepProviderModel(step);
expect(result.provider).toBe('opencode');
});
it('should prioritize persona providers over step-level provider', () => {
const step = createMovement({ personaDisplayName: 'coder', provider: 'claude' as 'claude' });
const builder = createBuilder(step, {
provider: 'mock',
personaProviders: { coder: { provider: 'codex' } },
});
const result = builder.resolveStepProviderModel(step);
expect(result.provider).toBe('codex');
});
it('should return undefined model when no model is configured', () => {
const step = createMovement();
const builder = createBuilder(step, { provider: 'claude', model: undefined });
const result = builder.resolveStepProviderModel(step);
expect(result.model).toBeUndefined();
});
it('should return undefined provider when no provider is configured', () => {
const step = createMovement();
const builder = createBuilder(step, { provider: undefined });
const result = builder.resolveStepProviderModel(step);
expect(result.provider).toBeUndefined();
});
it('should match buildBaseOptions stepProvider and stepModel', () => {
const step = createMovement({ personaDisplayName: 'coder' });
const builder = createBuilder(step, {
provider: 'claude',
model: 'sonnet',
personaProviders: { coder: { provider: 'codex', model: 'o3-mini' } },
});
const providerInfo = builder.resolveStepProviderModel(step);
const baseOptions = builder.buildBaseOptions(step);
expect(providerInfo.provider).toBe(baseOptions.stepProvider);
expect(providerInfo.model).toBe(baseOptions.stepModel);
});
});
describe('OptionsBuilder.buildResumeOptions', () => {
it('should enforce readonly permission and empty allowedTools for report/status phases', () => {
// Given
const step = createMovement({ requiredPermissionMode: 'full' });
const builder = createBuilder(step);
// When
const options = builder.buildResumeOptions(step, 'session-123', { maxTurns: 3 });
// Then
expect(options.permissionMode).toBe('readonly');
expect(options.allowedTools).toEqual([]);
expect(options.maxTurns).toBe(3);
expect(options.sessionId).toBe('session-123');
});
});