| ... | ... | @@ -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 | 1129 | function makePendingTask(id: string): PendingNewTask { |
| 995 | 1130 | const creation = { |
| 996 | 1131 | projectId: ProjectId.make("project-1"), |