takt/src/__tests__/engine-provider-options.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

137 lines
3.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { rmSync } from 'node:fs';
vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(),
}));
vi.mock('../core/piece/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(),
}));
vi.mock('../core/piece/phase-runner.js', () => ({
needsStatusJudgmentPhase: vi.fn(),
runReportPhase: vi.fn(),
runStatusJudgmentPhase: vi.fn(),
}));
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
}));
import { PieceEngine } from '../core/piece/index.js';
import { runAgent } from '../agents/runner.js';
import {
applyDefaultMocks,
cleanupPieceEngine,
createTestTmpDir,
makeMovement,
makeResponse,
makeRule,
mockDetectMatchedRuleSequence,
mockRunAgentSequence,
} from './engine-test-helpers.js';
import type { PieceConfig } from '../core/models/index.js';
describe('PieceEngine provider_options resolution', () => {
let tmpDir: string;
let engine: PieceEngine | undefined;
beforeEach(() => {
vi.resetAllMocks();
applyDefaultMocks();
tmpDir = createTestTmpDir();
});
afterEach(() => {
if (engine) {
cleanupPieceEngine(engine);
engine = undefined;
}
if (tmpDir) {
rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should merge provider_options in order: global < piece/movement < project', async () => {
const movement = makeMovement('implement', {
providerOptions: {
codex: { networkAccess: false },
claude: { sandbox: { excludedCommands: ['./gradlew'] } },
},
rules: [makeRule('done', 'COMPLETE')],
});
const config: PieceConfig = {
name: 'provider-options-priority',
movements: [movement],
initialMovement: 'implement',
maxMovements: 1,
};
mockRunAgentSequence([
makeResponse({ persona: movement.persona, content: 'done' }),
]);
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
engine = new PieceEngine(config, tmpDir, 'test task', {
projectCwd: tmpDir,
provider: 'claude',
providerOptionsSource: 'project',
providerOptions: {
codex: { networkAccess: true },
claude: { sandbox: { allowUnsandboxedCommands: false } },
opencode: { networkAccess: true },
},
});
await engine.run();
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
expect(options?.providerOptions).toEqual({
codex: { networkAccess: true },
opencode: { networkAccess: true },
claude: {
sandbox: {
allowUnsandboxedCommands: false,
excludedCommands: ['./gradlew'],
},
},
});
});
it('should pass global provider_options when project and movement options are absent', async () => {
const movement = makeMovement('implement', {
rules: [makeRule('done', 'COMPLETE')],
});
const config: PieceConfig = {
name: 'provider-options-global-only',
movements: [movement],
initialMovement: 'implement',
maxMovements: 1,
};
mockRunAgentSequence([
makeResponse({ persona: movement.persona, content: 'done' }),
]);
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
engine = new PieceEngine(config, tmpDir, 'test task', {
projectCwd: tmpDir,
provider: 'claude',
providerOptions: {
codex: { networkAccess: true },
},
});
await engine.run();
const options = vi.mocked(runAgent).mock.calls[0]?.[2];
expect(options?.providerOptions).toEqual({
codex: { networkAccess: true },
});
});
});