chore: complete clean Agent Desk source snapshot

This commit is contained in:
2026-10-16 12:03:16 +00:00
parent 756ae33d05
commit e57610a27c
123 changed files with 37513 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
import assert from "node:assert/strict"
import test from "node:test"
import { readFile } from "node:fs/promises"
import {
buildSelectConversationPatch,
findConversationById,
isOpenImConversation,
pickConversationToResume,
resolveConversationTitle,
sortConversationsByRecency,
totalUnreadConversations,
upsertConversation,
CLOSED_IM_CONVERSATION_STATUS,
} from "./support-conversation-list.ts"
const AISERVING = 1
const PENDING = 2
const ACTIVE = 3
const CLOSED = 4
function makeConversation(overrides = {}) {
return {
id: 1,
channelId: 1,
customerName: "Alice",
status: ACTIVE,
serviceMode: 3,
priority: 0,
currentAssigneeId: 0,
lastMessageId: 0,
customerUnreadCount: 0,
agentUnreadCount: 0,
customerLastReadMessageId: 0,
agentLastReadMessageId: 0,
closedBy: 0,
...overrides,
}
}
test("the mirrored closed status still matches the generated backend enum", async () => {
const source = await readFile(
new URL("./generated/enums.ts", import.meta.url),
"utf8"
)
const block = source.match(/export enum IMConversationStatus \{[^}]*\}/)
assert.ok(block, "expected an IMConversationStatus enum in the generated file")
const values = Object.fromEntries(
[...block[0].matchAll(/(\w+)\s*=\s*(\d+)/g)].map((match) => [
match[1],
Number(match[2]),
])
)
assert.equal(values.Closed, CLOSED_IM_CONVERSATION_STATUS)
// The server counts a conversation open while it is any of these three.
assert.deepEqual(
{ serving: values.AIServing, pending: values.Pending, active: values.Active },
{ serving: AISERVING, pending: PENDING, active: ACTIVE }
)
})
test("anything not closed is still an open request", () => {
assert.equal(isOpenImConversation(makeConversation({ status: AISERVING })), true)
assert.equal(isOpenImConversation(makeConversation({ status: PENDING })), true)
assert.equal(isOpenImConversation(makeConversation({ status: ACTIVE })), true)
assert.equal(isOpenImConversation(makeConversation({ status: CLOSED })), false)
// A status the server adds later defaults to open rather than being mistaken
// for a finished request.
assert.equal(isOpenImConversation(makeConversation({ status: 99 })), true)
})
test("a closed request is not open, and neither is nothing", () => {
assert.equal(isOpenImConversation(null), false)
assert.equal(isOpenImConversation(undefined), false)
})
test("sorting puts the most recently active request first", () => {
const list = [
makeConversation({ id: 1, lastActiveAt: "2026-10-01 10:00:00" }),
makeConversation({ id: 2, lastActiveAt: "2026-10-03 10:00:00" }),
makeConversation({ id: 3, lastActiveAt: "2026-10-02 10:00:00" }),
]
assert.deepEqual(
sortConversationsByRecency(list).map((item) => item.id),
[2, 3, 1]
)
})
// The server writes time.DateTime, which has no timezone marker. Parsing it as
// a bare local timestamp keeps it consistent with formatDateTime elsewhere.
test("server timestamps without a zone still order correctly", () => {
const list = [
makeConversation({ id: 1, lastActiveAt: "2026-10-01 09:00:00" }),
makeConversation({ id: 2, lastActiveAt: "2026-10-01 11:00:00" }),
]
assert.deepEqual(
sortConversationsByRecency(list).map((item) => item.id),
[2, 1]
)
})
test("a brand new request with no timestamps sorts ahead by id", () => {
const list = [
makeConversation({ id: 9 }),
makeConversation({ id: 12 }),
makeConversation({ id: 3, lastActiveAt: "2026-10-01 09:00:00" }),
]
assert.deepEqual(
sortConversationsByRecency(list).map((item) => item.id),
[12, 9, 3]
)
})
test("an unparsable timestamp does not become NaN and scramble the order", () => {
const list = [
makeConversation({ id: 1, lastActiveAt: "not-a-date" }),
makeConversation({ id: 2, lastActiveAt: "2026-10-01 09:00:00" }),
]
assert.deepEqual(
sortConversationsByRecency(list).map((item) => item.id),
[2, 1]
)
})
test("lastMessageAt stands in when lastActiveAt is missing", () => {
const list = [
makeConversation({ id: 1, lastMessageAt: "2026-10-01 08:00:00" }),
makeConversation({ id: 2, lastMessageAt: "2026-10-01 10:00:00" }),
]
assert.deepEqual(
sortConversationsByRecency(list).map((item) => item.id),
[2, 1]
)
})
test("upsert merges an existing request instead of duplicating it", () => {
const list = [makeConversation({ id: 1, subject: "old" })]
const next = upsertConversation(list, makeConversation({ id: 1, subject: "new" }))
assert.equal(next.length, 1)
assert.equal(next[0].subject, "new")
})
test("upsert keeps fields the patch does not carry", () => {
const list = [makeConversation({ id: 1, subject: "kept", customerUnreadCount: 4 })]
const next = upsertConversation(list, { id: 1, status: CLOSED })
assert.equal(next[0].subject, "kept")
assert.equal(next[0].customerUnreadCount, 4)
assert.equal(next[0].status, CLOSED)
})
test("upserting a new request puts it at the top", () => {
const list = [makeConversation({ id: 1, lastActiveAt: "2026-10-01 09:00:00" })]
const next = upsertConversation(list, makeConversation({ id: 2 }))
assert.deepEqual(next.map((item) => item.id), [2, 1])
})
test("upsert does not mutate the input", () => {
const list = [makeConversation({ id: 1 })]
upsertConversation(list, makeConversation({ id: 2 }))
assert.equal(list.length, 1)
})
test("findConversationById returns null rather than throwing", () => {
const list = [makeConversation({ id: 7 })]
assert.equal(findConversationById(list, 7).id, 7)
assert.equal(findConversationById(list, 8), null)
assert.equal(findConversationById(list, 0), null)
})
test("resume picks the newest open request, skipping closed ones", () => {
const list = [
makeConversation({ id: 1, status: CLOSED, lastActiveAt: "2026-10-05 10:00:00" }),
makeConversation({ id: 2, status: ACTIVE, lastActiveAt: "2026-10-01 10:00:00" }),
]
assert.equal(pickConversationToResume(list).id, 2)
})
test("resume returns null when everything is closed so a new one gets started", () => {
const list = [
makeConversation({ id: 1, status: CLOSED }),
makeConversation({ id: 2, status: CLOSED }),
]
assert.equal(pickConversationToResume(list), null)
})
test("resume returns null for a visitor who never wrote to us", () => {
assert.equal(pickConversationToResume([]), null)
})
test("title prefers the visitor's subject", () => {
const conversation = makeConversation({
subject: "Проблема с оплатой",
lastMessageSummary: "Здравствуйте",
})
assert.equal(resolveConversationTitle(conversation, "Запрос"), "Проблема с оплатой")
})
test("title falls back to the last message, then to the localized label", () => {
assert.equal(
resolveConversationTitle(makeConversation({ lastMessageSummary: "Здравствуйте" }), "Запрос"),
"Здравствуйте"
)
assert.equal(resolveConversationTitle(makeConversation(), "Запрос"), "Запрос")
assert.equal(resolveConversationTitle(null, "Запрос"), "Запрос")
})
test("a whitespace-only subject is treated as absent", () => {
assert.equal(
resolveConversationTitle(makeConversation({ subject: " ", lastMessageSummary: "Привет" }), "Запрос"),
"Привет"
)
})
test("unread totals across requests include the ones not being viewed", () => {
const list = [
makeConversation({ id: 1, customerUnreadCount: 2 }),
makeConversation({ id: 2, customerUnreadCount: 3 }),
makeConversation({ id: 3, customerUnreadCount: 0 }),
]
assert.equal(totalUnreadConversations(list), 5)
})
test("a missing unread count counts as zero", () => {
assert.equal(
totalUnreadConversations([
makeConversation({ id: 1 }),
{ ...makeConversation({ id: 2 }), customerUnreadCount: undefined },
]),
0
)
})
test("switching requests re-derives the conversation along with the active id", () => {
const a = makeConversation({ id: 1 })
const b = makeConversation({ id: 2 })
const patch = buildSelectConversationPatch(
{ conversations: [a, b], activeConversationId: 1 },
2
)
assert.equal(patch.activeConversationId, 2)
assert.equal(patch.conversation.id, 2)
assert.deepEqual(patch.messages, [])
assert.equal(patch.messagesHasMore, false)
})
test("switching to an unknown or already active request is a no-op", () => {
const a = makeConversation({ id: 1 })
const state = { conversations: [a], activeConversationId: 1 }
assert.equal(buildSelectConversationPatch(state, 99), null)
assert.equal(buildSelectConversationPatch(state, 1), null)
})