190 lines
7.5 KiB
JavaScript
190 lines
7.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
const read = (relativePath) => readFile(join(appRoot, relativePath), 'utf8');
|
|
|
|
const shortcuts = await read('src/pages/settings/ShortcutsSection.tsx');
|
|
const agentSettings = await read('src/pages/settings/CodingAgentSection.tsx');
|
|
const settingsTabs = await read('src/pages/settings/tabs.tsx');
|
|
assert(
|
|
!/if \(os === 'win'/.test(agentSettings),
|
|
'Windows must expose Less Computer configuration and its text entry point',
|
|
);
|
|
assert(
|
|
shortcuts.includes("(os === 'mac' || os === 'win') && ("),
|
|
'Windows must expose the Less Computer voice shortcut',
|
|
);
|
|
// Platform visibility is covered by navigation.test.ts; the detail route must
|
|
// still mount the existing settings consumer.
|
|
assert(
|
|
settingsTabs.includes("item.id === 'lessComputer' && <CodingAgentSection />"),
|
|
'the Less Computer subpage must mount its settings consumer',
|
|
);
|
|
|
|
const [
|
|
settings,
|
|
ipc,
|
|
opencode,
|
|
dictation,
|
|
lib,
|
|
onboarding,
|
|
lessComputerIpc,
|
|
qaCommands,
|
|
credentialCommands,
|
|
miscCommands,
|
|
coreAdapters,
|
|
coordinatorHost,
|
|
coreApi,
|
|
] = await Promise.all([
|
|
read('src/pages/settings/CodingAgentSection.tsx'),
|
|
read('src/lib/ipc/coding-agent.ts'),
|
|
read('crates/openless-core/src/coding_agent.rs'),
|
|
read('crates/openless-core/src/api.rs'),
|
|
read('src-tauri/src/lib.rs'),
|
|
read('src/components/Onboarding.tsx'),
|
|
read('src/lib/ipc/less-computer.ts'),
|
|
read('src-tauri/src/commands/qa.rs'),
|
|
read('src-tauri/src/commands/credentials.rs'),
|
|
read('src-tauri/src/commands/misc.rs'),
|
|
read('src-tauri/src/core_adapters.rs'),
|
|
read('src-tauri/src/tauri_coordinator_host.rs'),
|
|
read('crates/openless-core/src/api.rs'),
|
|
]);
|
|
|
|
assert(
|
|
settings.includes('codingAgentListOpencodeModels(exe, true)'),
|
|
'OpenCode settings must automatically refresh the available models after detection',
|
|
);
|
|
assert(
|
|
settings.includes('settings.codingAgent.opencodeModelsRefresh'),
|
|
'OpenCode settings must expose the localized manual refresh action',
|
|
);
|
|
assert(
|
|
/['"]coding_agent_list_opencode_models['"]/.test(ipc),
|
|
'frontend IPC must call the OpenCode model-list command',
|
|
);
|
|
assert(opencode.includes('"--refresh"'), 'OpenCode model discovery must refresh its model cache');
|
|
assert(
|
|
opencode.includes('"--auto"'),
|
|
'OpenCode runs must use the current automatic permission flag',
|
|
);
|
|
assert(opencode.includes('"--continue"'), 'OpenCode runs must preserve follow-up session context');
|
|
assert(
|
|
!opencode.includes('--dangerously-skip-permissions'),
|
|
'OpenCode adapter must not use the removed Claude-style permission flag',
|
|
);
|
|
assert(
|
|
coreApi.includes('resolve_coding_agent_model(provider') &&
|
|
dictation.includes('submit_less_computer_with_session'),
|
|
'Less Computer must resolve model defaults per provider',
|
|
);
|
|
assert(
|
|
settings.includes(
|
|
"const SANDBOX_PERMISSION_MODES: CodingAgentPermissionMode[] = ['plan', 'acceptEdits']",
|
|
) &&
|
|
settings.includes("provider === 'codex-cli' || provider === 'dsh-cli'") &&
|
|
settings.includes('normalizePermissionMode'),
|
|
'Codex and dsh settings must expose only read-only/plan and workspace-write permission modes and normalize legacy values',
|
|
);
|
|
const providerChangeStart = settings.indexOf('const nextProvider = v as CodingAgentProviderId');
|
|
const providerChangeEnd = settings.indexOf('options={PROVIDERS}', providerChangeStart);
|
|
const providerChange = settings.slice(providerChangeStart, providerChangeEnd);
|
|
assert(
|
|
providerChange.includes('codingAgentModel: null') &&
|
|
providerChange.includes('codingAgentExe: null') &&
|
|
providerChange.includes('normalizePermissionMode'),
|
|
'switching coding-agent provider must clear provider-specific model/executable state and normalize permissions',
|
|
);
|
|
|
|
const microphoneMenuStart = lib.indexOf('fn build_microphone_tray_menu');
|
|
const microphoneMenuEnd = lib.indexOf('pub(crate) fn refresh_tray_microphone_menu');
|
|
const microphoneMenu = lib.slice(microphoneMenuStart, microphoneMenuEnd);
|
|
assert(
|
|
microphoneMenu.includes('TrayMicrophoneDeviceCache'),
|
|
'tray menu construction must read the asynchronous microphone-device cache',
|
|
);
|
|
assert(
|
|
!microphoneMenu.includes('recorder::list_input_devices'),
|
|
'tray menu construction must never enumerate CoreAudio devices on the AppKit main thread',
|
|
);
|
|
assert(
|
|
lib.includes('.name("openless-tray-mic-event".into())'),
|
|
'native microphone notifications must dispatch enumeration to a background thread',
|
|
);
|
|
assert(
|
|
credentialCommands.includes('pub async fn get_credentials(') &&
|
|
credentialCommands.includes('core.get_credentials_status()') &&
|
|
credentialCommands.includes('tauri::async_runtime::spawn_blocking'),
|
|
'Keychain reads must not block the AppKit main thread while settings load',
|
|
);
|
|
assert(
|
|
miscCommands.includes('pub async fn list_microphone_devices(') &&
|
|
miscCommands.includes('.platform') &&
|
|
miscCommands.includes('.microphone_devices()') &&
|
|
coreAdapters.includes(
|
|
'tauri::async_runtime::spawn_blocking(crate::recorder::list_input_devices',
|
|
),
|
|
'settings microphone enumeration must not block the AppKit main thread',
|
|
);
|
|
assert(
|
|
onboarding.includes("t('onboarding.continueToSettings')"),
|
|
'users must be able to configure OpenCode without granting unrelated system permissions first',
|
|
);
|
|
assert(
|
|
settings.includes('lessComputerWindowOpen()') &&
|
|
/['"]less_computer_window_open['"]/.test(lessComputerIpc) &&
|
|
qaCommands.includes('window.label() != "main"'),
|
|
'Advanced settings must expose a main-window-only text entry point for Less Computer',
|
|
);
|
|
const showLessComputerStart = lib.indexOf('pub(crate) fn show_less_computer_window');
|
|
const showLessComputerEnd = lib.indexOf('pub(crate) fn hide_less_computer_window');
|
|
const showLessComputer = lib.slice(showLessComputerStart, showLessComputerEnd);
|
|
assert(
|
|
showLessComputer.includes('window_clone.show()'),
|
|
'the first lazy Less Computer window must clear Tauri visible=false before ordering its NSPanel',
|
|
);
|
|
assert(
|
|
showLessComputer.indexOf('run_on_main_thread') <
|
|
showLessComputer.indexOf('position_less_computer_window(&window_clone)'),
|
|
'Less Computer NSPanel positioning must run on the AppKit main thread',
|
|
);
|
|
const submitTextStart = qaCommands.indexOf('pub fn less_computer_submit_text');
|
|
const submitTextEnd = qaCommands.indexOf('pub fn less_computer_window_open', submitTextStart);
|
|
const submitText = qaCommands.slice(submitTextStart, submitTextEnd);
|
|
assert(
|
|
submitText.includes('host.spawn') &&
|
|
submitText.includes('backend.submit_less_computer(text)') &&
|
|
!submitText.includes('tauri::async_runtime::spawn') &&
|
|
!submitText.includes('tokio::spawn') &&
|
|
coordinatorHost.includes('tauri::async_runtime::spawn(future)'),
|
|
'Less Computer text submit must spawn through the explicit Tauri host from the WebKit IPC thread',
|
|
);
|
|
|
|
const localeFiles = ['zh-CN.ts', 'zh-TW.ts', 'en.ts', 'ja.ts', 'ko.ts'];
|
|
const localizedKeys = [
|
|
'opencodeModelDefault',
|
|
'opencodeModelHint',
|
|
'opencodeModelsRefresh',
|
|
'opencodeModelsRefreshing',
|
|
'opencodeModelsLoaded',
|
|
'opencodeModelsEmpty',
|
|
'opencodeModelsError',
|
|
'codexBudgetHint',
|
|
'codexMode',
|
|
'openPanel',
|
|
'openPanelHint',
|
|
'openPanelAction',
|
|
];
|
|
for (const localeFile of localeFiles) {
|
|
const locale = await read(`src/i18n/${localeFile}`);
|
|
assert(locale.includes('continueToSettings:'), `${localeFile} is missing continueToSettings`);
|
|
for (const key of localizedKeys) {
|
|
assert(locale.includes(`${key}:`), `${localeFile} is missing ${key}`);
|
|
}
|
|
}
|
|
|
|
console.log('less-computer-opencode-contract.test.mjs passed');
|