@@ -183,6 +183,8 @@ func TestModelsBrowserRendersCardAndTableModes(t *testing.T) {
183183 Slots : []uiSlot {{Name : "default-or" , ModelID : "qwen/qwen3.5-plus" }},
184184 Models : []uiModel {{
185185 ID : "qwen/qwen3.5-plus" ,
186+ Name : "Qwen 3.5 Plus" ,
187+ Description : "Balanced multilingual model for tool-heavy assistant workflows." ,
186188 PromptPrice : 1.0 ,
187189 CompletionPrice : 3.0 ,
188190 ContextLength : 128000 ,
@@ -193,19 +195,22 @@ func TestModelsBrowserRendersCardAndTableModes(t *testing.T) {
193195 CatalogProvider : "openrouter" ,
194196 }
195197
196- t .Run ("cards default" , func (t * testing.T ) {
198+ t .Run ("compact default" , func (t * testing.T ) {
197199 data := base
198- data .Filters .View = "cards "
200+ data .Filters .View = "compact "
199201 var buf bytes.Buffer
200202 if err := render (& buf , viewModelsBrowser , data ); err != nil {
201203 t .Fatal (err )
202204 }
203205 html := buf .String ()
204- if ! strings .Contains (html , "catalog-grid" ) || ! strings .Contains (html , "catalog-card" ) {
205- t .Fatalf ("card view missing catalog cards: %s" , html )
206+ if ! strings .Contains (html , "catalog-list" ) || ! strings .Contains (html , "catalog-row" ) {
207+ t .Fatalf ("compact view missing catalog rows: %s" , html )
208+ }
209+ if ! strings .Contains (html , "Balanced multilingual model" ) {
210+ t .Fatalf ("compact view missing model description: %s" , html )
206211 }
207212 if strings .Contains (html , "catalog-audit-table" ) {
208- t .Fatalf ("card view should not render audit table" )
213+ t .Fatalf ("compact view should not render audit table" )
209214 }
210215 })
211216
@@ -220,14 +225,14 @@ func TestModelsBrowserRendersCardAndTableModes(t *testing.T) {
220225 if ! strings .Contains (html , "catalog-audit-table" ) {
221226 t .Fatalf ("table view missing audit table: %s" , html )
222227 }
223- if strings .Contains (html , "catalog-grid " ) || strings .Contains (html , "catalog-card " ) {
224- t .Fatalf ("table view should not render card layout" )
228+ if strings .Contains (html , "catalog-list " ) || strings .Contains (html , "catalog-row " ) {
229+ t .Fatalf ("table view should not render compact layout" )
225230 }
226231 })
227232
228233 t .Run ("preset state is preserved except clear" , func (t * testing.T ) {
229234 data := base
230- data .Filters .View = "cards "
235+ data .Filters .View = "compact "
231236 data .Filters .ActivePreset = "default"
232237 var buf bytes.Buffer
233238 if err := render (& buf , viewModelsBrowser , data ); err != nil {
@@ -237,12 +242,130 @@ func TestModelsBrowserRendersCardAndTableModes(t *testing.T) {
237242 if ! strings .Contains (html , `name="preset" value="default"` ) {
238243 t .Fatalf ("active preset should be submitted with filters and view toggle: %s" , html )
239244 }
240- if ! strings .Contains (html , `hx-get="/models?provider=openrouter&view=cards &full=1"` ) {
245+ if ! strings .Contains (html , `hx-get="/models?provider=openrouter&view=compact &full=1"` ) {
241246 t .Fatalf ("clear preset should refresh browser without preset: %s" , html )
242247 }
243248 })
244249}
245250
251+ func TestModelsBrowserRendersFreeCheckAction (t * testing.T ) {
252+ data := indexData {
253+ Slots : []uiSlot {{Name : "default-or" , ModelID : "qwen/qwen3.5-plus" }},
254+ Models : []uiModel {{
255+ ID : "qwen/qwen3.5-flash:free" ,
256+ Description : "Free endpoint for quick routing checks." ,
257+ PromptPrice : 0 ,
258+ CompletionPrice : 0 ,
259+ ContextLength : 64000 ,
260+ Tools : true ,
261+ Free : true ,
262+ Policy : "free_degraded" ,
263+ CheckStatus : "free_degraded" ,
264+ CheckLatencyMS : 321 ,
265+ StatusLabel : "Untested" ,
266+ PolicyLabel : "Free degraded" ,
267+ PrimaryReason : "Free endpoint degraded" ,
268+ }},
269+ Filters : uiFilters {View : "compact" },
270+ CatalogProvider : "openrouter" ,
271+ }
272+ var buf bytes.Buffer
273+ if err := render (& buf , viewModelsBrowser , data ); err != nil {
274+ t .Fatal (err )
275+ }
276+ html := buf .String ()
277+ if ! strings .Contains (html , `hx-post="/models/check"` ) || ! strings .Contains (html , "Check free" ) {
278+ t .Fatalf ("free check action missing: %s" , html )
279+ }
280+ if ! strings .Contains (html , "Free model must pass check before routing" ) {
281+ t .Fatalf ("unverified free model should not render active assign buttons: %s" , html )
282+ }
283+ if ! strings .Contains (html , "free_degraded" ) || ! strings .Contains (html , "321 ms" ) {
284+ t .Fatalf ("check status not rendered: %s" , html )
285+ }
286+ }
287+
288+ func TestWebModelCheckPersistsFreeModelStatus (t * testing.T ) {
289+ s , _ := newTGModelTestServer (t )
290+ settings := & fakeSettingsStore {values : map [string ]string {}}
291+ s .settings = settings
292+ s .modelProbe = func (context.Context , string , string , llm.Capabilities ) modelCheckStatus {
293+ return modelCheckStatus {
294+ Status : "free_verified" ,
295+ CheckedAt : "2026-06-15T12:00:00Z" ,
296+ LatencyMS : 88 ,
297+ }
298+ }
299+
300+ form := url.Values {}
301+ form .Set ("provider" , "openrouter" )
302+ form .Set ("model_id" , "qwen/qwen3.5-flash:free" )
303+ form .Set ("view" , "compact" )
304+ req := httptest .NewRequest (http .MethodPost , "/models/check" , strings .NewReader (form .Encode ()))
305+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
306+ rec := httptest .NewRecorder ()
307+ s .handleModelCheck (rec , req )
308+
309+ if rec .Code != http .StatusOK {
310+ t .Fatalf ("status = %d, want %d, body: %s" , rec .Code , http .StatusOK , rec .Body .String ())
311+ }
312+ checks := s .loadModelChecks (context .Background ())
313+ got := checks [modelCheckKey ("openrouter" , "qwen/qwen3.5-flash:free" )]
314+ if got .Status != "free_verified" || got .LatencyMS != 88 {
315+ t .Fatalf ("status not persisted: %+v" , checks )
316+ }
317+ if ! strings .Contains (rec .Body .String (), "free_verified" ) {
318+ t .Fatalf ("response should render updated check status: %s" , rec .Body .String ())
319+ }
320+ }
321+
322+ func TestWebSlotAssignRejectsUnverifiedFreeModel (t * testing.T ) {
323+ s , provider := newTGModelTestServer (t )
324+ s .settings = & fakeSettingsStore {values : map [string ]string {}}
325+
326+ form := url.Values {}
327+ form .Set ("provider" , "openrouter" )
328+ form .Set ("model_id" , "qwen/qwen3.5-flash:free" )
329+ req := httptest .NewRequest (http .MethodPost , "/slots/default-or/assign" , strings .NewReader (form .Encode ()))
330+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
331+ rec := httptest .NewRecorder ()
332+ s .handleSlotAssign (rec , req )
333+
334+ if rec .Code != http .StatusBadRequest {
335+ t .Fatalf ("status = %d, want %d, body: %s" , rec .Code , http .StatusBadRequest , rec .Body .String ())
336+ }
337+ if provider .CurrentModel () == "qwen/qwen3.5-flash:free" {
338+ t .Fatalf ("unverified free model should not be assigned" )
339+ }
340+ }
341+
342+ func TestWebSlotAssignAllowsVerifiedFreeModel (t * testing.T ) {
343+ s , provider := newTGModelTestServer (t )
344+ checks := map [string ]modelCheckStatus {
345+ modelCheckKey ("openrouter" , "qwen/qwen3.5-flash:free" ): {Status : "free_verified" , CheckedAt : time .Now ().Format (time .RFC3339 )},
346+ }
347+ data , err := json .Marshal (checks )
348+ if err != nil {
349+ t .Fatal (err )
350+ }
351+ s .settings = & fakeSettingsStore {values : map [string ]string {settingKeyModelChecks : string (data )}}
352+
353+ form := url.Values {}
354+ form .Set ("provider" , "openrouter" )
355+ form .Set ("model_id" , "qwen/qwen3.5-flash:free" )
356+ req := httptest .NewRequest (http .MethodPost , "/slots/default-or/assign" , strings .NewReader (form .Encode ()))
357+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
358+ rec := httptest .NewRecorder ()
359+ s .handleSlotAssign (rec , req )
360+
361+ if rec .Code != http .StatusOK {
362+ t .Fatalf ("status = %d, want %d, body: %s" , rec .Code , http .StatusOK , rec .Body .String ())
363+ }
364+ if provider .CurrentModel () != "qwen/qwen3.5-flash:free" {
365+ t .Fatalf ("model = %q, want free model" , provider .CurrentModel ())
366+ }
367+ }
368+
246369func signedTGInitData (t * testing.T , token string , userID int64 , authTime time.Time ) string {
247370 t .Helper ()
248371 userJSON , err := json .Marshal (tgAdminUser {ID : userID , FirstName : "Alex" })
0 commit comments