Skip to content
19 changes: 19 additions & 0 deletions tests/unit/agents-modal-guards.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const { createAgentsMethods } = await import(
const { createCodexConfigMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.methods.codex-config.mjs'))
);
const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

test('closeConfigTemplateModal ignores user close attempts while template apply is busy', () => {
const methods = createCodexConfigMethods({
Expand All @@ -20,7 +23,9 @@ test('closeConfigTemplateModal ignores user close attempts while template apply
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
showConfigTemplateModal: true,
configTemplateApplying: true,
configTemplateContent: 'draft-template'
Expand Down Expand Up @@ -52,7 +57,9 @@ test('applyConfigTemplate force closes the modal after a successful apply', asyn
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
showConfigTemplateModal: true,
configTemplateApplying: false,
configTemplateContent: 'draft-template',
Expand Down Expand Up @@ -103,7 +110,9 @@ test('applyConfigTemplate keeps the successful apply result when only the refres
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
showConfigTemplateModal: true,
configTemplateApplying: false,
configTemplateContent: 'draft-template',
Expand Down Expand Up @@ -161,7 +170,9 @@ test('applyConfigTemplate applies immediately when diff confirm is disabled', as
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
showConfigTemplateModal: true,
configTemplateApplying: false,
configTemplateContent: 'draft-template',
Expand Down Expand Up @@ -192,7 +203,9 @@ test('runHealthCheck treats backend error payloads as failures', async () => {
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
providersList: ['alpha'],
speedResults: {},
speedLoading: {},
Expand Down Expand Up @@ -227,7 +240,9 @@ test('runHealthCheck skips Claude speed tests when the primary health check alre
});
let claudeSpeedTestCalls = 0;
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
providersList: ['alpha'],
speedResults: {},
speedLoading: {},
Expand Down Expand Up @@ -276,7 +291,9 @@ test('runHealthCheck preserves backend remote health result while appending spee
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
providersList: ['alpha', 'beta'],
speedResults: {},
speedLoading: {},
Expand Down Expand Up @@ -324,7 +341,9 @@ test('applyCodexConfigDirect keeps the successful apply result when only the ref
}
});
const context = {
...createI18nMethods(),
...methods,
lang: 'zh',
codexApplying: false,
_pendingCodexApplyOptions: null,
currentProvider: 'alpha',
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/claude-settings-sync.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import { readBundledWebUiScript, readProjectFile } from './helpers/web-ui-source.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

const appSource = readBundledWebUiScript();
const claudeConfigModuleSource = readProjectFile('web-ui/modules/app.methods.claude-config.mjs');

Expand Down Expand Up @@ -224,7 +233,10 @@ test('addClaudeConfig requires a visible model value before saving', () => {
const addClaudeConfig = instantiateFunction(source, 'addClaudeConfig');
const messages = [];
let saveCount = 0;
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
newClaudeConfig: {
name: 'Claude Test',
apiKey: 'sk-test',
Expand Down Expand Up @@ -256,7 +268,10 @@ ${extractMethodAsFunction(appSource, 'claudeConfigFieldError')}`;
${extractMethodAsFunction(appSource, 'canSubmitClaudeConfig')}`;
const claudeConfigFieldError = instantiateFunction(fieldErrorSource, 'claudeConfigFieldError');
const canSubmitClaudeConfig = instantiateFunction(canSubmitSource, 'canSubmitClaudeConfig');
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
newClaudeConfig: {
name: 'Existing',
apiKey: '',
Expand Down Expand Up @@ -297,7 +312,10 @@ ${extractMethodAsFunction(appSource, 'claudeConfigFieldError')}`;
${extractMethodAsFunction(appSource, 'canSubmitClaudeConfig')}`;
const claudeConfigFieldError = instantiateFunction(fieldErrorSource, 'claudeConfigFieldError');
const canSubmitClaudeConfig = instantiateFunction(canSubmitSource, 'canSubmitClaudeConfig');
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
newClaudeConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
editingConfig: {
name: 'Imported Auth Token',
Expand Down Expand Up @@ -350,7 +368,10 @@ test('addClaudeConfig trims and persists the entered model', () => {
let saveCount = 0;
let closed = false;
let refreshed = false;
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
newClaudeConfig: {
name: 'Claude Test',
apiKey: 'sk-test',
Expand Down Expand Up @@ -548,7 +569,10 @@ test('saveAndApplyConfig writes the edited Claude model through apply api', asyn
let saveCount = 0;
let closed = false;
let refreshCount = 0;
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
editingConfig: {
name: 'UI Claude Use',
apiKey: 'sk-test',
Expand Down Expand Up @@ -607,7 +631,10 @@ test('saveAndApplyConfig saves external credential config without api key', asyn
let saveCount = 0;
let closed = false;
let refreshCount = 0;
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
editingConfig: {
name: 'Imported Auth Token',
apiKey: '',
Expand Down Expand Up @@ -654,7 +681,10 @@ test('applyClaudeConfig reports informative message for external credential only

const messages = [];
let refreshCount = 0;
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
claudeConfigs: {
imported: {
apiKey: '',
Expand Down Expand Up @@ -689,7 +719,10 @@ test('onClaudeModelChange applies external credential config without api key', (
let updateCount = 0;
const applyCalls = [];
const messages = [];
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
currentClaudeConfig: 'imported',
currentClaudeModel: ' claude-opus-4-6 ',
claudeConfigs: {
Expand Down Expand Up @@ -735,7 +768,10 @@ test('onClaudeModelChange still requires api key when no external credential is
let updateCount = 0;
const applyCalls = [];
const messages = [];
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
currentClaudeConfig: 'local',
currentClaudeModel: ' claude-opus-4-6 ',
claudeConfigs: {
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/openclaw-persist-regression.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const __dirname = path.dirname(__filename);
const { createOpenclawPersistMethods, DEFAULT_OPENCLAW_CONFIG_NAME } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.methods.openclaw-persist.mjs'))
);
const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

function deferred() {
let resolve;
Expand All @@ -21,7 +24,9 @@ function deferred() {

function createContext(methods, overrides = {}) {
return {
...createI18nMethods(),
...methods,
lang: 'zh',
openclawConfigs: {
saved: {
content: 'saved-local'
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/provider-switch-regression.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import {
captureCurrentBundledAppOptions,
withGlobalOverrides
} from './helpers/web-ui-app-options.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

const currentAppOptions = await captureCurrentBundledAppOptions();
const currentMethods = currentAppOptions.methods;
const currentComputed = currentAppOptions.computed;
Expand All @@ -13,6 +22,8 @@ function createProviderSwitchContext() {
const messages = [];

return {
...createI18nMethods(),
lang: 'zh',
currentProvider: 'alpha',
currentModel: 'alpha-model',
models: ['alpha-model'],
Expand Down Expand Up @@ -58,6 +69,8 @@ function createProviderSwitchContext() {
function createProviderUpdateContext() {
const messages = [];
return {
...createI18nMethods(),
lang: 'zh',
editingProvider: {
name: 'alpha',
url: ' https://api.example.com/v1 ',
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/providers-validation.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import { createProvidersMethods } from '../../web-ui/modules/app.methods.providers.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

function createContext(overrides = {}, apiImpl = async () => ({ success: true })) {
const messages = [];
const loadAllCalls = [];
const methods = createProvidersMethods({ api: apiImpl });
const context = {
...createI18nMethods(),
lang: 'zh',
providersList: [],
codexAuthProfiles: [],
showAddModal: true,
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/session-trash-state.test.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import {
readBundledWebUiCss,
readBundledWebUiHtml,
readBundledWebUiScript,
readProjectFile
} from './helpers/web-ui-source.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

const appSource = readBundledWebUiScript();
const cliSource = readProjectFile('cli.js');
const indexHtmlSource = readBundledWebUiHtml();
Expand Down Expand Up @@ -1904,7 +1912,10 @@ test('cloneSession keeps success message when refresh fails after clone succeeds
});

const messages = [];
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
sessionCloning: {},
sessionsList: [],
isCloneAvailable: () => true,
Expand Down Expand Up @@ -1941,7 +1952,10 @@ test('cloneSession keeps success message when selecting the cloned session fails
});

const messages = [];
const i18nMethods = createI18nMethods();
const context = {
...i18nMethods,
lang: 'zh',
sessionCloning: {},
sessionsList: [],
isCloneAvailable: () => true,
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import {
captureBehaviorParityBaselineAppOptions,
captureCurrentBundledAppOptions,
withGlobalOverrides
} from './helpers/web-ui-app-options.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createI18nMethods } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'i18n.mjs'))
);

const currentAppOptions = await captureCurrentBundledAppOptions();
const parityBaseline = await captureBehaviorParityBaselineAppOptions();
const headAppOptions = parityBaseline.options;
Expand Down Expand Up @@ -289,6 +298,8 @@ function createDownloadEnvironment() {
function createCopyActionContext(methods) {
const messages = createMessagesRecorder();
return {
...createI18nMethods(),
lang: 'zh',
...messages,
sessionResumeWithYolo: true,
shareCommandPrefix: 'npm start',
Expand Down
16 changes: 15 additions & 1 deletion web-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,21 @@ document.addEventListener('DOMContentLoaded', () => {
codexDownloadProgress: 0,
codexDownloadTimer: null,
settingsTab: 'general',
toolConfigPermissions: { codex: false, claude: false },
toolConfigPermissions: (function() {
try {
const cached = localStorage.getItem('toolConfigPermissions');
if (cached) {
const parsed = JSON.parse(cached);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return {
codex: parsed.codex === true,
claude: parsed.claude === true
};
}
}
} catch (_) {}
return { codex: false, claude: false };
})(),
toolConfigPermissionSaving: { codex: false, claude: false },
sessionTrashEnabled: true,
sessionTrashItems: [],
Expand Down
Loading
Loading