Skip to content

Commit ee5f09a

Browse files
committed
fix: tighten codex budget review follow-ups
1 parent fcf26b5 commit ee5f09a

6 files changed

Lines changed: 246 additions & 15 deletions

File tree

cli.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,11 +3445,13 @@ function getConfigTemplate(params = {}) {
34453445
}
34463446

34473447
function readPositiveIntegerConfigValue(config, key) {
3448+
const options = arguments[2] && typeof arguments[2] === 'object' ? arguments[2] : {};
3449+
const useDefaultsWhenMissing = options.useDefaultsWhenMissing !== false;
34483450
if (!config || typeof config !== 'object' || !key) {
34493451
return '';
34503452
}
34513453
const raw = config[key];
3452-
if (raw === undefined) {
3454+
if (raw === undefined && useDefaultsWhenMissing) {
34533455
if (key === 'model_context_window') return DEFAULT_MODEL_CONTEXT_WINDOW;
34543456
if (key === 'model_auto_compact_token_limit') return DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT;
34553457
}
@@ -10095,8 +10097,19 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1009510097
const config = statusConfigResult.config;
1009610098
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
1009710099
const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
10098-
const modelContextWindow = readPositiveIntegerConfigValue(config, 'model_context_window');
10099-
const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(config, 'model_auto_compact_token_limit');
10100+
const budgetReadOptions = {
10101+
useDefaultsWhenMissing: !hasConfigLoadError(statusConfigResult)
10102+
};
10103+
const modelContextWindow = readPositiveIntegerConfigValue(
10104+
config,
10105+
'model_context_window',
10106+
budgetReadOptions
10107+
);
10108+
const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(
10109+
config,
10110+
'model_auto_compact_token_limit',
10111+
budgetReadOptions
10112+
);
1010010113
result = {
1010110114
provider: config.model_provider || '未设置',
1010210115
model: config.model || '未设置',
@@ -11583,8 +11596,19 @@ function buildMcpStatusPayload() {
1158311596
const config = statusConfigResult.config;
1158411597
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
1158511598
const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
11586-
const modelContextWindow = readPositiveIntegerConfigValue(config, 'model_context_window');
11587-
const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(config, 'model_auto_compact_token_limit');
11599+
const budgetReadOptions = {
11600+
useDefaultsWhenMissing: !hasConfigLoadError(statusConfigResult)
11601+
};
11602+
const modelContextWindow = readPositiveIntegerConfigValue(
11603+
config,
11604+
'model_context_window',
11605+
budgetReadOptions
11606+
);
11607+
const modelAutoCompactTokenLimit = readPositiveIntegerConfigValue(
11608+
config,
11609+
'model_auto_compact_token_limit',
11610+
budgetReadOptions
11611+
);
1158811612
return {
1158911613
provider: config.model_provider || '未设置',
1159011614
model: config.model || '未设置',

tests/e2e/test-config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ module.exports = async function testConfig(ctx) {
9595
modelAutoCompactTokenLimit: 195000
9696
});
9797
assert(typeof templateContextBudget.template === 'string', 'get-config-template(context budget) missing template');
98+
assert(templateContextBudget.template.includes('model_provider = "shadow"'), 'get-config-template(context budget) missing provider override');
99+
assert(templateContextBudget.template.includes('model = "shadow-model"'), 'get-config-template(context budget) missing model override');
98100
assert(/^\s*model_context_window\s*=\s*200000\s*$/m.test(templateContextBudget.template), 'get-config-template(context budget) missing model_context_window');
99101
assert(/^\s*model_auto_compact_token_limit\s*=\s*195000\s*$/m.test(templateContextBudget.template), 'get-config-template(context budget) missing model_auto_compact_token_limit');
100102

tests/unit/config-tabs-ui.test.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ test('config template keeps expected config tabs in top and side navigation', ()
3232
assert.match(html, /<span class="selector-title"><\/span>/);
3333
assert.match(html, /v-model="modelContextWindowInput"/);
3434
assert.match(html, /v-model="modelAutoCompactTokenLimitInput"/);
35+
assert.match(html, /@focus="editingCodexBudgetField = 'modelContextWindowInput'"/);
36+
assert.match(html, /@focus="editingCodexBudgetField = 'modelAutoCompactTokenLimitInput'"/);
3537
assert.match(html, /@blur="onModelContextWindowBlur"/);
3638
assert.match(html, /@blur="onModelAutoCompactTokenLimitBlur"/);
3739
assert.match(html, /@keydown\.enter\.prevent="onModelContextWindowBlur"/);
3840
assert.match(html, /@keydown\.enter\.prevent="onModelAutoCompactTokenLimitBlur"/);
39-
assert.match(html, /@click="resetCodexContextBudgetDefaults"/);
40-
assert.match(html, />\s*\s*<\/button>/);
41+
assert.match(
42+
html,
43+
/<button[^>]*@click="resetCodexContextBudgetDefaults"[^>]*>[\s\S]*?[\s\S]*?<\/button>/
44+
);
4145
assert.match(html, /class="codex-config-grid"/);
4246
assert.match(html, /onSettingsTabClick\('backup'\)/);
4347
assert.match(html, /onSettingsTabClick\('trash'\)/);
@@ -200,6 +204,7 @@ test('web ui script defines provider mode metadata for codex only', () => {
200204
assert.match(appScript, /pendingProviderSwitch:\s*''/);
201205
assert.match(appScript, /modelContextWindowInput:\s*'190000'/);
202206
assert.match(appScript, /modelAutoCompactTokenLimitInput:\s*'185000'/);
207+
assert.match(appScript, /editingCodexBudgetField:\s*''/);
203208
assert.match(appScript, /statusRes\.modelContextWindow/);
204209
assert.match(appScript, /statusRes\.modelAutoCompactTokenLimit/);
205210
assert.match(appScript, /onModelContextWindowBlur\(\)/);

tests/unit/provider-share-command.test.mjs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,64 @@ test('readPositiveIntegerConfigValue falls back to defaults only when budget key
385385
assert.strictEqual(readPositiveIntegerConfigValue({ model_context_window: 0 }, 'model_context_window'), '');
386386
});
387387

388+
test('buildMcpStatusPayload does not synthesize budget defaults after config load errors', () => {
389+
const normalizePositiveIntegerParamSource = extractBlockBySignature(
390+
cliSource,
391+
'function normalizePositiveIntegerParam(value) {'
392+
);
393+
const normalizePositiveIntegerParam = instantiateFunction(
394+
normalizePositiveIntegerParamSource,
395+
'normalizePositiveIntegerParam'
396+
);
397+
const readPositiveIntegerConfigValueSource = extractBlockBySignature(
398+
cliSource,
399+
'function readPositiveIntegerConfigValue(config, key) {'
400+
);
401+
const readPositiveIntegerConfigValue = instantiateFunction(
402+
readPositiveIntegerConfigValueSource,
403+
'readPositiveIntegerConfigValue',
404+
{
405+
normalizePositiveIntegerParam,
406+
DEFAULT_MODEL_CONTEXT_WINDOW: 190000,
407+
DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT: 185000
408+
}
409+
);
410+
const buildMcpStatusPayloadSource = extractBlockBySignature(
411+
cliSource,
412+
'function buildMcpStatusPayload() {'
413+
);
414+
const hasConfigLoadErrorSource = extractBlockBySignature(
415+
cliSource,
416+
'function hasConfigLoadError(result) {'
417+
);
418+
const hasConfigLoadError = instantiateFunction(
419+
hasConfigLoadErrorSource,
420+
'hasConfigLoadError'
421+
);
422+
const buildMcpStatusPayload = instantiateFunction(
423+
buildMcpStatusPayloadSource,
424+
'buildMcpStatusPayload',
425+
{
426+
readConfigOrVirtualDefault: () => ({
427+
config: {},
428+
isVirtual: true,
429+
errorType: 'parse',
430+
reason: 'config.toml 解析失败'
431+
}),
432+
hasConfigLoadError,
433+
readPositiveIntegerConfigValue,
434+
consumeInitNotice: () => ''
435+
}
436+
);
437+
438+
const result = buildMcpStatusPayload();
439+
440+
assert.strictEqual(result.modelContextWindow, '');
441+
assert.strictEqual(result.modelAutoCompactTokenLimit, '');
442+
assert.strictEqual(result.configErrorType, 'parse');
443+
assert.strictEqual(result.configNotice, 'config.toml 解析失败');
444+
});
445+
388446
test('status api case keeps lexical declarations scoped to the switch branch', () => {
389447
assert.match(cliSource, /^\s*case\s+['"]status['"]:\s*\{/m);
390448
});
@@ -471,8 +529,129 @@ test('applyCodexConfigDirect queues the latest pending budget update while an ap
471529
assert.strictEqual(templateRequests[1].modelContextWindow, 190000);
472530
assert.strictEqual(templateRequests[1].modelAutoCompactTokenLimit, 175000);
473531
assert.strictEqual(appliedTemplates.length, 2);
532+
assert.strictEqual(appliedTemplates[0].template, 'template-1');
533+
assert.strictEqual(appliedTemplates[1].template, 'template-2');
474534
assert.strictEqual(loadAllCalls, 2);
475535
assert.strictEqual(context._pendingCodexApplyOptions, null);
476536
assert.strictEqual(context.codexApplying, false);
477537
assert.deepStrictEqual(messages, []);
478538
});
539+
540+
test('loadAll preserves an unsaved codex budget draft while refreshing the sibling value', async () => {
541+
const loadAllSource = extractBlockBySignature(
542+
appSource,
543+
'async loadAll() {'
544+
).replace(/^async loadAll/, 'async function loadAll');
545+
const loadAll = instantiateFunction(loadAllSource, 'loadAll', {
546+
DEFAULT_MODEL_CONTEXT_WINDOW: 190000,
547+
DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT: 185000,
548+
api: async (action) => {
549+
if (action === 'status') {
550+
return {
551+
provider: 'alpha',
552+
model: 'alpha-model',
553+
serviceTier: 'fast',
554+
modelReasoningEffort: 'high',
555+
modelContextWindow: 200000,
556+
modelAutoCompactTokenLimit: 185000,
557+
configReady: true,
558+
initNotice: ''
559+
};
560+
}
561+
if (action === 'list') {
562+
return {
563+
providers: [{ name: 'alpha', url: 'https://api.example.com/v1', hasKey: true }]
564+
};
565+
}
566+
throw new Error(`Unexpected api action: ${action}`);
567+
}
568+
});
569+
570+
const context = {
571+
loading: false,
572+
initError: '',
573+
currentProvider: 'alpha',
574+
currentModel: 'alpha-model',
575+
serviceTier: 'fast',
576+
modelReasoningEffort: 'high',
577+
modelContextWindowInput: '190000',
578+
modelAutoCompactTokenLimitInput: '180000',
579+
editingCodexBudgetField: 'modelAutoCompactTokenLimitInput',
580+
providersList: [],
581+
normalizePositiveIntegerInput(value, label, fallback = '') {
582+
const raw = value === undefined || value === null || value === ''
583+
? String(fallback || '')
584+
: String(value);
585+
const text = raw.trim();
586+
const numeric = Number.parseInt(text, 10);
587+
if (!Number.isFinite(numeric) || numeric <= 0) {
588+
return { ok: false, error: `${label} invalid` };
589+
}
590+
return { ok: true, value: numeric, text: String(numeric) };
591+
},
592+
showMessage() {},
593+
maybeShowStarPrompt() {},
594+
async loadModelsForProvider() {},
595+
async loadCodexAuthProfiles() {}
596+
};
597+
598+
await loadAll.call(context);
599+
600+
assert.strictEqual(context.modelContextWindowInput, '200000');
601+
assert.strictEqual(context.modelAutoCompactTokenLimitInput, '180000');
602+
});
603+
604+
test('applyCodexConfigDirect surfaces backend validation details from direct apply failures', async () => {
605+
const applyCodexConfigDirectSource = extractBlockBySignature(
606+
appSource,
607+
'async applyCodexConfigDirect(options = {}) {'
608+
).replace(/^async applyCodexConfigDirect/, 'async function applyCodexConfigDirect');
609+
const messages = [];
610+
const applyCodexConfigDirect = instantiateFunction(applyCodexConfigDirectSource, 'applyCodexConfigDirect', {
611+
DEFAULT_MODEL_CONTEXT_WINDOW: 190000,
612+
DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT: 185000,
613+
api: async (action) => {
614+
if (action === 'get-config-template') {
615+
return { error: '模板中的 model_context_window 必须是正整数' };
616+
}
617+
throw new Error(`Unexpected api action: ${action}`);
618+
}
619+
});
620+
621+
const context = {
622+
codexApplying: false,
623+
_pendingCodexApplyOptions: null,
624+
currentProvider: 'alpha',
625+
currentModel: 'alpha-model',
626+
serviceTier: 'fast',
627+
modelReasoningEffort: 'high',
628+
modelContextWindowInput: '190000',
629+
modelAutoCompactTokenLimitInput: '185000',
630+
normalizePositiveIntegerInput(value, label, fallback = '') {
631+
const raw = value === undefined || value === null || value === ''
632+
? String(fallback || '')
633+
: String(value);
634+
const text = raw.trim();
635+
const numeric = Number.parseInt(text, 10);
636+
if (!Number.isFinite(numeric) || numeric <= 0) {
637+
return { ok: false, error: `${label} invalid` };
638+
}
639+
return { ok: true, value: numeric, text: String(numeric) };
640+
},
641+
showMessage(message, type) {
642+
messages.push({ message, type });
643+
},
644+
async loadAll() {
645+
throw new Error('loadAll should not be called when template generation fails');
646+
}
647+
};
648+
649+
await applyCodexConfigDirect.call(context, { silent: true });
650+
651+
assert.deepStrictEqual(messages, [{
652+
message: '模板中的 model_context_window 必须是正整数',
653+
type: 'error'
654+
}]);
655+
assert.strictEqual(context.codexApplying, false);
656+
assert.strictEqual(context._pendingCodexApplyOptions, null);
657+
});

web-ui/app.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
118118
modelReasoningEffort: 'high',
119119
modelContextWindowInput: '190000',
120120
modelAutoCompactTokenLimitInput: '185000',
121+
editingCodexBudgetField: '',
121122
providersList: [],
122123
models: [],
123124
codexModelsLoading: false,
@@ -714,19 +715,23 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
714715
'model_context_window',
715716
DEFAULT_MODEL_CONTEXT_WINDOW
716717
);
717-
this.modelContextWindowInput = contextWindow.ok && contextWindow.text
718-
? contextWindow.text
719-
: '190000';
718+
if (this.editingCodexBudgetField !== 'modelContextWindowInput') {
719+
this.modelContextWindowInput = contextWindow.ok && contextWindow.text
720+
? contextWindow.text
721+
: '190000';
722+
}
720723
}
721724
{
722725
const autoCompactTokenLimit = this.normalizePositiveIntegerInput(
723726
statusRes.modelAutoCompactTokenLimit,
724727
'model_auto_compact_token_limit',
725728
DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT
726729
);
727-
this.modelAutoCompactTokenLimitInput = autoCompactTokenLimit.ok && autoCompactTokenLimit.text
728-
? autoCompactTokenLimit.text
729-
: '185000';
730+
if (this.editingCodexBudgetField !== 'modelAutoCompactTokenLimitInput') {
731+
this.modelAutoCompactTokenLimitInput = autoCompactTokenLimit.ok && autoCompactTokenLimit.text
732+
? autoCompactTokenLimit.text
733+
: '185000';
734+
}
730735
}
731736
this.providersList = listRes.providers;
732737
if (statusRes.configReady === false) {
@@ -3244,6 +3249,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
32443249
},
32453250

32463251
async onModelContextWindowBlur() {
3252+
this.editingCodexBudgetField = '';
32473253
const normalized = this.normalizePositiveIntegerInput(
32483254
this.modelContextWindowInput,
32493255
'model_context_window',
@@ -3261,6 +3267,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
32613267
},
32623268

32633269
async onModelAutoCompactTokenLimitBlur() {
3270+
this.editingCodexBudgetField = '';
32643271
const normalized = this.normalizePositiveIntegerInput(
32653272
this.modelAutoCompactTokenLimitInput,
32663273
'model_auto_compact_token_limit',
@@ -3452,15 +3459,27 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
34523459
modelAutoCompactTokenLimit: modelAutoCompactTokenLimit.value
34533460
});
34543461
if (tplRes.error) {
3455-
this.showMessage('获取模板失败', 'error');
3462+
this.showMessage(
3463+
(typeof tplRes.error === 'string' && tplRes.error.trim())
3464+
|| (typeof tplRes.message === 'string' && tplRes.message.trim())
3465+
|| (typeof tplRes.detail === 'string' && tplRes.detail.trim())
3466+
|| '获取模板失败',
3467+
'error'
3468+
);
34563469
return;
34573470
}
34583471

34593472
const applyRes = await api('apply-config-template', {
34603473
template: tplRes.template
34613474
});
34623475
if (applyRes.error) {
3463-
this.showMessage('应用模板失败', 'error');
3476+
this.showMessage(
3477+
(typeof applyRes.error === 'string' && applyRes.error.trim())
3478+
|| (typeof applyRes.message === 'string' && applyRes.message.trim())
3479+
|| (typeof applyRes.detail === 'string' && applyRes.detail.trim())
3480+
|| '应用模板失败',
3481+
'error'
3482+
);
34643483
return;
34653484
}
34663485

web-ui/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ <h1 class="main-title">
490490
inputmode="numeric"
491491
autocomplete="off"
492492
placeholder="例如: 190000"
493+
@focus="editingCodexBudgetField = 'modelContextWindowInput'"
493494
@input="sanitizePositiveIntegerDraft('modelContextWindowInput')"
494495
@blur="onModelContextWindowBlur"
495496
@keydown.enter.prevent="onModelContextWindowBlur">
@@ -504,6 +505,7 @@ <h1 class="main-title">
504505
inputmode="numeric"
505506
autocomplete="off"
506507
placeholder="例如: 185000"
508+
@focus="editingCodexBudgetField = 'modelAutoCompactTokenLimitInput'"
507509
@input="sanitizePositiveIntegerDraft('modelAutoCompactTokenLimitInput')"
508510
@blur="onModelAutoCompactTokenLimitBlur"
509511
@keydown.enter.prevent="onModelAutoCompactTokenLimitBlur">

0 commit comments

Comments
 (0)