mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
A conversation opens on the first model its endpoint offers, and that was a priced rung. An account on the free plan holds no balance by design — it is free, and buys its own credit — so its first message reached the wallet gate where an answer belonged. The plan is read from the one service that decides it (cloud /v1/allowance, which names the plan and the day's free calls) and the free route leads the catalog for a free caller. Reordered, never trimmed: the priced rungs stay listed, because seeing what a plan buys is the offer. A plan that cannot be read is not a free plan, so no failure of that read gives the free lane away. Consent follows the route rather than the caller. The free models cost nothing and share data in exchange; that is as true of a free plan defaulting to them as of a guest pinned to them. A spent day of free calls now renders as the limit it is. The gateway names it allowance_spent beside the same 402 an empty wallet gets, and unmapped it read as "something went wrong on our side" — an outage, at the moment an account is most ready to buy. It says when the count returns and offers the plan that lifts it, and no longer offers a switch to the route that just refused.
65 lines
2.1 KiB
JavaScript
65 lines
2.1 KiB
JavaScript
jest.mock('@hanzochat/api', () => ({ resolveTenantBearer: jest.fn() }));
|
|
|
|
const { resolveTenantBearer } = require('@hanzochat/api');
|
|
const { planOf, FREE } = require('./plan');
|
|
|
|
const req = { headers: {} };
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
resolveTenantBearer.mockReturnValue('bearer');
|
|
process.env.HANZO_CLOUD_URL = 'http://cloud.hanzo.svc:8000';
|
|
global.fetch = jest.fn();
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.HANZO_CLOUD_URL;
|
|
});
|
|
|
|
const answers = (body, ok = true) =>
|
|
global.fetch.mockResolvedValue({ ok, json: async () => body });
|
|
|
|
describe('planOf', () => {
|
|
it('reads the plan the allowance names', async () => {
|
|
answers({ plan: 'free', limit: 20, used: 0, spent: false });
|
|
await expect(planOf(req)).resolves.toBe(FREE);
|
|
const [url, init] = global.fetch.mock.calls[0];
|
|
expect(url).toBe('http://cloud.hanzo.svc:8000/v1/allowance');
|
|
expect(init.headers.Authorization).toBe('Bearer bearer');
|
|
});
|
|
|
|
it('reads a paid plan as itself', async () => {
|
|
answers({ plan: 'pro', limit: 0 });
|
|
await expect(planOf(req)).resolves.toBe('pro');
|
|
});
|
|
|
|
// Every way of not knowing answers the same: null, never FREE. Guessing free
|
|
// from silence would give the free lane away on any hiccup.
|
|
it('answers null when the caller has no bearer', async () => {
|
|
resolveTenantBearer.mockReturnValue(null);
|
|
await expect(planOf(req)).resolves.toBeNull();
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('answers null when no cloud is configured', async () => {
|
|
delete process.env.HANZO_CLOUD_URL;
|
|
await expect(planOf(req)).resolves.toBeNull();
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('answers null when cloud refuses', async () => {
|
|
answers({}, false);
|
|
await expect(planOf(req)).resolves.toBeNull();
|
|
});
|
|
|
|
it('answers null when cloud is unreachable', async () => {
|
|
global.fetch.mockRejectedValue(new Error('timed out'));
|
|
await expect(planOf(req)).resolves.toBeNull();
|
|
});
|
|
|
|
it('answers null when the answer names no plan', async () => {
|
|
answers({ limit: 5 });
|
|
await expect(planOf(req)).resolves.toBeNull();
|
|
});
|
|
});
|