Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/core/src/chat/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ function contract(
expect(list.find((s) => s.id === b.id)?.messageCount).toBe(0);
});

it('breaks an updatedAt tie deterministically by id (guards the handler-list flake)', async () => {
// A frozen clock makes both conversations share `updatedAt`; without a
// stable secondary sort key the order would be non-deterministic.
let n = 0;
const store = await makeStore({
now: () => '2026-01-01T00:00:00.000Z',
idFactory: () => `id-${++n}`,
});
await store.create({ title: 'A' }); // id-1
await store.create({ title: 'B' }); // id-2

const list = await store.list();
// Equal updatedAt → tiebreak by id descending (id-2 before id-1).
expect(list.map((s) => s.id)).toEqual(['id-2', 'id-1']);
});

it('searches across titles and bodies with token-AND matching', async () => {
const store = await makeStore(deterministicDeps());
const a = await store.create({ title: 'Login flow' });
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/chat/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,14 @@ export abstract class BaseConversationStore implements ConversationStore {

async list(): Promise<ConversationSummary[]> {
const all = await this.loadAll();
return all.map(toSummary).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
// Newest-first by `updatedAt`, with `id` as a stable tiebreaker: two
// conversations touched in the same millisecond would otherwise compare
// equal and the sort order would be non-deterministic (the `handler-list`
// flake). Ids carry a monotonic counter (`conv-<ts>-<n>-<rand>`), so
// id-descending ≈ creation order, keeping the tiebreak newest-first too.
return all
.map(toSummary)
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.id.localeCompare(a.id));
}

async search(query: string, options?: SearchOptions): Promise<ConversationSearchResult[]> {
Expand Down
Loading