Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
html,
/<div class="docs-command-row">[\s\S]*<div class="docs-command-box"[\s\S]*<code class="install-command">\{\{ target\.command \}\}<\/code>[\s\S]*<button[\s\S]*class="btn-mini docs-copy-btn"/
);
assert.match(html, /<button v-if="name !== '默认配置'" class="card-action-btn delete"[^>]*@click="deleteOpenclawConfig\(name\)"[^>]*:aria-label="t\('openclaw\.action\.deleteAria', \{ name \}\)"[^>]*:title="t\('openclaw\.action\.delete'\)">/);
assert.match(html, /<button v-if="!isDefaultOpenclawConfig\(name, config\)" class="card-action-btn delete"[^>]*@click="deleteOpenclawConfig\(name\)"[^>]*:aria-label="t\('openclaw\.action\.deleteAria', \{ name \}\)"[^>]*:title="t\('openclaw\.action\.delete'\)">/);
assert.match(modalsBasic, /<div v-if="showAddModal" class="modal-overlay" @click\.self="closeAddModal">/);
assert.match(modalsBasic, /<div v-if="showModelModal" class="modal-overlay" @click\.self="closeModelModal">/);
assert.match(modalsBasic, /<div v-if="showClaudeConfigModal" class="modal-overlay" @click\.self="closeClaudeConfigModal">/);
Expand Down Expand Up @@ -380,7 +380,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(openclawModal, /<div class="modal-title" id="openclaw-config-modal-title">{{ openclawEditorTitle }}<\/div>/);
assert.match(openclawModal, /:readonly="openclawSaving \|\| openclawApplying"/);
assert.match(openclawModal, /<button class="btn btn-cancel" @click="closeOpenclawConfigModal" :disabled="openclawSaving \|\| openclawApplying">取消<\/button>/);
assert.match(openclawModal, /<button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving \|\| openclawApplying \|\| \(openclawEditing\.lockName && openclawEditing\.name === '默认配置'\)">/);
assert.match(openclawModal, /<button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving \|\| openclawApplying \|\| \(openclawEditing\.lockName && isDefaultOpenclawConfig\(openclawEditing\.name\)\)">/);
assert.match(openclawModal, /<button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawSaving \|\| openclawApplying">/);
assert.doesNotMatch(baseTheme, /fonts\.googleapis\.com/);
assert.match(controlsForms, /\.btn-tool-compact:disabled:hover,\s*\.btn-tool-compact\[disabled\]:hover/);
Expand Down
37 changes: 36 additions & 1 deletion tests/unit/openclaw-persist-regression.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { createOpenclawPersistMethods } = await import(
const { createOpenclawPersistMethods, DEFAULT_OPENCLAW_CONFIG_NAME } = await import(
pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'app.methods.openclaw-persist.mjs'))
);

Expand Down Expand Up @@ -566,3 +566,38 @@ test('persistOpenclawConfig restores the previous config content when saving an
type: 'error'
}]);
});


test('OpenClaw default config predicate uses invariant marker and canonical key', async () => {
const methods = createOpenclawPersistMethods({
api: async () => ({
error: '',
exists: true,
path: '/tmp/openclaw.json',
lineEnding: '\n',
content: 'real-default-content'
})
});
const context = createContext(methods, {
openclawConfigs: {
[DEFAULT_OPENCLAW_CONFIG_NAME]: { content: 'old-default' },
displayDefault: { content: 'renamed-default', isDefault: true },
userConfig: { content: 'user-content' }
}
});

assert.strictEqual(methods.isDefaultOpenclawConfig.call(context, DEFAULT_OPENCLAW_CONFIG_NAME), true);
assert.strictEqual(methods.isDefaultOpenclawConfig.call(context, 'displayDefault', context.openclawConfigs.displayDefault), true);
assert.strictEqual(methods.isDefaultOpenclawConfig.call(context, 'userConfig', context.openclawConfigs.userConfig), false);

await methods.deleteOpenclawConfig.call(context, 'displayDefault');

assert.ok(context.openclawConfigs.displayDefault, 'marked default config should not be deleted');
assert.deepStrictEqual(context.shownMessages, [{
message: '默认配置始终映射当前系统配置,不可删除',
type: 'info'
}]);

await methods.syncDefaultOpenclawConfigEntry.call(context, { silent: true });
assert.strictEqual(context.openclawConfigs[DEFAULT_OPENCLAW_CONFIG_NAME].isDefault, true);
});
3 changes: 2 additions & 1 deletion tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'pasteAgentsContent',
'importSingleSkill',
'openSkillsMenu',
'isClaudeLocalBridgeDisabled'
'isClaudeLocalBridgeDisabled',
'isDefaultOpenclawConfig'
);
const allowedMissingCurrentMethodKeys = [
'convertSession',
Expand Down
6 changes: 4 additions & 2 deletions web-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ document.addEventListener('DOMContentLoaded', () => {
currentOpenclawConfig: '',
openclawConfigs: {
'默认配置': {
content: DEFAULT_OPENCLAW_TEMPLATE
content: DEFAULT_OPENCLAW_TEMPLATE,
isDefault: true
}
},
openclawEditing: { name: '', content: '', lockName: false },
Expand Down Expand Up @@ -567,7 +568,8 @@ document.addEventListener('DOMContentLoaded', () => {
: { content: DEFAULT_OPENCLAW_TEMPLATE };
const normalized = {
'默认配置': {
content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE
content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE,
isDefault: true
}
};
for (const [name, value] of Object.entries(source)) {
Expand Down
20 changes: 13 additions & 7 deletions web-ui/modules/app.methods.openclaw-persist.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const DEFAULT_OPENCLAW_CONFIG_NAME = '默认配置';
export const DEFAULT_OPENCLAW_CONFIG_NAME = '默认配置';

function buildNormalizedOpenclawConfigs(configs, defaultContent = '') {
const source = configs && typeof configs === 'object' && !Array.isArray(configs)
Expand All @@ -11,7 +11,8 @@ function buildNormalizedOpenclawConfigs(configs, defaultContent = '') {
: { content: defaultContent };
const normalized = {
[DEFAULT_OPENCLAW_CONFIG_NAME]: {
content: typeof defaultEntry.content === 'string' ? defaultEntry.content : defaultContent
content: typeof defaultEntry.content === 'string' ? defaultEntry.content : defaultContent,
isDefault: true
}
};
for (const [name, value] of Object.entries(source)) {
Expand All @@ -25,7 +26,8 @@ function syncDefaultOpenclawConfigState(vm, content, options = {}) {
const nextContent = typeof content === 'string' ? content : '';
vm.openclawConfigs = buildNormalizedOpenclawConfigs(vm.openclawConfigs, nextContent);
vm.openclawConfigs[DEFAULT_OPENCLAW_CONFIG_NAME] = {
content: nextContent
content: nextContent,
isDefault: true
};
if (typeof options.path === 'string') {
vm.openclawConfigPath = options.path;
Expand All @@ -51,6 +53,10 @@ export function createOpenclawPersistMethods(options = {}) {
} = options;

return {
isDefaultOpenclawConfig(name, config = null) {
return !!(config && config.isDefault === true) || name === DEFAULT_OPENCLAW_CONFIG_NAME;
},

syncDefaultOpenclawConfigEntry(options = {}) {
const silent = !!options.silent;
return api('get-openclaw-config')
Expand Down Expand Up @@ -104,7 +110,7 @@ export function createOpenclawPersistMethods(options = {}) {

openOpenclawEditModal(name) {
const existing = this.openclawConfigs[name];
const isDefaultConfig = name === DEFAULT_OPENCLAW_CONFIG_NAME;
const isDefaultConfig = this.isDefaultOpenclawConfig(name, existing);
const modalToken = (Number(this.openclawModalLoadToken || 0) + 1);
this.openclawModalLoadToken = modalToken;
this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
Expand Down Expand Up @@ -146,7 +152,7 @@ export function createOpenclawPersistMethods(options = {}) {
const force = !!options.force;
const fallbackToTemplate = options.fallbackToTemplate !== false;
const syncDefaultEntry = options.syncDefaultEntry === true
|| (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME);
|| (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the config object when checking default status in guarded save/load paths.

On Line 155 and Line 269, isDefaultOpenclawConfig is called with only name, which bypasses marker-based detection (config.isDefault === true) for non-canonical names. This makes save/load behavior inconsistent with deletion guard logic and can allow local-save on a marked default entry.

Suggested fix
-                || (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name));
+                || (
+                    this.openclawEditing
+                    && this.openclawEditing.lockName
+                    && this.isDefaultOpenclawConfig(
+                        this.openclawEditing.name,
+                        this.openclawConfigs && this.openclawConfigs[this.openclawEditing.name]
+                    )
+                );
-            if (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name)) {
+            if (
+                this.openclawEditing
+                && this.openclawEditing.lockName
+                && this.isDefaultOpenclawConfig(
+                    this.openclawEditing.name,
+                    this.openclawConfigs && this.openclawConfigs[this.openclawEditing.name]
+                )
+            ) {

Also applies to: 269-269

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` at line 155, Calls to
isDefaultOpenclawConfig currently pass only the config name (e.g.,
this.isDefaultOpenclawConfig(this.openclawEditing.name)), which skips
marker-based detection (config.isDefault) and makes guarded save/load
inconsistent; update both guarded save/load checks to pass the full config
object (e.g., this.openclawEditing) or the actual config variable instead of
just name so isDefaultOpenclawConfig can inspect config.isDefault; target the
calls referencing this.openclawEditing and any similar call that supplies only a
name to isDefaultOpenclawConfig.

const modalToken = Number(options.modalToken || this.openclawModalLoadToken || 0);
const expectedEditorContent = typeof options.expectedEditorContent === 'string'
? options.expectedEditorContent
Expand Down Expand Up @@ -260,7 +266,7 @@ export function createOpenclawPersistMethods(options = {}) {
if (this.openclawSaving || this.openclawApplying) {
return;
}
if (this.openclawEditing && this.openclawEditing.lockName && this.openclawEditing.name === DEFAULT_OPENCLAW_CONFIG_NAME) {
if (this.openclawEditing && this.openclawEditing.lockName && this.isDefaultOpenclawConfig(this.openclawEditing.name)) {
this.showMessage('默认配置代表当前系统配置,请使用“保存并应用”', 'info');
return;
}
Expand Down Expand Up @@ -312,7 +318,7 @@ export function createOpenclawPersistMethods(options = {}) {
},

async deleteOpenclawConfig(name) {
if (name === DEFAULT_OPENCLAW_CONFIG_NAME) {
if (this.isDefaultOpenclawConfig(name, this.openclawConfigs && this.openclawConfigs[name])) {
return this.showMessage('默认配置始终映射当前系统配置,不可删除', 'info');
}
if (Object.keys(this.openclawConfigs).length <= 1) {
Expand Down
4 changes: 4 additions & 0 deletions web-ui/modules/i18n.dict.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,8 @@ const DICT = Object.freeze({

// OpenClaw config panel
'openclaw.applyHint': '写入 ~/.openclaw/openclaw.json,支持 JSON5。',
'openclaw.workspace.title': 'OpenClaw 工作区',
'openclaw.configs.hint': '选择常用配置,或进入编辑器维护完整 JSON5。',
'openclaw.agents.hint': '读写 Workspace 的 AGENTS.md,默认路径 ~/.openclaw/workspace/AGENTS.md。',
'openclaw.agents.open': '打开 AGENTS.md',
'openclaw.workspaceFile': '工作区文件',
Expand Down Expand Up @@ -3178,6 +3180,8 @@ const DICT = Object.freeze({

// OpenClaw config panel
'openclaw.applyHint': 'Writes to ~/.openclaw/openclaw.json (JSON5 supported).',
'openclaw.workspace.title': 'OpenClaw workspace',
'openclaw.configs.hint': 'Select a saved config, or open the editor to maintain full JSON5.',
'openclaw.agents.hint': 'Read/write Workspace AGENTS.md. Default: ~/.openclaw/workspace/AGENTS.md.',
'openclaw.agents.open': 'Open AGENTS.md',
'openclaw.workspaceFile': 'Workspace file',
Expand Down
4 changes: 2 additions & 2 deletions web-ui/partials/index/modal-openclaw-config.html
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,14 @@
:readonly="openclawSaving || openclawApplying"
placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
<div class="template-editor-warning">
<span v-if="openclawEditing.lockName && openclawEditing.name === '默认配置'">默认配置始终映射当前 openclaw.json,请直接使用“保存并应用”。</span>
<span v-if="openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name)">默认配置始终映射当前 openclaw.json,请直接使用“保存并应用”。</span>
<span v-else>保存仅写入本地配置库。点击“保存并应用”后会写入 openclaw.json。</span>
</div>
</div>

<div class="btn-group">
<button class="btn btn-cancel" @click="closeOpenclawConfigModal" :disabled="openclawSaving || openclawApplying">取消</button>
<button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving || openclawApplying || (openclawEditing.lockName && openclawEditing.name === '默认配置')">
<button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving || openclawApplying || (openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name))">
{{ openclawSaving ? '保存中...' : '保存' }}
</button>
<button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawSaving || openclawApplying">
Expand Down
Loading
Loading