Files
openless/openless-all/app/scripts/less-computer-opencode-contract.test.mjs
T
Chris233 fc38edd1a3 refactor: consolidate Core 2.0 and restore cross-platform behavior (#1019)
* refactor: split Linux egui host from shared backend

* fix: keep CI contracts portable across runners

* fix(ci): preserve CRLF while removing qwen dependency

* fix(ci): regenerate target lockfile and install ripgrep

* fix(ci): gate desktop-only selection voice module

* ci: limit native build parallelism on cross-platform runners

* fix(host): expose blocking task seam to coordinator

* fix(settings): avoid macOS default hotkey collision

* docs: record cross-platform CI evidence

* ci: expose Linux validation package artifact

* ci: grant reusable Linux package permissions

* refactor: remove legacy coordinator dictation facade

* ci: skip unsupported MSI prerelease builds

* ci: install minisign only for signed Linux releases

* fix: keep test-only coordinator session helper

* refactor: move style preview diagnostics into core

* fix: import shared preferences type from core

* refactor: centralize enabled vocabulary projection

* ci: install ripgrep for Linux package gates

* test: make source contract line-ending agnostic

* fix: keep Linux desktop entry standards compliant

* docs: expose core style diagnostics contract

* ci: resolve Linux package version in app directory

* refactor: centralize ASR vocabulary priority

* fix: use shared vocabulary note constant

* refactor: keep vocabulary priority inside core

* test: retain starting-stop queue coverage

* ci: scope Linux updater URL to the current repository

* docs: close implementation and artifact evidence

* docs: record final CI artifact evidence

* fix(android): align updater plugin versions

* fix(android): pin updater crate exactly

* refactor(core): remove unused type anchor

* docs: record final artifact runs

* fix(remote-input): preserve beta status fields

* fix(tauri): finish beta host merge

* fix(asr): pass scoped operation to native qwen adapter

* fix(macos): bundle mlx metallib as a resource

* test(remote-input): update status wire contract

* feat: share Less Computer voice flow with Linux

* refactor(core): centralize host-independent runtime policy

* refactor(core): complete 2.0 backend consolidation

* fix(ci): prepare Sherpa after cache restore

* fix(macos): route native model paths through Core store

* fix(macos): use ModelStore catalog API

* fix(ci): serialize macOS native test build

* test(ci): preserve macOS cargo diagnostics

* fix(macos): release input source lock before await

* test(core): replace legacy hotkey injection gate

* test(core): move Codex sandbox contract into Core

* fix(core): close final runtime policy gaps

* fix(windows): terminate coding agent process trees

* fix(core): complete local model migration

* fix(credentials): align Core LLM model account

* fix(core): restore 1.x parity across shared backend

* fix(android): complete shared backend stubs

* fix: close cross-platform migration review gaps

* fix: complete migration lifecycles and platform compatibility

* docs: define desktop 2.0 acceptance and Linux handoff

* fix: close desktop parity and session lifecycle gaps

* fix: finish voice lifecycle and desktop feedback parity

* fix: close desktop cancellation and feedback race gaps

* fix: honor proxy settings and reject stale session effects

* fix: dispatch native cleanup on host runtimes and order events

* fix: keep optional recording effects best effort

* fix: close desktop agent model and QA lifecycle gaps

* fix: preserve desktop raw voice and QA turn ownership

* fix: finalize desktop dictation and QA feedback
2026-09-06 18:39:46 +08:00

158 lines
7.3 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');
assert(settingsTabs.includes("(os === 'mac' || os === 'win') && <CodingAgentSection />"), 'the settings tab must mount Less Computer on Windows');
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(
ipc.includes('"coding_agent_list_opencode_models"'),
'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()') &&
lessComputerIpc.includes('"less_computer_window_open"') &&
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');