takt/src/__tests__/options-builder.test.ts
nrs dec77e069e
add-model-to-persona-providers (#324)
* takt: add-model-to-persona-providers

* refactor: loadConfigを廃止しresolveConfigValueにキー単位解決を一元化

loadConfig()による一括マージを廃止し、resolveConfigValue()でキーごとに
global/project/piece/envの優先順位を宣言的に解決する方式に移行。
providerOptionsの優先順位をglobal < piece < project < envに修正し、
sourceトラッキングでOptionsBuilderのマージ方向を制御する。
2026-02-20 11:12:46 +09:00

133 lines
3.8 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 default profile when provider_profiles are not provided', () => {
const step = createMovement();
const builder = createBuilder(step, {
provider: undefined,
providerProfiles: undefined,
});
const options = builder.buildBaseOptions(step);
expect(options.permissionMode).toBe('edit');
});
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.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');
});
});