displayArtifact.test.tsx3.0 KBView on GitHub
/**
 * Phase 1 (design: chat-display-artifact) — the display artifact is per-thread.
 * `setSelectedArtifact` writes the ACTIVE thread's slot; `getDisplayArtifact` reads it and is
 * null when nothing is open; switching threads switches the resolved window.
 */
import { DEFAULT_THREAD_ID } from '@/store/messages/MessageTypes';
import { useCedarStore } from '@/store/CedarStore';

const T1 = DEFAULT_THREAD_ID;
const T2 = 'thread-2';

function seedTwoThreads() {
  useCedarStore.setState((state) => ({
    ...state,
    threadMap: {
      [T1]: { id: T1, messages: [], selectedArtifact: null },
      [T2]: { id: T2, messages: [], selectedArtifact: null },
    },
    mainThreadId: T1,
    activeThreadId: T1,
    messages: [],
  }));
}

describe('per-thread display artifact', () => {
  beforeEach(seedTwoThreads);

  it('setSelectedArtifact writes only the active thread', () => {
    useCedarStore.getState().setSelectedArtifact({ kind: 'conversation', id: 'conv_a' });

    const { threadMap } = useCedarStore.getState();
    expect(threadMap[T1].selectedArtifact).toEqual({ kind: 'conversation', id: 'conv_a' });
    expect(threadMap[T2].selectedArtifact).toBeNull(); // the other thread is untouched
  });

  it('getDisplayArtifact resolves the active thread, and is null when nothing is open', () => {
    const store = useCedarStore.getState();

    // Nothing open on the active thread → null. There is no default artifact.
    expect(store.getDisplayArtifact()).toBeNull();

    store.setSelectedArtifact({ kind: 'file', id: 'doc_1' });
    expect(useCedarStore.getState().getDisplayArtifact()).toEqual({ kind: 'file', id: 'doc_1' });
  });

  it('switching the active thread switches the resolved display artifact', () => {
    const store = useCedarStore.getState();
    store.setSelectedArtifact({ kind: 'conversation', id: 'conv_a' }); // on T1

    // Point the active thread at T2 (which has nothing open) → null.
    useCedarStore.setState((s) => ({ ...s, mainThreadId: T2, activeThreadId: T2 }));
    expect(useCedarStore.getState().getDisplayArtifact()).toBeNull();

    // Back to T1 → its conversation is restored (per-thread persistence).
    useCedarStore.setState((s) => ({ ...s, mainThreadId: T1, activeThreadId: T1 }));
    expect(useCedarStore.getState().getDisplayArtifact()).toEqual({
      kind: 'conversation',
      id: 'conv_a',
    });
  });

  // With no active thread there is nowhere to write the pointer, and the write used to be
  // dropped — which is exactly the state a deep-link opened in a fresh tab arrives in (nothing
  // has seeded a chat yet), so the link opened nothing. Opening now seeds the chat it needs.
  it('seeds a chat thread when there is no active one, instead of dropping the open', () => {
    useCedarStore.setState((s) => ({ ...s, threadMap: {}, mainThreadId: '', activeThreadId: '' }));

    useCedarStore.getState().setSelectedArtifact({ kind: 'file', id: 'doc_1' });

    expect(useCedarStore.getState().getDisplayArtifact()).toEqual({ kind: 'file', id: 'doc_1' });
  });
});