Files
chat/api/server/controllers/ModelController.spec.js
zooqueen 868d12da48 a free conversation is titled on the route it already uses
Titles are generated with the endpoint's configured title model, which is
priced. A conversation that opens on the free route belongs to an account with
no balance, so every one of those titles was refused at the wallet and the chat
kept the name it was born with.

The conversation's own model is already the starting point; for a free one,
leaving it there is the answer.

Also pins that reordering the catalog copies it. The list it reorders is the
cached one every caller is served, and doing it in place would let one free
caller move the default for everybody.
2026-08-16 03:14:58 -07:00

87 lines
3.2 KiB
JavaScript

const { FREE_MODEL } = require('@hanzo/ai');
jest.mock('@hanzochat/data-schemas', () => ({ logger: { error: jest.fn() } }));
jest.mock('~/server/services/Config', () => ({
loadDefaultModels: jest.fn(),
loadConfigModels: jest.fn(),
}));
jest.mock('~/cache', () => ({ getLogStores: jest.fn() }));
jest.mock('~/server/services/guestConfig', () => ({
buildGuestModelsConfig: jest.fn(() => ({ Hanzo: [require('@hanzo/ai').FREE_MODEL] })),
}));
jest.mock('~/server/services/plan', () => ({ planOf: jest.fn(), FREE: 'free' }));
const { getLogStores } = require('~/cache');
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
const { planOf } = require('~/server/services/plan');
const { modelController, leadFree } = require('./ModelController');
/** The deployed shape: priced rungs first, the free route last. */
const CATALOG = {
Hanzo: ['enso', 'enso-flash', 'zen5', FREE_MODEL],
Anthropic: ['claude-sonnet'],
};
const answer = () => {
const res = { send: jest.fn(), status: jest.fn(() => res) };
return res;
};
beforeEach(() => {
jest.clearAllMocks();
getLogStores.mockReturnValue({ get: jest.fn().mockResolvedValue(CATALOG), set: jest.fn() });
loadDefaultModels.mockResolvedValue({});
loadConfigModels.mockResolvedValue(CATALOG);
});
describe('leadFree', () => {
it('puts the free route first without dropping a priced one', () => {
expect(leadFree(CATALOG).Hanzo).toEqual(['enso-free', 'enso', 'enso-flash', 'zen5']);
});
it('leaves an endpoint that has no free route exactly as it was', () => {
expect(leadFree(CATALOG).Anthropic).toEqual(['claude-sonnet']);
});
// The catalog it reorders is the CACHED one, shared by every caller. Reordering
// it in place would let one free caller's request move the default for
// everybody, paying callers included.
it('does not touch the catalog it was given', () => {
const before = JSON.stringify(CATALOG);
leadFree(CATALOG);
expect(JSON.stringify(CATALOG)).toBe(before);
});
});
describe('modelController', () => {
it('leads a free plan with the model it can actually run', async () => {
planOf.mockResolvedValue('free');
const res = answer();
await modelController({ user: {} }, res);
expect(res.send.mock.calls[0][0].Hanzo[0]).toBe(FREE_MODEL);
});
it('leaves a paid plan on the model it pays for', async () => {
planOf.mockResolvedValue('pro');
const res = answer();
await modelController({ user: {} }, res);
expect(res.send.mock.calls[0][0].Hanzo[0]).toBe('enso');
});
// A plan that could not be read is not a free plan. Reading silence as free
// would hand the free lane to every caller cloud failed to name.
it('leaves an unreadable plan on the paid default', async () => {
planOf.mockResolvedValue(null);
const res = answer();
await modelController({ user: {} }, res);
expect(res.send.mock.calls[0][0].Hanzo[0]).toBe('enso');
});
it('narrows a guest to the one model they are pinned to, and asks no plan', async () => {
const res = answer();
await modelController({ user: { guest: true } }, res);
expect(res.send).toHaveBeenCalledWith({ Hanzo: [FREE_MODEL] });
expect(planOf).not.toHaveBeenCalled();
});
});