mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
Four lines on the empty chat: the model picker, the modes and their sources, /agent, and the sidebar. It dims nothing and gates nothing -- the arrival modal was removed by owner call, and this sits beside the product rather than in front of it. `toured` carries no schema default. `false` shows the card, `true` has seen it, and ABSENT is an account older than the card, which never gets one; a default reaches every record that lacks the field, which is every account that already exists. `reconcileUser` writes `false` at creation, so exactly the accounts born from here on qualify. The client tests `=== false` for the same reason, and the spec fails when that is weakened to a falsy check. It rides out on `GET /v1/chat/user`, which already sends the projected document, so there is no second GET to keep in step -- one POST records the dismissal, and the cached user is written on mutate so the card goes on the click rather than a round-trip later. In the flow, under the composer, not floating over it. The composer block is min-h-[280px] at the bottom of that column and its input is capped and centred, leaving ~142px of gutter at 1440 against a card that needs 304 -- measured in Chromium against the built stylesheet: 288x252 at 16px from the column edge, clear of the composer at 1440 and at 390, no sideways scroll, 17.25:1 on the title and 12.82:1 on the lines.
108 lines
4.1 KiB
JavaScript
108 lines
4.1 KiB
JavaScript
jest.mock('@hanzochat/data-schemas', () => ({
|
|
logger: { error: jest.fn(), warn: jest.fn(), debug: jest.fn(), info: jest.fn() },
|
|
}));
|
|
jest.mock('@hanzochat/data-provider', () => ({ SystemRoles: { USER: 'USER', ADMIN: 'ADMIN' } }));
|
|
|
|
const mockFindOpenIDUser = jest.fn();
|
|
jest.mock('@hanzochat/api', () => ({
|
|
findOpenIDUser: (...args) => mockFindOpenIDUser(...args),
|
|
getBalanceConfig: () => ({ enabled: false }),
|
|
}));
|
|
jest.mock('~/server/services/Config', () => ({ getAppConfig: async () => ({}) }));
|
|
|
|
const mockFindUser = jest.fn();
|
|
const mockCreateUser = jest.fn();
|
|
const mockUpdateUser = jest.fn();
|
|
jest.mock('~/models', () => ({
|
|
findUser: (...args) => mockFindUser(...args),
|
|
createUser: (...args) => mockCreateUser(...args),
|
|
updateUser: (...args) => mockUpdateUser(...args),
|
|
countUsers: async () => 7,
|
|
}));
|
|
|
|
const { reconcileUser } = require('./iamUser');
|
|
|
|
const claims = { sub: 'iam-subject-1', email: '[email protected]', name: 'Newcomer', owner: 'hanzo' };
|
|
|
|
describe('the local record behind an IAM identity', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockFindOpenIDUser.mockResolvedValue({ user: null, error: null, migration: false });
|
|
});
|
|
|
|
/**
|
|
* A first sign-in arrives as a burst — the app opens with a dozen
|
|
* authenticated requests at once, all carrying the same token, none of which
|
|
* has a record to read yet. The record has to be created once and read by all
|
|
* of them, because a request that cannot resolve the identity answers 401 to
|
|
* a visitor who has just signed in.
|
|
*/
|
|
it('creates the record once for a burst of first requests', async () => {
|
|
let created = null;
|
|
mockCreateUser.mockImplementation(async (fields) => {
|
|
if (created) {
|
|
throw new Error('E11000 duplicate key');
|
|
}
|
|
created = { _id: 'user-1', ...fields };
|
|
return created;
|
|
});
|
|
|
|
const burst = await Promise.all([...Array(12)].map(() => reconcileUser(claims, 'token')));
|
|
|
|
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
for (const user of burst) {
|
|
expect(user).toBe(created);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The welcome card is for accounts that start here, so the flag is written at
|
|
* creation rather than defaulted on the schema — a default reaches every
|
|
* record that lacks the field, which is every account that already exists.
|
|
*/
|
|
it('marks a new record as never having seen the welcome card', async () => {
|
|
mockCreateUser.mockImplementation(async (fields) => ({ _id: 'user-1', ...fields }));
|
|
|
|
await reconcileUser(claims, 'token');
|
|
|
|
expect(mockCreateUser).toHaveBeenCalledWith(
|
|
expect.objectContaining({ toured: false }),
|
|
expect.anything(),
|
|
true,
|
|
true,
|
|
);
|
|
});
|
|
|
|
/**
|
|
* An identity has exactly one record and the unique index decides who writes
|
|
* it, so a caller that loses the insert reads what won rather than failing.
|
|
*/
|
|
it('reads the record when another writer created it first', async () => {
|
|
const winner = { _id: 'user-1', openidId: claims.sub };
|
|
mockCreateUser.mockRejectedValue(new Error('E11000 duplicate key'));
|
|
mockFindUser.mockResolvedValue(winner);
|
|
|
|
await expect(reconcileUser(claims, 'token')).resolves.toBe(winner);
|
|
expect(mockFindUser).toHaveBeenCalledWith({ openidId: claims.sub });
|
|
});
|
|
|
|
/** A create that failed for any other reason is still a failure. */
|
|
it('refuses when the record cannot be written or found', async () => {
|
|
mockCreateUser.mockRejectedValue(new Error('disk full'));
|
|
mockFindUser.mockResolvedValue(null);
|
|
|
|
await expect(reconcileUser(claims, 'token')).rejects.toThrow('disk full');
|
|
});
|
|
|
|
/** The in-flight reconcile is dropped when it settles, never kept as a cache. */
|
|
it('resolves the identity again after the burst has settled', async () => {
|
|
const existing = { _id: 'user-1', openidId: claims.sub, provider: 'openid', role: 'USER' };
|
|
mockCreateUser.mockResolvedValue(existing);
|
|
await reconcileUser(claims, 'token');
|
|
|
|
mockFindOpenIDUser.mockResolvedValue({ user: existing, error: null, migration: false });
|
|
await expect(reconcileUser(claims, 'token')).resolves.toBe(existing);
|
|
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|