|
1 | 1 | import { describe, expect, test } from 'bun:test' |
2 | 2 |
|
| 3 | +import { isFreeModeAllowedAgentModel } from '../constants/free-agents' |
3 | 4 | import { |
4 | 5 | canFreebuffModelSpawnGeminiThinker, |
5 | 6 | DEFAULT_FREEBUFF_MODEL_ID, |
@@ -49,6 +50,7 @@ import { |
49 | 50 | isFreebuffGpt56LunaModelId, |
50 | 51 | isFreebuffLimitedOfferModelId, |
51 | 52 | getFreebuffPerModelSessionCap, |
| 53 | + freebuffWithdrawnModelMessage, |
52 | 54 | isFreebuffPausedFreeModelId, |
53 | 55 | isFreebuffSessionModelAllowedForAccessTier, |
54 | 56 | isFreebuffSessionModelAvailable, |
@@ -163,9 +165,13 @@ describe('freebuff model availability', () => { |
163 | 165 | ) |
164 | 166 | }) |
165 | 167 |
|
166 | | - test('only the DeepSeek family is trace-stored in free mode; M3 has no warning', () => { |
167 | | - const m3 = FREEBUFF_MODELS.find((m) => m.id === MINIMAX_M3_MODEL_ID) |
168 | | - expect((m3 as { warning?: string } | undefined)?.warning).toBeUndefined() |
| 168 | + test('only the DeepSeek family is trace-stored in free mode', () => { |
| 169 | + // MiMo is the non-training row still in the picker; M3 was withdrawn on |
| 170 | + // 2026-08-20 and is no longer there to check. |
| 171 | + const mimo = FREEBUFF_MODELS.find( |
| 172 | + (m) => m.id === FREEBUFF_MIMO_V25_MODEL_ID, |
| 173 | + ) |
| 174 | + expect((mimo as { warning?: string } | undefined)?.warning).toBeUndefined() |
169 | 175 | // The DeepSeek family discloses AI training and IS stored. |
170 | 176 | expect(isFreebuffTracedModelId(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID)).toBe( |
171 | 177 | true, |
@@ -683,35 +689,43 @@ describe('freebuff model availability', () => { |
683 | 689 | ) |
684 | 690 | }) |
685 | 691 |
|
686 | | - test('MiniMax M3 is selectable and UNLIMITED, on no daily pool at all', () => { |
687 | | - expect(SUPPORTED_FREEBUFF_MODELS.map((model) => model.id)).toContain( |
688 | | - MINIMAX_M3_MODEL_ID, |
689 | | - ) |
690 | | - expect(FREEBUFF_MODELS.map((model) => model.id)).toContain( |
691 | | - MINIMAX_M3_MODEL_ID, |
692 | | - ) |
693 | | - expect(getFreebuffModelsForAccessTier('full').map((m) => m.id)).toContain( |
| 692 | + test('MiniMax M3 is withdrawn: recognised, refused, served to nobody', () => { |
| 693 | + // Withdrawn from free mode entirely on 2026-08-20 after reaching $213/hr. |
| 694 | + // Out of every picker and pool... |
| 695 | + expect(FREEBUFF_MODELS.map((model) => model.id)).not.toContain( |
694 | 696 | MINIMAX_M3_MODEL_ID, |
695 | 697 | ) |
696 | | - expect(isFreebuffModelId(MINIMAX_M3_MODEL_ID)).toBe(true) |
697 | | - expect(isSupportedFreebuffModelId(MINIMAX_M3_MODEL_ID)).toBe(true) |
698 | | - // Un-premiumed on 2026-08-20, so the premium pool is spent on the DeepSeek |
699 | | - // rows and Luna rather than on the cheapest one. Both must agree — a row |
700 | | - // dropped from the id list while still flagged `premium` is metered by NO |
701 | | - // pool (FREEBUFF_WEB_STANDARD_MODEL_IDS filters on `!premium`). |
| 698 | + expect(isFreebuffModelId(MINIMAX_M3_MODEL_ID)).toBe(false) |
702 | 699 | expect(isFreebuffPremiumModelId(MINIMAX_M3_MODEL_ID)).toBe(false) |
703 | | - expect(isFreebuffWebPremiumModelId(MINIMAX_M3_MODEL_ID)).toBe(false) |
704 | 700 | expect( |
705 | | - FREEBUFF_MODELS.find((m) => m.id === MINIMAX_M3_MODEL_ID)?.premium, |
| 701 | + isFreebuffSessionModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'full'), |
706 | 702 | ).toBe(false) |
707 | | - expect( |
708 | | - isFreebuffModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'full'), |
| 703 | + |
| 704 | + // ...but still RECOGNISED, which is what separates withdrawing a model from |
| 705 | + // breaking the clients that still ask for it. Released binaries keep this id |
| 706 | + // in their compiled-in catalog; an unrecognised id can only be refused, and |
| 707 | + // that refusal is the #1801 retry loop. |
| 708 | + expect(isFreebuffSessionModelId(MINIMAX_M3_MODEL_ID)).toBe(true) |
| 709 | + expect(isFreebuffPausedFreeModelId(MINIMAX_M3_MODEL_ID)).toBe(true) |
| 710 | + // It is REFUSED, not silently substituted — the user asked for a specific |
| 711 | + // model and is told it is gone, with what to use instead. The refusal is |
| 712 | + // not session-ending, so the client shows it rather than re-admitting. |
| 713 | + expect(freebuffWithdrawnModelMessage(MINIMAX_M3_MODEL_ID)).toContain( |
| 714 | + 'no longer available in Freebuff', |
| 715 | + ) |
| 716 | + expect(freebuffWithdrawnModelMessage(MINIMAX_M3_MODEL_ID)).toContain( |
| 717 | + 'DeepSeek V4 Flash', |
| 718 | + ) |
| 719 | + |
| 720 | + // The AGENT door stays open, and that is not an oversight. Withdrawal is |
| 721 | + // enforced at admission, so no NEW session can name the model. Sessions |
| 722 | + // admitted before the deploy are still live and hit this allowlist on |
| 723 | + // every turn; dropping the row would fail them mid-turn with |
| 724 | + // free_mode_invalid_agent_model — the same wedge withdrawal exists to |
| 725 | + // avoid. They drain against a door that is already shut in front of them. |
| 726 | + expect( |
| 727 | + isFreeModeAllowedAgentModel('base2-free-minimax-m3', MINIMAX_M3_MODEL_ID), |
709 | 728 | ).toBe(true) |
710 | | - // Flash leads again as of 2026-08-20: the one-a-day ceiling narrowed to V4 |
711 | | - // Pro, so the recommended row is no longer the capped one. |
712 | | - expect(FREEBUFF_MODELS[0]!.id).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID) |
713 | | - expect(FREEBUFF_MODELS[1]!.id).toBe(FREEBUFF_GPT_5_6_LUNA_MODEL_ID) |
714 | | - expect(FREEBUFF_MODELS[2]!.id).toBe(MINIMAX_M3_MODEL_ID) |
715 | 729 | }) |
716 | 730 |
|
717 | 731 | test('the recommended default leads FREEBUFF_MODELS, and the fallback is in it', () => { |
@@ -991,16 +1005,19 @@ describe('freebuff model availability', () => { |
991 | 1005 | } |
992 | 1006 | }) |
993 | 1007 |
|
994 | | - test('points users off MiniMax M3 to V4 Flash', () => { |
995 | | - // Flash-0731 overtook M3 on 2026-07-31, so M3 carries a notice and a switch |
996 | | - // target rather than being removed — it is still selectable. |
| 1008 | + test('a withdrawn model is not offered as anyone else’s switch target', () => { |
| 1009 | + // M3 carried a "switch to V4 Flash" nudge until it was withdrawn on |
| 1010 | + // 2026-08-20. Now the check runs the other way: the picker offers a |
| 1011 | + // one-click switch for whatever a notice names, so naming a withdrawn model |
| 1012 | + // would hand users a row the server refuses. |
997 | 1013 | const all = FREEBUFF_MODELS.map((model) => model.id) |
998 | | - const superseded = getFreebuffModelSupersededBy(MINIMAX_M3_MODEL_ID, all) |
999 | | - expect(superseded?.modelId).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID) |
1000 | | - expect(superseded!.notice.length).toBeGreaterThan(0) |
1001 | | - expect(superseded!.actionLabel.length).toBeGreaterThan(0) |
1002 | | - // M3 remains a real, selectable model — this is a nudge, not a retirement. |
1003 | | - expect(all).toContain(MINIMAX_M3_MODEL_ID) |
| 1014 | + expect(all).not.toContain(MINIMAX_M3_MODEL_ID) |
| 1015 | + for (const id of all) { |
| 1016 | + const superseded = getFreebuffModelSupersededBy(id, all) |
| 1017 | + if (!superseded) continue |
| 1018 | + expect(superseded.modelId).not.toBe(MINIMAX_M3_MODEL_ID) |
| 1019 | + expect(all).toContain(superseded.modelId) |
| 1020 | + } |
1004 | 1021 | // The recommended default is never itself marked superseded. |
1005 | 1022 | expect( |
1006 | 1023 | getFreebuffModelSupersededBy(DEFAULT_FREEBUFF_MODEL_ID, all), |
|
0 commit comments