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();
});
});