Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/cli/src/nonInteractive/control/ControlDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* which wraps these controllers with a stable programmatic API.
*
* Controllers:
* - SystemController: initialize, interrupt, set_model, supported_commands, get_context_usage
* - SystemController: initialize, interrupt, set_model, set_effort, supported_commands, get_context_usage, get_available_models, get_usage_info
* - PermissionController: can_use_tool, set_permission_mode
* - SdkMcpController: mcp_server_status (mcp_message handled via callback)
*
Expand Down Expand Up @@ -375,8 +375,11 @@ export class ControlDispatcher implements IPendingRequestRegistry {
case 'interrupt':
case 'continue_last_turn':
case 'set_model':
case 'set_effort':
case 'supported_commands':
case 'get_context_usage':
case 'get_available_models':
case 'get_usage_info':
return this.systemController;

case 'can_use_tool':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ function createContext(
setSessionSubagents: vi.fn(),
setApprovalMode: vi.fn(),
setModel: vi.fn(),
setReasoningEffort: vi.fn(),
getReasoningEffort: vi.fn().mockReturnValue(undefined),
getAvailableModels: vi.fn().mockReturnValue([]),
} as unknown as IControlContext['config'],
streamJson: {
send: vi.fn(),
Expand Down Expand Up @@ -248,4 +251,235 @@ describe('SystemController', () => {
).rejects.toThrow(/was not registered on ControlContext/);
});
});

describe('set_effort', () => {
it('sets effort and returns applied=true when read-back matches', async () => {
const context = createContext();
(
context.config.getReasoningEffort as ReturnType<typeof vi.fn>
).mockReturnValue('high');
const controller = new SystemController(
context,
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'set_effort', effort: 'high' },
'effort-1',
);

expect(context.config.setReasoningEffort).toHaveBeenCalledWith('high');
expect(result).toEqual({
subtype: 'set_effort',
effort: 'high',
applied: true,
});
});

it('returns applied=false when thinking is disabled (read-back mismatch)', async () => {
const context = createContext();
(
context.config.getReasoningEffort as ReturnType<typeof vi.fn>
).mockReturnValue('medium');
const controller = new SystemController(
context,
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'set_effort', effort: 'high' },
'effort-2',
);

expect(result).toEqual({
subtype: 'set_effort',
effort: 'high',
applied: false,
});
});

it('rejects invalid effort value', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

await expect(
controller.handleRequest(
{ subtype: 'set_effort', effort: 'banana' },
'effort-3',
),
).rejects.toThrow('Invalid effort value');
});

it('rejects empty effort string', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

await expect(
controller.handleRequest(
{ subtype: 'set_effort', effort: ' ' },
'effort-4',
),
).rejects.toThrow('Invalid effort specified');
});
});

describe('get_available_models', () => {
it('returns models without exposing baseUrl or envKey', async () => {
const context = createContext();
(
context.config.getAvailableModels as ReturnType<typeof vi.fn>
).mockReturnValue([
{
id: 'qwen-max',
label: 'Qwen Max',
capabilities: { vision: true },
contextWindowSize: 128000,
baseUrl: 'https://internal-proxy.corp/v1',
envKey: 'SECRET_API_KEY',
},
]);
const controller = new SystemController(
context,
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'get_available_models' },
'models-1',
);

expect(result).toEqual({
subtype: 'get_available_models',
models: [
{
id: 'qwen-max',
label: 'Qwen Max',
capabilities: { vision: true },
contextWindowSize: 128000,
},
],
});
});

it('returns empty models list when none available', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'get_available_models' },
'models-2',
);

expect(result).toEqual({
subtype: 'get_available_models',
models: [],
});
});
});

describe('get_usage_info', () => {
it('returns dashboard with subtype when range is provided', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'get_usage_info', range: 'week' },
'usage-1',
);

expect(result).toHaveProperty('subtype', 'get_usage_info');
expect(result).toHaveProperty('generatedAt');
expect(result).toHaveProperty('summary');
});

it('returns dashboard without range filter', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'get_usage_info' },
'usage-2',
);

expect(result).toHaveProperty('subtype', 'get_usage_info');
expect(result).toHaveProperty('generatedAt');
});
});

describe('initialize with effort', () => {
it('sets effort during initialize when provided', async () => {
const context = createContext();
(
context.config.getReasoningEffort as ReturnType<typeof vi.fn>
).mockReturnValue('high');
const controller = new SystemController(
context,
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'initialize', effort: 'high' },
'init-effort-1',
);

expect(context.config.setReasoningEffort).toHaveBeenCalledWith('high');
expect(result).toHaveProperty('subtype', 'initialize');
expect(result).toHaveProperty('session_id', 'test-session-id');
});

it('warns when effort not applied during initialize (thinking disabled)', async () => {
const context = createContext();
(
context.config.getReasoningEffort as ReturnType<typeof vi.fn>
).mockReturnValue('medium');
const controller = new SystemController(
context,
createRegistry(),
'SystemController',
);

const result = await controller.handleRequest(
{ subtype: 'initialize', effort: 'high' },
'init-effort-2',
);

expect(context.config.setReasoningEffort).toHaveBeenCalledWith('high');
expect(context.config.getReasoningEffort).toHaveBeenCalled();
expect(result).toHaveProperty('subtype', 'initialize');
});

it('rejects invalid effort during initialize', async () => {
const controller = new SystemController(
createContext(),
createRegistry(),
'SystemController',
);

await expect(
controller.handleRequest(
{ subtype: 'initialize', effort: 'banana' },
'init-effort-3',
),
).rejects.toThrow('Invalid effort value');
});
});
});
Loading
Loading