@@ -4,10 +4,13 @@ import type { ModelOptionProvider } from '@/types/hermes'
44
55import {
66 collapseModelFamilies ,
7+ defaultVisibleKeys ,
78 effectiveVisibleKeys ,
89 emptyProviderSentinelKey ,
910 isProviderSentinel ,
10- modelVisibilityKey
11+ modelVisibilityKey ,
12+ resolveVisibleKeys ,
13+ toggleModelVisibility
1114} from './model-visibility'
1215
1316const provider = ( slug : string , models : string [ ] ) : ModelOptionProvider => ( {
@@ -96,4 +99,133 @@ describe('model visibility', () => {
9699 expect ( isProviderSentinel ( 'openai::' ) ) . toBe ( true )
97100 expect ( isProviderSentinel ( 'openai::gpt-4o' ) ) . toBe ( false )
98101 } )
102+
103+ it ( 'resolveVisibleKeys preserves sentinels that effectiveVisibleKeys strips' , ( ) => {
104+ const stored = new Set ( [ emptyProviderSentinelKey ( 'nous' ) ] )
105+ const providers = [ provider ( 'nous' , [ 'hermes-x' , 'hermes-y' ] ) , provider ( 'ollama' , [ 'qwen3:latest' ] ) ]
106+
107+ const resolved = resolveVisibleKeys ( stored , providers )
108+ expect ( resolved . has ( emptyProviderSentinelKey ( 'nous' ) ) ) . toBe ( true )
109+ expect ( resolved . has ( modelVisibilityKey ( 'nous' , 'hermes-x' ) ) ) . toBe ( false )
110+ // Un-customized providers still expand to their defaults.
111+ expect ( resolved . has ( modelVisibilityKey ( 'ollama' , 'qwen3:latest' ) ) ) . toBe ( true )
112+
113+ // Display variant drops the sentinel.
114+ expect ( effectiveVisibleKeys ( stored , providers ) . has ( emptyProviderSentinelKey ( 'nous' ) ) ) . toBe ( false )
115+ } )
116+ } )
117+
118+ describe ( 'toggleModelVisibility' , ( ) => {
119+ const providers = [ provider ( 'openai' , [ 'gpt-a' , 'gpt-b' ] ) , provider ( 'nous' , [ 'hermes-x' , 'hermes-y' ] ) ]
120+
121+ // Drive the handler the way the dialog does: feed each result back in as the
122+ // next `stored`, so the persisted set is what the next toggle starts from.
123+ const apply = ( stored : Set < string > | null , slug : string , model : string ) =>
124+ toggleModelVisibility ( stored , providers , slug , model )
125+
126+ it ( 'records a hide-all sentinel when the last model of a provider is toggled off' , ( ) => {
127+ let stored : Set < string > | null = null
128+ stored = apply ( stored , 'openai' , 'gpt-a' )
129+ stored = apply ( stored , 'openai' , 'gpt-b' )
130+
131+ expect ( stored . has ( emptyProviderSentinelKey ( 'openai' ) ) ) . toBe ( true )
132+ expect ( effectiveVisibleKeys ( stored , providers ) . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( false )
133+ expect ( effectiveVisibleKeys ( stored , providers ) . has ( modelVisibilityKey ( 'openai' , 'gpt-b' ) ) ) . toBe ( false )
134+ } )
135+
136+ it ( 'keeps a hidden provider hidden when a different provider is toggled (regression for #43485)' , ( ) => {
137+ // Hide ALL of nous — its sentinel is now stored.
138+ let stored : Set < string > | null = null
139+ stored = apply ( stored , 'nous' , 'hermes-x' )
140+ stored = apply ( stored , 'nous' , 'hermes-y' )
141+ expect ( stored . has ( emptyProviderSentinelKey ( 'nous' ) ) ) . toBe ( true )
142+
143+ // Toggle a model in another provider. nous must NOT snap back on.
144+ stored = apply ( stored , 'openai' , 'gpt-a' )
145+
146+ expect ( stored . has ( emptyProviderSentinelKey ( 'nous' ) ) ) . toBe ( true )
147+ const visible = effectiveVisibleKeys ( stored , providers )
148+ expect ( visible . has ( modelVisibilityKey ( 'nous' , 'hermes-x' ) ) ) . toBe ( false )
149+ expect ( visible . has ( modelVisibilityKey ( 'nous' , 'hermes-y' ) ) ) . toBe ( false )
150+ } )
151+
152+ it ( 'clears only the toggled provider sentinel when a model is re-enabled' , ( ) => {
153+ let stored : Set < string > | null = new Set ( [ emptyProviderSentinelKey ( 'openai' ) , emptyProviderSentinelKey ( 'nous' ) ] )
154+
155+ stored = apply ( stored , 'openai' , 'gpt-a' )
156+
157+ expect ( stored . has ( emptyProviderSentinelKey ( 'openai' ) ) ) . toBe ( false )
158+ expect ( stored . has ( emptyProviderSentinelKey ( 'nous' ) ) ) . toBe ( true )
159+ const visible = effectiveVisibleKeys ( stored , providers )
160+ expect ( visible . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( true )
161+ expect ( visible . has ( modelVisibilityKey ( 'nous' , 'hermes-x' ) ) ) . toBe ( false )
162+ } )
163+
164+ it ( 're-enabling one model of a hidden-all provider restores ONLY that model, not the curated defaults' , ( ) => {
165+ // openai hidden-all, nous untouched.
166+ let stored : Set < string > | null = new Set ( [ emptyProviderSentinelKey ( 'openai' ) ] )
167+
168+ stored = apply ( stored , 'openai' , 'gpt-a' )
169+
170+ const visible = effectiveVisibleKeys ( stored , providers )
171+ expect ( visible . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( true )
172+ // gpt-b is NOT restored — "you hid everything, you get back only what you re-enable".
173+ expect ( visible . has ( modelVisibilityKey ( 'openai' , 'gpt-b' ) ) ) . toBe ( false )
174+ } )
175+
176+ it ( 're-hiding the last re-enabled model re-adds the sentinel (full round-trip)' , ( ) => {
177+ let stored : Set < string > | null = new Set ( [ emptyProviderSentinelKey ( 'openai' ) ] )
178+
179+ // Re-enable gpt-a (clears sentinel, set = {gpt-a}), then toggle it back off.
180+ stored = apply ( stored , 'openai' , 'gpt-a' )
181+ expect ( stored . has ( emptyProviderSentinelKey ( 'openai' ) ) ) . toBe ( false )
182+ stored = apply ( stored , 'openai' , 'gpt-a' )
183+
184+ expect ( stored . has ( emptyProviderSentinelKey ( 'openai' ) ) ) . toBe ( true )
185+ expect ( effectiveVisibleKeys ( stored , providers ) . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( false )
186+ } )
187+
188+ it ( 'toggling from an empty (non-null) stored set adds the model without expanding defaults' , ( ) => {
189+ // Empty-but-not-null = "everything hidden". resolveVisibleKeys short-circuits to {}.
190+ const stored = new Set < string > ( )
191+
192+ const next = apply ( stored , 'openai' , 'gpt-a' )
193+
194+ expect ( next . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( true )
195+ // No curated defaults were expanded for any provider.
196+ expect ( next . has ( modelVisibilityKey ( 'openai' , 'gpt-b' ) ) ) . toBe ( false )
197+ expect ( next . has ( modelVisibilityKey ( 'nous' , 'hermes-x' ) ) ) . toBe ( false )
198+ } )
199+
200+ it ( 'toggling off one default model from null stored keeps the rest of the curated defaults' , ( ) => {
201+ // null = "never customized": resolveVisibleKeys expands all defaults first.
202+ const next = apply ( null , 'openai' , 'gpt-a' )
203+
204+ expect ( next . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( false )
205+ expect ( next . has ( modelVisibilityKey ( 'openai' , 'gpt-b' ) ) ) . toBe ( true )
206+ expect ( next . has ( modelVisibilityKey ( 'nous' , 'hermes-x' ) ) ) . toBe ( true )
207+ // Other models remain, so no sentinel.
208+ expect ( next . has ( emptyProviderSentinelKey ( 'openai' ) ) ) . toBe ( false )
209+ } )
210+
211+ it ( 'tolerates a provider with zero models (defensive — dialog filters these out)' , ( ) => {
212+ const ps = [ provider ( 'empty' , [ ] ) , provider ( 'openai' , [ 'gpt-a' ] ) ]
213+ const next = toggleModelVisibility ( new Set ( [ modelVisibilityKey ( 'openai' , 'gpt-a' ) ] ) , ps , 'empty' , 'ghost' )
214+
215+ // No crash; the phantom key is recorded but no defaults are invented.
216+ expect ( [ ...next ] . some ( k => k . startsWith ( 'empty::' ) && ! isProviderSentinel ( k ) ) ) . toBe ( true )
217+ expect ( next . has ( modelVisibilityKey ( 'openai' , 'gpt-a' ) ) ) . toBe ( true )
218+ } )
219+ } )
220+
221+ describe ( 'resolveVisibleKeys' , ( ) => {
222+ const providers = [ provider ( 'openai' , [ 'gpt-a' , 'gpt-b' ] ) , provider ( 'nous' , [ 'hermes-x' , 'hermes-y' ] ) ]
223+
224+ it ( 'returns the curated defaults verbatim for null stored' , ( ) => {
225+ expect ( resolveVisibleKeys ( null , providers ) ) . toEqual ( defaultVisibleKeys ( providers ) )
226+ } )
227+
228+ it ( 'returns an empty set for an empty (non-null) stored set' , ( ) => {
229+ expect ( [ ...resolveVisibleKeys ( new Set ( ) , providers ) ] ) . toEqual ( [ ] )
230+ } )
99231} )
0 commit comments