* takt: move-allowed-tools-claude * fix: E2Eフィクスチャの allowed_tools を provider_options.claude に移行 PR #469 で allowed_tools がムーブメント直下から provider_options.claude.allowed_tools に 移動されたが、E2Eフィクスチャとインラインピース定義が旧形式のままだった。
343 lines
9.4 KiB
TypeScript
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' }),
|
|
);
|
|
});
|
|
});
|