Files
chat/api/server/services/plan.spec.js
zooqueen 694061e40b a free plan opens on the model it can run
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.
2026-08-16 01:56:48 -07:00

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();
});
});