@@ -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+
388446test ( 'status api case keeps lexical declarations scoped to the switch branch' , ( ) => {
389447 assert . match ( cliSource , / ^ \s * c a s e \s + [ ' " ] s t a t u s [ ' " ] : \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 ( / ^ a s y n c l o a d A l l / , '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 ( / ^ a s y n c a p p l y C o d e x C o n f i g D i r e c t / , '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+ } ) ;
0 commit comments