aop-refresh-indicator.test.ts3.3 KBView on GitHub
/**
 * The "Agent updating fields…" indicator's state, in the conversations slice.
 *
 * An AOP change now returns before its agent refresh finishes (see
 * apps/server/docs/bug-reports/aop-change-60s-cloudfront-timeout.md), so the conversation's
 * fields keep changing for ~30s afterwards. `aopRefreshRunId` is what tells the UI that is
 * happening, and its presence IS the indicator.
 *
 * The subtle failure this pins: the same code path that sets the runId also invalidates and
 * refetches the conversation, which flows straight back through `setConversations`. If that
 * merge dropped the runId — as it would for any field not explicitly carried over — the
 * indicator would vanish a moment after appearing, and the user would be back to watching
 * fields change with no explanation.
 */
import { useCedarStore } from '@/modules/store';
import type { HydratedConversation } from '@/modules/crm/types';

const CONVERSATION_ID = 'dfd4c541-76a7-4ebb-8311-18715b3eea34';
const RUN_ID = '9f1c2b7e-0000-4000-8000-abcdefabcdef';

function makeHydrated(overrides: Record<string, unknown> = {}): HydratedConversation {
  return {
    conversation: {
      id: CONVERSATION_ID,
      name: 'pds GmbH (Channel Partner)',
      aopId: '70bb43c4-c1ae-4266-9429-04711c0dee8d',
      events: [],
      ...overrides,
    },
    userTasks: [],
  } as unknown as HydratedConversation;
}

function seed() {
  useCedarStore.getState().setConversations({ [CONVERSATION_ID]: makeHydrated() });
}

const runIdOf = (id: string) => useCedarStore.getState().conversations[id]?.aopRefreshRunId;

describe('conversations slice — AOP refresh run tracking', () => {
  beforeEach(() => {
    useCedarStore.setState({ conversations: {} });
  });

  it('starts clean', () => {
    seed();
    expect(runIdOf(CONVERSATION_ID)).toBeUndefined();
  });

  it('records the run id, and clears it with null', () => {
    seed();
    const { setAopRefreshRun } = useCedarStore.getState();

    setAopRefreshRun(CONVERSATION_ID, RUN_ID);
    expect(runIdOf(CONVERSATION_ID)).toBe(RUN_ID);

    setAopRefreshRun(CONVERSATION_ID, null);
    expect(runIdOf(CONVERSATION_ID)).toBeUndefined();
  });

  it('survives the refetch the AOP change itself triggers', () => {
    seed();
    useCedarStore.getState().setAopRefreshRun(CONVERSATION_ID, RUN_ID);

    // The invalidate + refetch that follows an AOP change lands here while the background
    // refresh is still running.
    useCedarStore.getState().setConversations({
      [CONVERSATION_ID]: makeHydrated({ statusOverview: 'refreshed by the agent' }),
    });

    expect(runIdOf(CONVERSATION_ID)).toBe(RUN_ID);
  });

  it('does not leak the indicator onto other conversations', () => {
    const other = '11111111-1111-1111-1111-111111111111';
    useCedarStore.getState().setConversations({
      [CONVERSATION_ID]: makeHydrated(),
      [other]: makeHydrated({ id: other }),
    });

    useCedarStore.getState().setAopRefreshRun(CONVERSATION_ID, RUN_ID);

    expect(runIdOf(CONVERSATION_ID)).toBe(RUN_ID);
    expect(runIdOf(other)).toBeUndefined();
  });

  it('ignores a run recorded against an unknown conversation', () => {
    seed();
    expect(() =>
      useCedarStore.getState().setAopRefreshRun('not-a-real-conversation', RUN_ID),
    ).not.toThrow();
    expect(runIdOf('not-a-real-conversation')).toBeUndefined();
  });
});