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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ function readSource(relativePath: string): string {
}

describe('Runtime settings information architecture', () => {
it('keeps concurrency input limits aligned with the host option schema', () => {
const source = readSource('./RuntimeSettingsPages.tsx');
const registry = readSource('../../../../../../src/crates/contracts/product-domains/src/product_control_owner_registry.rs');
for (const [option, constant] of [
['subagent-max-concurrency', 'SUBAGENT_MAX_CONCURRENCY_LIMIT'],
['swarm-max-concurrency', 'SWARM_MAX_CONCURRENCY_LIMIT'],
]) {
const maximum = registry.match(new RegExp(`"${option}",\\s*integer_range\\(1\\.0, (\\d+)\\.0\\)`))?.[1];
expect(maximum).toBeDefined();
expect(source).toContain(`const ${constant} = ${maximum};`);
expect(source).toContain(`max={${constant}}`);
}
});

it('keeps execution and permissions unified and stacks browser and desktop control in one owner', () => {
const source = readSource('./RuntimeSettingsPages.tsx');
const appearance = readSource('./RuntimeSettingsPages.appearance.ts');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { BrowserDesktopControlSettingsPage } from './RuntimeSettingsPages';
import { BrowserDesktopControlSettingsPage, ExecutionSettingsPage } from './RuntimeSettingsPages';

const mocks = vi.hoisted(() => {
Object.defineProperty(window, '__TAURI__', { configurable: true, value: {} });
return {
invoke: vi.fn(), setConfig: vi.fn(), setEnabled: vi.fn(),
invoke: vi.fn(), setConfig: vi.fn(), setEnabled: vi.fn(), getConfig: vi.fn(), error: vi.fn(),
t: (key: string) => key,
};
});
Expand All @@ -16,15 +16,18 @@ vi.mock('@/infrastructure/i18n', () => ({ i18nService: { formatNumber: String }
vi.mock('@/infrastructure/api/service-api/ApiClient', () => ({ api: { invoke: mocks.invoke } }));
vi.mock('@/infrastructure/api/service-api/SystemAPI', () => ({ systemAPI: { getSystemInfo: async () => ({ platform: 'macos' }) } }));
vi.mock('../services/ConfigManager', () => ({ configManager: {
getConfig: async () => false, setConfig: mocks.setConfig,
getConfig: mocks.getConfig, getOptionalConfig: async () => true, setConfig: mocks.setConfig,
} }));
vi.mock('../hooks/useComputerUseEnabled', () => ({ useComputerUseEnabled: () => ({ computerUseEnabled: false, setComputerUseEnabled: mocks.setEnabled }) }));
vi.mock('../services/AIExperienceConfigService', () => ({ aiExperienceConfigService: {} }));
vi.mock('../services/AgentCompanionPetService', () => ({ DEFAULT_AGENT_COMPANION_PET: 'default' }));
vi.mock('../services/PermissionConfigService', () => ({ DEFAULT_TOOL_PERMISSION_CONFIG: {}, permissionConfigService: {} }));
vi.mock('../services/PermissionConfigService', async (original) => ({
...await original<typeof import('../services/PermissionConfigService')>(),
permissionConfigService: { getConfig: async () => ({}) },
}));
vi.mock('@/infrastructure/peer-device/peerDeviceContextState', () => ({ usePeerDeviceModeOptional: () => null }));
vi.mock('@/infrastructure/confirm-dialog', () => ({ confirmDanger: vi.fn() }));
vi.mock('@/shared/notification-system', () => ({ useNotification: () => ({}), notificationService: { dismiss: vi.fn(), success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() } }));
vi.mock('@/shared/notification-system', () => ({ useNotification: () => ({}), notificationService: { dismiss: vi.fn(), success: vi.fn(), error: mocks.error, info: vi.fn(), warning: vi.fn() } }));
vi.mock('./GlobalPermissionRulesDialog', () => ({ GlobalPermissionRulesDialog: () => null }));
vi.mock('./SessionTitleConfig', () => ({ default: () => null }));
vi.mock('./ReviewCapacitySection', () => ({ default: () => null }));
Expand All @@ -46,6 +49,9 @@ const render = async () => { await act(async () => root.render(<BrowserDesktopCo
beforeEach(() => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
status = { ...readyStatus };
mocks.getConfig.mockReset().mockResolvedValue(null);
mocks.setConfig.mockReset().mockResolvedValue(undefined);
mocks.error.mockReset();
mocks.invoke.mockReset().mockImplementation(async (command: string) => {
if (command === 'computer_use_get_status') return { computerUseEnabled: false, accessibilityGranted: true, screenCaptureGranted: false, platformNote: null };
if (command === 'browser_control_get_status') return { ...status };
Expand All @@ -56,6 +62,54 @@ beforeEach(() => {
document.body.append(container);
root = createRoot(container);
});

describe('Tool execution concurrency settings', () => {
const concurrencyInput = (label: string) => Array.from(container.querySelectorAll('[data-openbitfun-part="row"]'))
.find(row => row.textContent?.includes(label))!.querySelector('input')!;

async function editAndBlur(input: HTMLInputElement, value: string) {
await act(async () => input.focus());
await act(async () => {
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
});
await act(async () => input.blur());
}

it.each([
['config.subagentMaxConcurrency', 'ai.subagent_max_concurrency', '50', 32],
['config.swarmMaxConcurrency', 'ai.swarm_max_concurrency', '100', 64],
['config.subagentMaxConcurrency', 'ai.subagent_max_concurrency', '20', 20],
['config.subagentMaxConcurrency', 'ai.subagent_max_concurrency', '5.5', 6],
['config.swarmMaxConcurrency', 'ai.swarm_max_concurrency', '0', 1],
])('saves a supported integer for %s when entering %s / %s', async (label, path, draft, expected) => {
await act(async () => root.render(<ExecutionSettingsPage />));
const input = concurrencyInput(label);
await editAndBlur(input, draft);
expect(mocks.setConfig).toHaveBeenCalledWith(path, expected);
expect(input.value).toBe(String(expected));
expect(mocks.error).not.toHaveBeenCalled();
});

it('preserves an existing value outside the current save range until it is edited', async () => {
mocks.getConfig.mockImplementation(async (path: string) => path === 'ai.subagent_max_concurrency' ? 50 : null);
await act(async () => root.render(<ExecutionSettingsPage />));
expect(concurrencyInput('config.subagentMaxConcurrency').value).toBe('50');
expect(mocks.setConfig).not.toHaveBeenCalled();
});

it.each([
['config.subagentMaxConcurrency', 5],
['config.swarmMaxConcurrency', 16],
])('restores the saved value and exposes the host error for %s', async (label, previous) => {
mocks.setConfig.mockRejectedValue(new Error('Host rejected the concurrency setting'));
await act(async () => root.render(<ExecutionSettingsPage />));
const input = concurrencyInput(label);
await editAndBlur(input, '20');
expect(input.value).toBe(String(previous));
expect(mocks.error).toHaveBeenCalledWith('messages.saveFailed: Host rejected the concurrency setting');
});
});
afterEach(async () => { await act(async () => root.unmount()); container.remove(); });

describe('Browser and desktop control settings', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ type ToolPermissionMode = 'ask' | 'auto' | 'full_access';
const DEFAULT_SUBAGENT_BATCH_EXECUTION_POLICY: SubagentBatchExecutionPolicy = 'force_parallel';
const DEFAULT_SUBAGENT_MAX_CONCURRENCY = 5;
const DEFAULT_SWARM_MAX_CONCURRENCY = 16;
// Match the setting.tools.execution integer ranges in the product control registry.
const SUBAGENT_MAX_CONCURRENCY_LIMIT = 32;
const SWARM_MAX_CONCURRENCY_LIMIT = 64;
const SHOW_PERMISSION_MODE_CONTROL_CONFIG_PATH = 'app.flow_chat.show_permission_mode_control';

function normalizeSubagentBatchExecutionPolicy(value: unknown): SubagentBatchExecutionPolicy {
Expand Down Expand Up @@ -623,8 +626,9 @@ const RuntimeSettingsPage: React.FC<RuntimeSettingsPageProps> = ({
}
};

const handleSwarmMaxConcurrencyChange = async (value: number) => {
if (Number.isNaN(value) || value < 1) return;
const handleSwarmMaxConcurrencyChange = async (input: number) => {
if (!Number.isFinite(input)) return;
const value = Math.min(SWARM_MAX_CONCURRENCY_LIMIT, Math.max(1, Math.round(input)));
const previous = swarmMaxConcurrency;
setSwarmMaxConcurrency(value);
setToolExecConfigLoading(true);
Expand All @@ -634,14 +638,17 @@ const RuntimeSettingsPage: React.FC<RuntimeSettingsPageProps> = ({
} catch (error) {
log.error('Failed to save swarm_max_concurrency', error);
setSwarmMaxConcurrency(previous);
notificationService.error(tTools('messages.saveFailed'));
notificationService.error(
`${tTools('messages.saveFailed')}: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
setToolExecConfigLoading(false);
}
};

const handleSubagentMaxConcurrencyChange = async (value: number) => {
if (Number.isNaN(value) || value < 1) return;
const handleSubagentMaxConcurrencyChange = async (input: number) => {
if (!Number.isFinite(input)) return;
const value = Math.min(SUBAGENT_MAX_CONCURRENCY_LIMIT, Math.max(1, Math.round(input)));
const previous = subagentMaxConcurrency;
setSubagentMaxConcurrency(value);
setToolExecConfigLoading(true);
Expand All @@ -651,7 +658,9 @@ const RuntimeSettingsPage: React.FC<RuntimeSettingsPageProps> = ({
} catch (error) {
log.error('Failed to save subagent_max_concurrency', error);
setSubagentMaxConcurrency(previous);
notificationService.error(tTools('messages.saveFailed'));
notificationService.error(
`${tTools('messages.saveFailed')}: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
setToolExecConfigLoading(false);
}
Expand Down Expand Up @@ -1256,7 +1265,7 @@ const RuntimeSettingsPage: React.FC<RuntimeSettingsPageProps> = ({
value={subagentMaxConcurrency}
onValueChange={(val) => void handleSubagentMaxConcurrencyChange(val)}
min={1}
max={100}
max={SUBAGENT_MAX_CONCURRENCY_LIMIT}
step={1}
size="sm"
variant="compact"
Expand All @@ -1274,7 +1283,7 @@ const RuntimeSettingsPage: React.FC<RuntimeSettingsPageProps> = ({
value={swarmMaxConcurrency}
onValueChange={(val) => void handleSwarmMaxConcurrencyChange(val)}
min={1}
max={100}
max={SWARM_MAX_CONCURRENCY_LIMIT}
step={1}
size="sm"
variant="compact"
Expand Down
Loading