import { describe, expect, it } from "vite-plus/test"; import { generateSpreadPinOrderKeys, pinOrderKeyBetween, planPinnedMove, planPinnedReorder, resolveSettledThreadTimestamp, sortActiveThreadsByOrderKey, sortPinnedThreadsByOrderKey, sortThreads, type ThreadSortInput, } from "./threadSort.ts"; type TestThread = { readonly id: string } & ThreadSortInput; function makeThread(overrides: Partial = {}): TestThread { return { id: "thread-1", createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:00:00.000Z", messages: [], latestUserMessageAt: null, ...overrides, }; } describe("resolveSettledThreadTimestamp", () => { it("prefers the persisted settlement stamp over later activity", () => { expect( resolveSettledThreadTimestamp({ settledAt: "2026-03-09T10:00:00.000Z", latestUserMessageAt: "2026-03-09T11:00:00.000Z", latestTurn: null, updatedAt: "2026-03-09T12:00:00.000Z", }), ).toBe("2026-03-09T10:00:00.000Z"); }); it("falls back to the latest activity when the stamp is missing or malformed", () => { expect( resolveSettledThreadTimestamp({ settledAt: "invalid", latestUserMessageAt: "2026-03-09T11:00:00.000Z", latestTurn: null, updatedAt: "2026-03-09T12:00:00.000Z", }), ).toBe("2026-03-09T11:00:00.000Z"); expect( resolveSettledThreadTimestamp({ settledAt: null, latestUserMessageAt: null, latestTurn: null, updatedAt: "2026-03-09T12:00:00.000Z", }), ).toBe("2026-03-09T12:00:00.000Z"); }); }); describe("sortThreads", () => { it("falls back to updatedAt and createdAt when latestUserMessageAt is invalid and there are no messages", () => { const sorted = sortThreads( [ makeThread({ id: "thread-1", latestUserMessageAt: "not-a-date", createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:05:00.000Z", }), makeThread({ id: "thread-2", latestUserMessageAt: "still-not-a-date", createdAt: "invalid-created-at", updatedAt: "invalid-updated-at", }), makeThread({ id: "thread-3", latestUserMessageAt: "invalid-latest-user-message-at", createdAt: "2026-03-09T10:06:00.000Z", updatedAt: "invalid-updated-at", }), ], "updated_at", ); expect(sorted.map((thread) => thread.id)).toEqual(["thread-3", "thread-1", "thread-2"]); }); it("falls back to the latest valid user message when latestUserMessageAt is invalid", () => { const sorted = sortThreads( [ makeThread({ id: "thread-1", latestUserMessageAt: "invalid-latest-user-message-at", updatedAt: "2026-03-09T10:00:00.000Z", messages: [ { role: "user", createdAt: "2026-03-09T10:05:00.000Z" }, { role: "assistant", createdAt: "2026-03-09T10:30:00.000Z" }, { role: "user", createdAt: "2026-03-09T10:20:00.000Z" }, ], }), makeThread({ id: "thread-2", createdAt: "2026-03-09T10:15:00.000Z", updatedAt: "2026-03-09T10:15:00.000Z", }), ], "updated_at", ); expect(sorted.map((thread) => thread.id)).toEqual(["thread-1", "thread-2"]); }); }); describe("planPinnedReorder with hidden rows", () => { it("keeps hidden slots available when inserting between visible neighbors", () => { const midpoint = pinOrderKeyBetween("f", "t")!; const keysById = new Map([ ["a", "f"], ["b", "t"], ["moved", "z"], ["snoozed", midpoint], ]); const assignments = planPinnedReorder({ orderedIds: ["a", "moved", "b"], keysById, movedId: "moved", }); expect(assignments).toHaveLength(1); const key = assignments[0]!.orderKey; expect(key > "f" && key < "t").toBe(true); expect(key).not.toBe(midpoint); expect(assignments[0]!.id).toBe("moved"); }); it("materializes keyless rows without overwriting hidden slots", () => { const reserved = generateSpreadPinOrderKeys(6); const keysById = new Map([ ["a", null], ["b", null], ["c", null], ...reserved.map((key, i) => [`hidden-${i}`, key] as const), ]); const assignments = planPinnedReorder({ orderedIds: ["c", "a", "b"], keysById, movedId: "c" }); expect(assignments.map(({ id }) => id)).toEqual(["c", "a", "b"]); const keys = assignments.map(({ orderKey }) => orderKey); expect(keys).toEqual([...keys].sort()); expect(new Set(keys).size).toBe(3); expect(keys.every((key) => !reserved.includes(key))).toBe(true); }); }); describe("planPinnedMove", () => { it("moves a thread up with a single key write", () => { const assignments = planPinnedMove({ orderedIds: ["a", "b", "c"], keysById: new Map([ ["a", "f"], ["b", "m"], ["c", "t"], ]), movedId: "c", direction: "up", }); expect(assignments).toHaveLength(1); expect(assignments![0]!.id).toBe("c"); expect(assignments![0]!.orderKey > "f" && assignments![0]!.orderKey < "m").toBe(true); }); it("returns null when the move falls off the end of the list", () => { const input = { orderedIds: ["a", "b"], keysById: new Map([ ["a", "f"], ["b", "m"], ]), }; expect(planPinnedMove({ ...input, movedId: "a", direction: "up" })).toBeNull(); expect(planPinnedMove({ ...input, movedId: "b", direction: "down" })).toBeNull(); }); it("materializes keys for the whole section when a neighbor is keyless", () => { const assignments = planPinnedMove({ orderedIds: ["a", "b", "c"], keysById: new Map([ ["a", null], ["b", "m"], ["c", null], ]), movedId: "b", direction: "up", }); expect(assignments).not.toBeNull(); const keys = assignments!.map((entry) => entry.orderKey); expect([...keys].sort()).toEqual(keys); }); }); describe("sortPinnedThreadsByOrderKey", () => { it("breaks equal keys by id THEN environment so merged lists are stable everywhere", () => { const sorted = sortPinnedThreadsByOrderKey([ { id: "thread-1", createdAt: "2026-03-09T10:00:00.000Z", pinOrderKey: "m", environmentId: "env-b", }, { id: "thread-1", createdAt: "2026-03-09T11:00:00.000Z", pinOrderKey: "m", environmentId: "env-a", }, ]); expect(sorted.map((thread) => thread.environmentId)).toEqual(["env-a", "env-b"]); }); }); describe("generateSpreadPinOrderKeys", () => { it.each([0, 1, 650, 675, 676, 1_001, 2_000])( "leaves unique, insertable keys for %i threads", (count) => { const keys = generateSpreadPinOrderKeys(count); expect(keys).toHaveLength(count); expect(new Set(keys).size).toBe(count); expect([...keys].sort()).toEqual(keys); for (let index = 0; index < keys.length; index += 1) { const before = keys[index - 1] ?? null; const after = keys[index]!; expect(after).toMatch(/^[a-z]*[b-z]$/); const between = pinOrderKeyBetween(before, after); expect(between).not.toBeNull(); expect(between! < after).toBe(true); if (before !== null) expect(between! > before).toBe(true); } }, ); }); describe("sortActiveThreadsByOrderKey", () => { it("keeps new and reopened threads ahead of the saved order", () => { const sorted = sortActiveThreadsByOrderKey([ { id: "arranged-first", createdAt: "2026-03-09T09:00:00.000Z", activeOrderKey: "f", }, { id: "new", createdAt: "2026-03-09T11:00:00.000Z", activeOrderKey: null, }, { id: "arranged-last", createdAt: "2026-03-09T12:00:00.000Z", unsettledAt: "2026-03-09T13:00:00.000Z", activeOrderKey: "t", }, { id: "reopened", createdAt: "2026-03-01T09:00:00.000Z", unsettledAt: "2026-03-09T12:00:00.000Z", }, ]); expect(sorted.map((thread) => thread.id)).toEqual([ "reopened", "new", "arranged-first", "arranged-last", ]); }); it("breaks equal order keys and timestamps by thread then environment", () => { for (const activeOrderKey of [null, "m"]) { const threads = [ { id: "thread-b", environmentId: "env-a" }, { id: "thread-a", environmentId: "env-b" }, { id: "thread-a", environmentId: "env-a" }, ].map((thread) => ({ ...thread, createdAt: "2026-03-09T10:00:00.000Z", activeOrderKey, })); expect( sortActiveThreadsByOrderKey(threads).map( (thread) => `${thread.id}:${thread.environmentId}`, ), ).toEqual(["thread-a:env-a", "thread-a:env-b", "thread-b:env-a"]); } }); it("applies every move across a mixed keyless and keyed section", () => { const threads = Array.from({ length: 6 }, (_, index) => ({ id: String(index), createdAt: `2026-03-09T0${6 - index}:00:00.000Z`, activeOrderKey: index < 3 ? null : ["f", "m", "t"][index - 3]!, })); const ids = threads.map((thread) => thread.id); const keysById = new Map(threads.map((thread) => [thread.id, thread.activeOrderKey])); for (const movedId of ids) { for (let targetIndex = 0; targetIndex < ids.length; targetIndex += 1) { const desired = ids.filter((id) => id !== movedId); desired.splice(targetIndex, 0, movedId); const assignments = planPinnedReorder({ orderedIds: desired, keysById, movedId }); const nextKeys = new Map( assignments.map((assignment) => [assignment.id, assignment.orderKey]), ); const updated = threads.map((thread) => ({ ...thread, activeOrderKey: nextKeys.get(thread.id) ?? thread.activeOrderKey, })); expect(sortActiveThreadsByOrderKey(updated).map((thread) => thread.id)).toEqual(desired); } } }); it("moves a keyless thread into the arranged run with one write", () => { const assignments = planPinnedMove({ orderedIds: ["new", "reopened", "first", "last"], keysById: new Map([ ["new", null], ["reopened", null], ["first", "f"], ["last", "t"], ]), movedId: "reopened", direction: "down", }); expect(assignments).toHaveLength(1); expect(assignments![0]!.id).toBe("reopened"); expect(assignments![0]!.orderKey > "f").toBe(true); expect(assignments![0]!.orderKey < "t").toBe(true); }); it("materializes a large active list without changing the requested order", () => { const threads = Array.from({ length: 1_200 }, (_, index) => ({ id: String(index), createdAt: "2026-03-09T10:00:00.000Z", activeOrderKey: null as string | null, })); const orderedIds = threads.map((thread) => thread.id).toReversed(); const assignments = planPinnedReorder({ orderedIds, movedId: orderedIds[0]!, keysById: new Map(threads.map((thread) => [thread.id, thread.activeOrderKey])), }); const keys = new Map(assignments.map((assignment) => [assignment.id, assignment.orderKey])); const updated = threads.map((thread) => ({ ...thread, activeOrderKey: keys.get(thread.id) })); expect(sortActiveThreadsByOrderKey(updated).map((thread) => thread.id)).toEqual(orderedIds); }); });