backendHistoryWindow.test.ts3.0 KBView on GitHub
import { useCedarStore } from '@/modules/store';
import type { Message } from '@/store/messages/MessageTypes';

/**
 * What the agent is told about the conversation so far.
 *
 * The browser holds only the newest page of a long thread and the send trims that to the last
 * 50 — so on a long chat the agent used to be answering with no idea what came before, and
 * nothing on the wire said so. The send now marks where its window starts, which is the server's
 * cue to read the rest of the thread out of the DB.
 */

const THREAD = 'chat_long';

function msg(i: number, minute: number): Message {
  return {
    id: `m${i}`,
    role: i % 2 === 0 ? 'user' : 'assistant',
    type: 'text',
    content: `turn ${i}`,
    createdAt: `2026-09-01T10:${String(minute).padStart(2, '0')}:00.000Z`,
  } as Message;
}

let sentBody: Record<string, unknown>;

function seed(messages: Message[], hasMoreMessages: boolean) {
  useCedarStore.setState((state) => ({
    ...state,
    threadMap: {
      [THREAD]: {
        id: THREAD,
        lastLoaded: new Date().toISOString(),
        messages,
        hasMoreMessages,
      },
    },
    mainThreadId: THREAD,
    activeThreadId: THREAD,
    providerConfig: { provider: 'mastra', baseURL: 'https://example.test' },
  }));
}

beforeEach(() => {
  sentBody = {};
  global.fetch = jest.fn(async (_url: unknown, init: { body?: string } = {}) => {
    sentBody = JSON.parse(init.body ?? '{}').json ?? {};
    return {
      ok: true,
      status: 200,
      body: { getReader: () => ({ read: async () => ({ done: true, value: undefined }) }) },
    };
  }) as unknown as typeof fetch;
});

describe('the history window sent to the agent', () => {
  it('marks where the window starts when the send trimmed older turns off it', async () => {
    seed(
      Array.from({ length: 60 }, (_, i) => msg(i, i)),
      false,
    );

    await useCedarStore.getState().sendMessage({ overridePrompt: 'and then?' });

    expect((sentBody.messages as unknown[]).length).toBe(50);
    // 60 candidates trimmed to 50 — the 10 oldest are missing, so the server is told to fetch
    // everything before the 11th (index 10).
    expect(sentBody.historyOldestCreatedAt).toBe(msg(10, 10).createdAt);
  });

  it('marks it when the thread has pages the browser never loaded', async () => {
    seed([msg(0, 0), msg(1, 1)], true);

    await useCedarStore.getState().sendMessage({ overridePrompt: 'and then?' });

    // Only two messages in memory, but the server said there is more behind them.
    expect(sentBody.historyOldestCreatedAt).toBe(msg(0, 0).createdAt);
  });

  it('leaves it off a short thread that is entirely in memory', async () => {
    seed([msg(0, 0), msg(1, 1)], false);

    await useCedarStore.getState().sendMessage({ overridePrompt: 'and then?' });

    // Nothing was left out, so a backfill query would be guaranteed to return nothing.
    expect(sentBody.historyOldestCreatedAt).toBeUndefined();
    expect((sentBody.messages as unknown[]).length).toBe(2);
  });
});