| ... | @@ -991,6 +991,141 @@ describe("buildThreadListV2Items settled paging", () => { | ... | @@ -991,6 +991,141 @@ describe("buildThreadListV2Items settled paging", () => { |
| 991 | }); | 991 | }); |
| 992 | }); | 992 | }); |
| 993 | | 993 | |
| | 994 | describe("buildThreadListV2Items settled sort reuse", () => { |
| | 995 | const otherEnvironmentId = EnvironmentId.make("environment-2"); |
| | 996 | |
| | 997 | function makeSettled( |
| | 998 | id: string, |
| | 999 | settledAt: string, |
| | 1000 | overrides: Partial<EnvironmentThreadShell> = {}, |
| | 1001 | ): EnvironmentThreadShell { |
| | 1002 | return makeThread({ |
| | 1003 | id: ThreadId.make(id), |
| | 1004 | title: id, |
| | 1005 | settledOverride: "settled", |
| | 1006 | settledAt, |
| | 1007 | ...overrides, |
| | 1008 | }); |
| | 1009 | } |
| | 1010 | |
| | 1011 | function settledOrder( |
| | 1012 | threads: ReadonlyArray<EnvironmentThreadShell>, |
| | 1013 | scope: EnvironmentId | null = null, |
| | 1014 | ) { |
| | 1015 | return buildThreadListV2Items({ |
| | 1016 | threads, |
| | 1017 | environmentId: scope, |
| | 1018 | searchQuery: "", |
| | 1019 | now: NOW, |
| | 1020 | }) |
| | 1021 | .items.filter((item) => item.variant === "slim") |
| | 1022 | .map((item) => `${item.thread.environmentId}:${item.thread.id}`); |
| | 1023 | } |
| | 1024 | |
| | 1025 | function countDateParses(build: () => void): number { |
| | 1026 | const spy = vi.spyOn(Date, "parse"); |
| | 1027 | try { |
| | 1028 | build(); |
| | 1029 | return spy.mock.calls.length; |
| | 1030 | } finally { |
| | 1031 | spy.mockRestore(); |
| | 1032 | } |
| | 1033 | } |
| | 1034 | |
| | 1035 | const older = makeSettled("older", "2026-06-01T01:00:00.000Z"); |
| | 1036 | const newer = makeSettled("newer", "2026-06-01T03:00:00.000Z"); |
| | 1037 | const middle = makeSettled("middle", "2026-06-01T02:00:00.000Z"); |
| | 1038 | |
| | 1039 | it("orders settled rows newest first regardless of input order", () => { |
| | 1040 | expect(settledOrder([older, newer, middle])).toEqual([ |
| | 1041 | `${environmentId}:newer`, |
| | 1042 | `${environmentId}:middle`, |
| | 1043 | `${environmentId}:older`, |
| | 1044 | ]); |
| | 1045 | expect(settledOrder([middle, older, newer])).toEqual([ |
| | 1046 | `${environmentId}:newer`, |
| | 1047 | `${environmentId}:middle`, |
| | 1048 | `${environmentId}:older`, |
| | 1049 | ]); |
| | 1050 | }); |
| | 1051 | |
| | 1052 | it("re-sorts when a settled row is replaced, added, or removed", () => { |
| | 1053 | expect(settledOrder([older, newer])).toEqual([ |
| | 1054 | `${environmentId}:newer`, |
| | 1055 | `${environmentId}:older`, |
| | 1056 | ]); |
| | 1057 | const resettledOlder = makeSettled("older", "2026-06-01T04:00:00.000Z"); |
| | 1058 | expect(settledOrder([resettledOlder, newer])).toEqual([ |
| | 1059 | `${environmentId}:older`, |
| | 1060 | `${environmentId}:newer`, |
| | 1061 | ]); |
| | 1062 | expect(settledOrder([resettledOlder, newer, middle])).toEqual([ |
| | 1063 | `${environmentId}:older`, |
| | 1064 | `${environmentId}:newer`, |
| | 1065 | `${environmentId}:middle`, |
| | 1066 | ]); |
| | 1067 | expect(settledOrder([newer, middle])).toEqual([ |
| | 1068 | `${environmentId}:newer`, |
| | 1069 | `${environmentId}:middle`, |
| | 1070 | ]); |
| | 1071 | }); |
| | 1072 | |
| | 1073 | it("re-sorts when the same settled rows arrive in a different order", () => { |
| | 1074 | // Same id and timestamp on two environments: only the stable input order |
| | 1075 | // breaks the tie, so a reordered input must not reuse the previous output. |
| | 1076 | const first = makeSettled("twin", "2026-06-01T01:00:00.000Z"); |
| | 1077 | const second = makeSettled("twin", "2026-06-01T01:00:00.000Z", { |
| | 1078 | environmentId: otherEnvironmentId, |
| | 1079 | }); |
| | 1080 | expect(settledOrder([first, second])).toEqual([ |
| | 1081 | `${environmentId}:twin`, |
| | 1082 | `${otherEnvironmentId}:twin`, |
| | 1083 | ]); |
| | 1084 | expect(settledOrder([second, first])).toEqual([ |
| | 1085 | `${otherEnvironmentId}:twin`, |
| | 1086 | `${environmentId}:twin`, |
| | 1087 | ]); |
| | 1088 | }); |
| | 1089 | |
| | 1090 | it("skips the settled sort when only unsettled rows change", () => { |
| | 1091 | const settled = Array.from({ length: 50 }, (_, index) => |
| | 1092 | makeSettled(`settled-${index}`, `2026-06-01T00:${String(index).padStart(2, "0")}:00.000Z`), |
| | 1093 | ); |
| | 1094 | const streaming = makeThread({ id: ThreadId.make("streaming"), title: "Streaming" }); |
| | 1095 | const baseline = countDateParses(() => settledOrder([streaming, ...settled])); |
| | 1096 | const updated = { ...streaming, updatedAt: "2026-06-01T23:00:00.000Z" }; |
| | 1097 | let order: string[] = []; |
| | 1098 | const reused = countDateParses(() => { |
| | 1099 | order = settledOrder([updated, ...settled]); |
| | 1100 | }); |
| | 1101 | |
| | 1102 | expect(order).toEqual(settled.toReversed().map((thread) => `${environmentId}:${thread.id}`)); |
| | 1103 | // The settled sort parses every settled row at least once. |
| | 1104 | expect(baseline - reused).toBeGreaterThanOrEqual(settled.length); |
| | 1105 | }); |
| | 1106 | |
| | 1107 | it("recomputes when builds alternate between environments", () => { |
| | 1108 | const remoteOlder = makeSettled("remote-older", "2026-06-01T01:30:00.000Z", { |
| | 1109 | environmentId: otherEnvironmentId, |
| | 1110 | }); |
| | 1111 | const remoteNewer = makeSettled("remote-newer", "2026-06-01T05:00:00.000Z", { |
| | 1112 | environmentId: otherEnvironmentId, |
| | 1113 | }); |
| | 1114 | const threads = [older, newer, remoteOlder, remoteNewer]; |
| | 1115 | |
| | 1116 | for (let round = 0; round < 2; round += 1) { |
| | 1117 | expect(settledOrder(threads, environmentId)).toEqual([ |
| | 1118 | `${environmentId}:newer`, |
| | 1119 | `${environmentId}:older`, |
| | 1120 | ]); |
| | 1121 | expect(settledOrder(threads, otherEnvironmentId)).toEqual([ |
| | 1122 | `${otherEnvironmentId}:remote-newer`, |
| | 1123 | `${otherEnvironmentId}:remote-older`, |
| | 1124 | ]); |
| | 1125 | } |
| | 1126 | }); |
| | 1127 | }); |
| | 1128 | |
| 994 | function makePendingTask(id: string): PendingNewTask { | 1129 | function makePendingTask(id: string): PendingNewTask { |
| 995 | const creation = { | 1130 | const creation = { |
| 996 | projectId: ProjectId.make("project-1"), | 1131 | projectId: ProjectId.make("project-1"), |