agent-ecosystem/test/renderer/store/teamMessagesCache.test.ts
2026-05-22 00:01:53 +03:00

186 lines
6.1 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import {
areInboxMessageArraysEquivalent,
clearTeamMessageSelectorCaches,
clearTeamMessageSelectorCachesForTeam,
EMPTY_TEAM_MESSAGES_CACHE_ENTRY,
extractRetainedCanonicalOlderTail,
getCanonicalHeadSlice,
getTeamMessagesCacheEntry,
getTeamMessageSelectorCacheSnapshotForTeam,
pruneOptimisticMessages,
selectMemberMessagesForTeamMember,
selectTeamMessages,
type TeamMessagesCacheEntry,
type TeamMessagesCacheState,
upsertOptimisticTeamMessage,
} from '../../../src/renderer/store/team/teamMessagesCache';
import type { InboxMessage } from '../../../src/shared/types';
afterEach(() => {
clearTeamMessageSelectorCaches();
});
function createMessage(overrides: Partial<InboxMessage> & { messageId: string }): InboxMessage {
return {
from: 'lead',
to: 'alice',
text: overrides.messageId,
timestamp: '2026-03-12T10:00:00.000Z',
read: false,
...overrides,
};
}
function createEntry(overrides: Partial<TeamMessagesCacheEntry> = {}): TeamMessagesCacheEntry {
return {
...EMPTY_TEAM_MESSAGES_CACHE_ENTRY,
...overrides,
};
}
describe('teamMessagesCache', () => {
it('returns the immutable empty entry when a team has no cached messages', () => {
const state: TeamMessagesCacheState = { teamMessagesByName: {} };
expect(getTeamMessagesCacheEntry(state, 'missing-team')).toBe(EMPTY_TEAM_MESSAGES_CACHE_ENTRY);
});
it('upserts optimistic messages by durable id and keeps deterministic timestamp order', () => {
const first = upsertOptimisticTeamMessage(
createEntry(),
createMessage({
messageId: 'msg-new',
timestamp: '2026-03-12T10:00:03.000Z',
text: 'draft',
})
);
const second = upsertOptimisticTeamMessage(
first,
createMessage({
messageId: 'msg-old',
timestamp: '2026-03-12T10:00:01.000Z',
})
);
const replaced = upsertOptimisticTeamMessage(
second,
createMessage({
messageId: 'msg-new',
timestamp: '2026-03-12T10:00:03.000Z',
text: 'sent',
})
);
expect(replaced.optimisticMessages.map((message) => message.messageId)).toEqual([
'msg-old',
'msg-new',
]);
expect(replaced.optimisticMessages[1].text).toBe('sent');
});
it('compares semantic message arrays and prunes optimistic rows confirmed by canonical data', () => {
const canonical = [
createMessage({ messageId: 'msg-1', text: 'confirmed' }),
createMessage({ messageId: 'msg-2' }),
];
const equivalentCanonical = [
createMessage({ messageId: 'msg-1', text: 'confirmed' }),
createMessage({ messageId: 'msg-2' }),
];
const optimistic = [
createMessage({ messageId: 'msg-1', text: 'draft that arrived' }),
createMessage({ messageId: 'msg-local', text: 'still local' }),
];
expect(areInboxMessageArraysEquivalent(canonical, equivalentCanonical)).toBe(true);
expect(
areInboxMessageArraysEquivalent(canonical, [
createMessage({ messageId: 'msg-1', text: 'changed' }),
createMessage({ messageId: 'msg-2' }),
])
).toBe(false);
expect(pruneOptimisticMessages(optimistic, canonical).map((message) => message.messageId)).toEqual(
['msg-local']
);
});
it('retains already-loaded older tail only when the fresh head anchors into canonical data', () => {
const canonical = [
createMessage({ messageId: 'msg-4', timestamp: '2026-03-12T10:00:04.000Z' }),
createMessage({ messageId: 'msg-3', timestamp: '2026-03-12T10:00:03.000Z' }),
createMessage({ messageId: 'msg-2', timestamp: '2026-03-12T10:00:02.000Z' }),
createMessage({ messageId: 'msg-1', timestamp: '2026-03-12T10:00:01.000Z' }),
];
const freshHead = [
createMessage({ messageId: 'msg-5', timestamp: '2026-03-12T10:00:05.000Z' }),
createMessage({ messageId: 'msg-3', timestamp: '2026-03-12T10:00:03.000Z' }),
];
expect(getCanonicalHeadSlice(canonical, 2).map((message) => message.messageId)).toEqual([
'msg-4',
'msg-3',
]);
expect(
extractRetainedCanonicalOlderTail(canonical, freshHead)?.map((message) => message.messageId)
).toEqual(['msg-2', 'msg-1']);
expect(
extractRetainedCanonicalOlderTail(canonical, [createMessage({ messageId: 'disjoint' })])
).toBeNull();
});
it('memoizes merged and member-scoped selectors and clears team-scoped caches', () => {
const state: TeamMessagesCacheState = {
teamMessagesByName: {
'my-team': createEntry({
canonicalMessages: [
createMessage({
messageId: 'msg-1',
to: 'alice',
timestamp: '2026-03-12T10:00:01.000Z',
}),
createMessage({
messageId: 'msg-2',
to: 'bob',
timestamp: '2026-03-12T10:00:02.000Z',
}),
],
optimisticMessages: [
createMessage({
messageId: 'msg-3',
from: 'alice',
to: 'lead',
timestamp: '2026-03-12T10:00:03.000Z',
}),
],
}),
},
};
const firstTeamMessages = selectTeamMessages(state, 'my-team');
const secondTeamMessages = selectTeamMessages(state, 'my-team');
const firstAliceMessages = selectMemberMessagesForTeamMember(state, 'my-team', 'alice');
const secondAliceMessages = selectMemberMessagesForTeamMember(state, 'my-team', 'alice');
expect(firstTeamMessages).toBe(secondTeamMessages);
expect(firstAliceMessages).toBe(secondAliceMessages);
expect(firstTeamMessages.map((message) => message.messageId)).toEqual([
'msg-3',
'msg-2',
'msg-1',
]);
expect(firstAliceMessages.map((message) => message.messageId)).toEqual(['msg-3', 'msg-1']);
expect(getTeamMessageSelectorCacheSnapshotForTeam('my-team')).toEqual({
hasMergedMessagesSelector: true,
memberMessagesSelectorCount: 1,
});
clearTeamMessageSelectorCachesForTeam('my-team');
expect(getTeamMessageSelectorCacheSnapshotForTeam('my-team')).toEqual({
hasMergedMessagesSelector: false,
memberMessagesSelectorCount: 0,
});
});
});