On opening the floating AI-chat window from the header on a document page, auto-open the LAST chat bound to that document. Binding reuses the existing ai_chats.page_id (no migration): the bound chat is the requesting user's most-recent non-deleted chat created on that page, so a new chat on the page becomes the bound one for free. Resolution happens only on a genuine closed -> open transition; the provenance badge deep-link is untouched. Server: AiChatRepo.findLatestByPage + POST /ai-chat/bound-chat (BoundChatDto), both read-only and owner/workspace-scoped. Client: getBoundChat service + useOpenAiChatForCurrentPage hook wired into the app-header entry point (fail-soft to a fresh chat; draft/role cleared only on a real switch). Tests: repo scoping/ordering, controller wiring, and hook behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
1.7 KiB
TypeScript
45 lines
1.7 KiB
TypeScript
import { AiChatController } from './ai-chat.controller';
|
|
import type { User, Workspace } from '@docmost/db/types/entity.types';
|
|
|
|
/**
|
|
* Wiring spec for the #191 `POST /ai-chat/bound-chat` endpoint. It must forward
|
|
* the requesting user + workspace + pageId to findLatestByPage and return the
|
|
* matched chat's id, or `{ chatId: null }` when there is none. The repo already
|
|
* scopes to the caller's OWN chats, so a foreign pageId simply yields no match
|
|
* (null) — no extra page-access check is needed. Exercised with hand-rolled
|
|
* mocks, no Nest graph and no DB.
|
|
*/
|
|
describe('AiChatController.boundChat', () => {
|
|
const user = { id: 'u1' } as User;
|
|
const workspace = { id: 'ws1' } as Workspace;
|
|
|
|
function makeController(chat: unknown) {
|
|
const aiChatRepo = {
|
|
findLatestByPage: jest.fn().mockResolvedValue(chat),
|
|
};
|
|
const controller = new AiChatController(
|
|
{} as never,
|
|
aiChatRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
return { controller, aiChatRepo };
|
|
}
|
|
|
|
it('returns the owned chat id and scopes the lookup to user + workspace + page', async () => {
|
|
const { controller, aiChatRepo } = makeController({
|
|
id: 'c1',
|
|
creatorId: 'u1',
|
|
});
|
|
const res = await controller.boundChat({ pageId: 'p1' }, user, workspace);
|
|
expect(aiChatRepo.findLatestByPage).toHaveBeenCalledWith('u1', 'ws1', 'p1');
|
|
expect(res).toEqual({ chatId: 'c1' });
|
|
});
|
|
|
|
it('returns { chatId: null } for a page with no owned chat (incl. foreign pageId)', async () => {
|
|
const { controller } = makeController(undefined);
|
|
const res = await controller.boundChat({ pageId: 'foreign' }, user, workspace);
|
|
expect(res).toEqual({ chatId: null });
|
|
});
|
|
});
|