openContextArtifact.test.tsx5.0 KBView on GitHub import { act, renderHook } from '@testing-library/react';
import { useCedarStore } from '@/modules/cedar-os/src/store/CedarStore';
import { useOpenContextArtifact } from '@/modules/cedar-os/src/cedar-os-components/chatComponents/useOpenContextArtifact';
import { api } from '@/modules/trpc/trpc';
const mockNavigate = jest.fn();
// The route the hook thinks it is on — a standalone doc opens in place on the agent home
// and full-screen in /brain everywhere else, so each test sets this to the surface it means.
let mockPathname = '/mail/inbox';
jest.mock('react-router', () => ({
useNavigate: () => mockNavigate,
useLocation: () => ({ pathname: mockPathname }),
}));
const getDoc = api.documents.getDoc.query as jest.Mock;
beforeEach(() => {
mockNavigate.mockReset();
getDoc.mockReset();
mockPathname = '/mail/inbox';
useCedarStore.setState((state) => ({
...state,
threadMap: { main: { id: 'main', lastLoaded: new Date().toISOString(), messages: [] } },
mainThreadId: 'main',
activeThreadId: 'main',
activeConversationId: null,
conversationSection: null,
conversationOpenFile: null,
}));
});
describe('useOpenContextArtifact', () => {
it('opens a conversation-scoped file in the conversation Files tab, in place', async () => {
getDoc.mockResolvedValue({ path: 'conversation/conv_9/notes/deck' });
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_1');
});
const s = useCedarStore.getState();
expect(s.activeConversationId).toBe('conv_9');
expect(s.conversationSection).toBe('files');
// The open file is set AFTER the section (setting the section clears the open file).
expect(s.conversationOpenFile).toBe('doc_1');
expect(mockNavigate).not.toHaveBeenCalled();
});
it("opens a standalone file full-screen in the Brain's Knowledge explorer", async () => {
getDoc.mockResolvedValue({ path: 'user/scratch/idea' });
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_2');
});
expect(mockNavigate).toHaveBeenCalledWith('/brain/knowledge?documentId=doc_2');
});
it('opens a standalone file in place as the display artifact on the agent home', async () => {
getDoc.mockResolvedValue({ path: 'user/scratch/idea' });
mockPathname = '/home';
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_2');
});
// Navigating away here would swap out the surface and take the transcript with it.
expect(mockNavigate).not.toHaveBeenCalled();
expect(useCedarStore.getState().threadMap.main?.selectedArtifact).toEqual({
kind: 'file',
id: 'doc_2',
});
});
it('falls back to full-screen when the document cannot be resolved', async () => {
getDoc.mockRejectedValue(new Error('nope'));
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_3');
});
expect(mockNavigate).toHaveBeenCalledWith('/brain/knowledge?documentId=doc_3');
});
it('deep-links a conversation-file when switching to another chat thread', async () => {
getDoc.mockResolvedValue({ path: 'conversation/conv_9/notes/deck' });
useCedarStore.setState((state) => ({
...state,
threadMap: {
...state.threadMap,
other: { id: 'other', lastLoaded: new Date().toISOString(), messages: [] },
},
}));
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_1', 'other');
});
expect(useCedarStore.getState().mainThreadId).toBe('other');
expect(mockNavigate).toHaveBeenCalledWith('/home?conversationId=conv_9/files/doc_1');
});
it('opens a conversation in place (no navigation) when already in the chat', async () => {
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('conversation', 'conv_5');
});
expect(useCedarStore.getState().activeConversationId).toBe('conv_5');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('going from a file back to its conversation closes the file, swapping the selection', async () => {
getDoc.mockResolvedValue({ path: 'conversation/conv_9/notes/deck' });
const { result } = renderHook(() => useOpenContextArtifact());
await act(async () => {
await result.current('file', 'doc_1');
});
expect(useCedarStore.getState().conversationOpenFile).toBe('doc_1');
// Back to the parent conversation — the file must close so the CONVERSATION becomes the
// selected context (otherwise the file stays lit and the selection never swaps).
await act(async () => {
await result.current('conversation', 'conv_9');
});
const s = useCedarStore.getState();
expect(s.conversationOpenFile).toBeNull();
expect(s.activeConversationId).toBe('conv_9');
});
});