Files
Jyotisha/frontend/tests/session-groups.test.ts
T
jesse-uxandGrok (agent loop) c6c69aa04b 拖动排序一次写完,新对话仍排在最上面(BUG-1449)。
Co-Authored-By: Grok (agent loop)
2026-10-27 00:03:59 +08:00

144 lines
6.1 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
beginSessionPageLoad,
groupSessionsByRecency,
mergeSessionPage,
recencyKeyFor,
sortSessions,
} from "../src/lib/session-groups.ts";
import { sessionSidebarSubtitle } from "../src/lib/session-sidebar-row.ts";
import { shanghaiDateTimeLabel } from "../src/lib/session-shanghai-clock.ts";
import type { ChatSession } from "../src/lib/home-types.ts";
function session(id: string, updatedAt: number, pinned = false) {
return { id, pinned, updatedAt };
}
test("sortSessions puts pinned ahead of a newer unpinned row", () => {
const olderPinned = session("pin", 100, true);
const newer = session("new", 500);
assert.deepEqual(sortSessions([newer, olderPinned]).map((item) => item.id), ["pin", "new"]);
});
test("sortSessions orders same pin state by updatedAt descending", () => {
const early = session("early", 100);
const late = session("late", 400);
assert.deepEqual(sortSessions([early, late]).map((item) => item.id), ["late", "early"]);
});
test("sortSessions keeps the original order when pin and time are equal", () => {
const first = session("a", 200);
const second = session("b", 200);
assert.deepEqual(sortSessions([first, second]).map((item) => item.id), ["a", "b"]);
});
test("sortSessions keeps chats the user has not ordered above dragged rows", () => {
const dragged = { id: "dragged", pinned: false, updatedAt: 100, sidebarOrder: 10 };
const olderOpen = { id: "older", pinned: false, updatedAt: 200, sidebarOrder: null };
const newest = { id: "newest", pinned: false, updatedAt: 900 };
assert.deepEqual(
sortSessions([dragged, olderOpen, newest]).map((item) => item.id),
["newest", "older", "dragged"],
);
});
test("sortSessions still pins a dragged row above a newer open chat", () => {
const pinnedDragged = { id: "pin", pinned: true, updatedAt: 100, sidebarOrder: 10 };
const newest = { id: "newest", pinned: false, updatedAt: 900 };
assert.deepEqual(sortSessions([newest, pinnedDragged]).map((item) => item.id), ["pin", "newest"]);
});
test("sortSessions keeps a new pinned chat above pinned chats the user ordered", () => {
const ordered = { id: "ordered", pinned: true, updatedAt: 500, sidebarOrder: 10 };
const fresh = { id: "fresh", pinned: true, updatedAt: 100 };
assert.deepEqual(sortSessions([ordered, fresh]).map((item) => item.id), ["fresh", "ordered"]);
});
test("sortSessions does not drop archived rows", () => {
const archived = { ...session("arc", 50), archivedAt: "2026-09-01T00:00:00.000Z" };
const live = session("live", 80);
assert.deepEqual(sortSessions([archived, live]).map((item) => item.id), ["live", "arc"]);
});
test("groupSessionsByRecency uses local midnight for yesterday across 23:59 to 00:01", () => {
const now = new Date(2026, 8, 6, 0, 1, 0).getTime();
const justYesterday = new Date(2026, 8, 5, 23, 59, 0).getTime();
const justToday = new Date(2026, 8, 6, 0, 0, 0).getTime();
assert.equal(recencyKeyFor(justYesterday, now), "yesterday");
assert.equal(recencyKeyFor(justToday, now), "today");
});
test("groupSessionsByRecency splits day 7 and day 8, and day 30 and day 31", () => {
const now = new Date(2026, 8, 6, 12, 0, 0).getTime();
const todayStart = new Date(2026, 8, 6).getTime();
const dayMs = 24 * 60 * 60 * 1000;
const day7 = todayStart - 6 * dayMs + 12 * 60 * 60 * 1000;
const day8 = todayStart - 7 * dayMs + 12 * 60 * 60 * 1000;
const day30 = todayStart - 29 * dayMs + 12 * 60 * 60 * 1000;
const day31 = todayStart - 30 * dayMs + 12 * 60 * 60 * 1000;
assert.equal(recencyKeyFor(day7, now), "week");
assert.equal(recencyKeyFor(day8, now), "month");
assert.equal(recencyKeyFor(day30, now), "month");
assert.equal(recencyKeyFor(day31, now), "older");
const groups = groupSessionsByRecency([
session("today", now),
session("week", day7),
session("older", day31),
], now);
assert.deepEqual(groups.map((group) => group.label), ["今天", "最近 7 天", "更早"]);
});
test("mergeSessionPage keeps the local row and skips a duplicate id", () => {
const local = session("keep", 900);
const incoming = [session("keep", 100), session("next", 80)];
assert.deepEqual(mergeSessionPage([local], incoming).map((item) => item.id), ["keep", "next"]);
assert.equal(mergeSessionPage([local], incoming)[0]?.updatedAt, 900);
});
test("sidebar subtitle follows updatedAt so the visible clock matches sortSessions", () => {
const createdEarly = Date.parse("2026-09-07T05:42:00.000Z");
const activeLate = Date.parse("2026-09-16T10:01:00.000Z");
const mid = Date.parse("2026-09-17T14:53:00.000Z");
const newest = Date.parse("2026-09-18T00:01:00.000Z");
function row(id: string, createdAt: number, updatedAt: number): ChatSession {
return {
id,
title: id,
theme: "general",
modelId: "m",
messages: [{ role: "user", text: "问一句" }],
createdAt,
updatedAt,
sessionType: "consultation",
rectificationCaseId: null,
chartProfileId: null,
chartProfileName: null,
chartProfileRole: null,
pinned: false,
archivedAt: null,
messagesHydrated: true,
};
}
const earlyCreatedLateActive = row("early-created", createdEarly, activeLate);
const sessions = [
earlyCreatedLateActive,
row("mid", mid, mid),
row("newest", newest, newest),
];
assert.deepEqual(sortSessions(sessions).map((item) => item.id), ["newest", "mid", "early-created"]);
const clocks = sortSessions(sessions).map((item) => sessionSidebarSubtitle(item));
const expected = sortSessions(sessions).map((item) => shanghaiDateTimeLabel(new Date(item.updatedAt)));
assert.deepEqual(clocks, expected);
assert.equal(sessionSidebarSubtitle(earlyCreatedLateActive), shanghaiDateTimeLabel(new Date(activeLate)));
assert.notEqual(sessionSidebarSubtitle(earlyCreatedLateActive), shanghaiDateTimeLabel(new Date(createdEarly)));
});
test("beginSessionPageLoad only starts one in-flight request", () => {
const inFlight = { current: false };
assert.equal(beginSessionPageLoad(inFlight, "cursor"), true);
assert.equal(beginSessionPageLoad(inFlight, "cursor"), false);
assert.equal(beginSessionPageLoad({ current: false }, null), false);
});