takt/src/__tests__/option-resolution-order.test.ts
nrs 2f268f6d43
[#320] move-allowed-tools-claude (#469)
* takt: move-allowed-tools-claude

* fix: E2Eフィクスチャの allowed_tools を provider_options.claude に移行

PR #469 で allowed_tools がムーブメント直下から provider_options.claude.allowed_tools に
移動されたが、E2Eフィクスチャとインラインピース定義が旧形式のままだった。
2026-03-05 11:27:48 +09:00

343 lines
9.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const {
getProviderMock,
loadCustomAgentsMock,
loadAgentPromptMock,
loadProjectConfigMock,
loadGlobalConfigMock,
resolveConfigValueMock,
loadTemplateMock,
providerSetupMock,
providerCallMock,
} = vi.hoisted(() => {
const providerCall = vi.fn();
const providerSetup = vi.fn(() => ({ call: providerCall }));
return {
getProviderMock: vi.fn(() => ({ setup: providerSetup })),
loadCustomAgentsMock: vi.fn(),
loadAgentPromptMock: vi.fn(),
loadProjectConfigMock: vi.fn(),
loadGlobalConfigMock: vi.fn(),
resolveConfigValueMock: vi.fn(),
loadTemplateMock: vi.fn(),
providerSetupMock: providerSetup,
providerCallMock: providerCall,
};
});
vi.mock('../infra/providers/index.js', () => ({
getProvider: getProviderMock,
}));
vi.mock('../infra/config/index.js', () => ({
loadProjectConfig: loadProjectConfigMock,
loadGlobalConfig: loadGlobalConfigMock,
resolveConfigValue: resolveConfigValueMock,
loadCustomAgents: loadCustomAgentsMock,
loadAgentPrompt: loadAgentPromptMock,
}));
vi.mock('../shared/prompts/index.js', () => ({
loadTemplate: loadTemplateMock,
}));
import { runAgent } from '../agents/runner.js';
describe('option resolution order', () => {
beforeEach(() => {
vi.clearAllMocks();
providerCallMock.mockResolvedValue({ content: 'ok' });
loadProjectConfigMock.mockReturnValue({});
loadGlobalConfigMock.mockReturnValue({
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
resolveConfigValueMock.mockImplementation((_cwd: string, key: string) => {
if (key === 'personaProviders') {
return loadProjectConfigMock.mock.results.at(-1)?.value?.personaProviders;
}
return undefined;
});
loadCustomAgentsMock.mockReturnValue(new Map());
loadAgentPromptMock.mockReturnValue('prompt');
loadTemplateMock.mockReturnValue('template');
});
it('should resolve provider in order: CLI > stepProvider > local config > global config', async () => {
loadProjectConfigMock.mockReturnValue({ provider: 'opencode' });
loadGlobalConfigMock.mockReturnValue({
provider: 'mock',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent(undefined, 'task', {
cwd: '/repo',
provider: 'codex',
stepProvider: 'claude',
});
expect(getProviderMock).toHaveBeenLastCalledWith('codex');
await runAgent(undefined, 'task', {
cwd: '/repo',
stepProvider: 'claude',
});
expect(getProviderMock).toHaveBeenLastCalledWith('claude');
loadProjectConfigMock.mockReturnValue({});
loadGlobalConfigMock.mockReturnValue({
provider: 'mock',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent(undefined, 'task', {
cwd: '/repo',
stepProvider: 'claude',
});
expect(getProviderMock).toHaveBeenLastCalledWith('claude');
await runAgent(undefined, 'task', { cwd: '/repo' });
expect(getProviderMock).toHaveBeenLastCalledWith('mock');
});
it('should apply persona provider override before local/global config', async () => {
loadProjectConfigMock.mockReturnValue({
provider: 'opencode',
personaProviders: {
coder: { provider: 'claude' },
},
});
loadGlobalConfigMock.mockReturnValue({
provider: 'mock',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent('coder', 'task', {
cwd: '/repo',
});
expect(getProviderMock).toHaveBeenLastCalledWith('claude');
});
it('should resolve model in order: CLI > persona > step > local > global', async () => {
loadGlobalConfigMock.mockReturnValue({
provider: 'claude',
model: 'global-model',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
loadProjectConfigMock.mockReturnValue({
provider: 'claude',
model: 'local-model',
personaProviders: {
coder: { model: 'persona-model' },
},
});
await runAgent('coder', 'task', {
cwd: '/repo',
model: 'cli-model',
stepModel: 'step-model',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: 'cli-model' }),
);
await runAgent(undefined, 'task', {
cwd: '/repo',
stepModel: 'step-model',
stepProvider: 'claude',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: 'step-model' }),
);
await runAgent('coder', 'task', {
cwd: '/repo',
stepProvider: 'claude',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: 'persona-model' }),
);
loadGlobalConfigMock.mockReturnValue({
provider: 'codex',
model: 'global-model',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
loadProjectConfigMock.mockReturnValue({
provider: 'codex',
});
await runAgent(undefined, 'task', {
cwd: '/repo',
stepProvider: 'codex',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: 'global-model' }),
);
});
it('should ignore local/global model if resolved provider is not matching', async () => {
loadProjectConfigMock.mockReturnValue({
provider: 'claude',
model: 'local-model',
});
loadGlobalConfigMock.mockReturnValue({
provider: 'mock',
model: 'global-model',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent(undefined, 'task', {
cwd: '/repo',
stepProvider: 'opencode',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: undefined }),
);
});
it('should use providerOptions from piece/step only', async () => {
const stepProviderOptions = {
claude: {
sandbox: {
allowUnsandboxedCommands: false,
},
},
};
await runAgent(undefined, 'task', {
cwd: '/repo',
provider: 'claude',
providerOptions: stepProviderOptions,
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ providerOptions: stepProviderOptions }),
);
});
it('should ignore custom agent provider/model overrides', async () => {
loadProjectConfigMock.mockReturnValue({ provider: 'claude', model: 'project-model' });
loadGlobalConfigMock.mockReturnValue({
provider: 'mock',
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
loadCustomAgentsMock.mockReturnValue(new Map([
['custom', { name: 'custom', prompt: 'agent prompt' }],
]));
await runAgent('custom', 'task', { cwd: '/repo' });
expect(getProviderMock).toHaveBeenLastCalledWith('claude');
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ model: 'project-model' }),
);
});
it('should use custom agent allowedTools when run options do not provide allowedTools', async () => {
loadProjectConfigMock.mockReturnValue({ provider: 'claude' });
loadCustomAgentsMock.mockReturnValue(new Map([
['custom', { name: 'custom', prompt: 'agent prompt', allowedTools: ['Read', 'Grep'] }],
]));
await runAgent('custom', 'task', { cwd: '/repo' });
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ allowedTools: ['Read', 'Grep'] }),
);
});
it('should prioritize run options allowedTools over custom agent allowedTools', async () => {
loadProjectConfigMock.mockReturnValue({ provider: 'claude' });
loadCustomAgentsMock.mockReturnValue(new Map([
['custom', { name: 'custom', prompt: 'agent prompt', allowedTools: ['Read', 'Grep'] }],
]));
await runAgent('custom', 'task', { cwd: '/repo', allowedTools: ['Write'] });
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ allowedTools: ['Write'] }),
);
});
it('should resolve permission mode after provider resolution using provider profiles', async () => {
loadProjectConfigMock.mockReturnValue({});
loadGlobalConfigMock.mockReturnValue({
provider: 'codex',
providerProfiles: {
codex: { defaultPermissionMode: 'full' },
},
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent(undefined, 'task', {
cwd: '/repo',
permissionResolution: {
movementName: 'supervise',
},
});
expect(getProviderMock).toHaveBeenLastCalledWith('codex');
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ permissionMode: 'full' }),
);
});
it('should preserve explicit permission mode when permissionResolution is not set', async () => {
loadProjectConfigMock.mockReturnValue({});
loadGlobalConfigMock.mockReturnValue({
provider: 'codex',
providerProfiles: {
codex: { defaultPermissionMode: 'full' },
},
language: 'en',
concurrency: 1,
taskPollIntervalMs: 500,
});
await runAgent(undefined, 'task', {
cwd: '/repo',
permissionMode: 'readonly',
});
expect(providerCallMock).toHaveBeenLastCalledWith(
'task',
expect.objectContaining({ permissionMode: 'readonly' }),
);
});
});