Files
agent-desk-ng/web/lib/support-availability.test.mjs
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
2026-10-16 15:21:42 +00:00

135 lines
8.7 KiB
JavaScript

import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import test from "node:test"
import vm from "node:vm"
import ts from "typescript"
async function load(path, imports = {}, extras = {}) {
const source = await readFile(new URL(path, import.meta.url), "utf8")
const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX } })
const loaded = { exports: {} }
vm.runInNewContext(compiled.outputText, { exports: loaded.exports, module: loaded, require: name => { assert.ok(name in imports, `unexpected import ${name}`); return imports[name] }, Intl, Date, Set, AbortController, setTimeout, clearTimeout, process, ...extras })
return loaded.exports
}
const helpers = await load("./support-availability.ts")
const base = { teamId: 5, mode: "recurring", timezone: "Europe/Moscow", effectiveTimezone: "Europe/Moscow", deploymentTimezone: "UTC", weekly: [], exceptions: [] }
test("calendar accepts split, overnight and full-day midnight intervals", () => {
for (const weekly of [
[{ weekday: 1, start: "09:00", end: "12:00" }, { weekday: 1, start: "13:00", end: "18:00" }],
[{ weekday: 7, start: "22:00", end: "02:00" }],
[{ weekday: 1, start: "00:00", end: "24:00" }],
]) assert.equal(helpers.calendarValidationKey({ ...base, weekly }), null)
})
test("invalid IANA zones, offsets, equal intervals and invalid midnight starts fail", () => {
for (const timezone of ["Mars/Olympus", "+03:00", "Local"]) assert.equal(helpers.calendarValidationKey({ ...base, timezone }), "supportAvailability.invalidTimezone")
assert.equal(helpers.calendarValidationKey({ ...base, teamId: 0, timezone: "" }), "supportAvailability.invalidTimezone")
assert.equal(helpers.calendarValidationKey({ ...base, timezone: "" }), null)
for (const [start, end] of [["08:00", "08:00"], ["24:00", "08:00"], ["09:00", "24:01"]]) assert.equal(helpers.calendarValidationKey({ ...base, weekly: [{ weekday: 1, start, end }] }), "supportAvailability.invalidHours")
})
test("invalid, duplicate and leap exception dates are checked", () => {
for (const date of ["2026-02-29", "2026-04-31", "invalid"]) assert.equal(helpers.calendarValidationKey({ ...base, exceptions: [{ date, closed: true, periods: [] }] }), "supportAvailability.invalidExceptionDate")
const leap = { date: "2028-02-29", closed: true, periods: [] }
assert.equal(helpers.calendarValidationKey({ ...base, exceptions: [leap] }), null)
assert.equal(helpers.calendarValidationKey({ ...base, exceptions: [leap, leap] }), "supportAvailability.invalidExceptionDate")
})
test("closing a holiday normalizes outgoing periods without discarding the editing draft", () => {
const calendar = { ...base, exceptions: [{ date: "2026-12-25", closed: true, periods: [{ start: "00:00", end: "24:00" }] }] }
const payload = helpers.supportCalendarPayload(calendar)
assert.equal(payload.exceptions[0].periods.length, 0)
assert.equal(calendar.exceptions[0].periods.length, 1)
calendar.exceptions[0].closed = false
assert.equal(helpers.supportCalendarPayload(calendar).exceptions[0].periods[0].end, "24:00")
})
test("vacation validates absolute interval and rejects DST gap without shifting", () => {
const previous = process.env.TZ
process.env.TZ = "America/New_York"
try {
assert.equal(helpers.localDateTimeToISO("2026-03-08T02:30"), null)
assert.equal(helpers.localDateTimeToISO("2026-03-08T03:30"), "2026-03-08T07:30:00.000Z")
assert.equal(helpers.vacationValidationKey("2026-10-16T10:00", "2026-10-16T09:00"), "supportAvailability.invalidVacation")
assert.equal(helpers.vacationValidationKey("2026-10-16T10:00", "2026-10-16T11:00"), null)
} finally { process.env.TZ = previous }
})
test("DST overlap exposes both exact instants and preserves existing later occurrence", () => {
const previous = process.env.TZ
process.env.TZ = "America/New_York"
try {
const values = Array.from(helpers.localDateTimeCandidates("2026-11-01T01:30"))
assert.deepEqual(values, ["2026-11-01T05:30:00.000Z", "2026-11-01T06:30:00.000Z"])
assert.equal(helpers.selectedLocalDateTimeISO("2026-11-01T01:30", values[1]), values[1])
assert.equal(helpers.selectedLocalDateTimeISO("2026-11-01T01:30", "stale"), values[0])
} finally { process.env.TZ = previous }
})
test("preview renders calendar zone and explicitly uses UTC for opaque legacy process zones", () => {
assert.match(helpers.formatAvailabilityTime("2026-10-16T12:00:00Z", "Europe/Moscow", "en-US"), /03:00|15:00/)
assert.match(helpers.formatAvailabilityTime("2026-10-16T12:00:00Z", "Local", "en-US"), /UTC/)
})
async function apiWith(request, extras = {}) {
return load("./api/support-availability.ts", {
"@/lib/api/client": { request },
"@/lib/support-availability": helpers,
"@/lib/sdk/runtime-config": { readSupportChatRuntimeConfig: () => ({ channelId: "channel-a", baseUrl: "https://support.example/", userToken: "must-not-send" }) },
}, extras)
}
test("public status is channel-scoped, anonymous and uses shared request without private credentials", async () => {
let call
const api = await apiWith(async (url, options) => { call = { url, options }; return { available: false, reason: "temporarily_absent", timezone: "UTC" } })
const response = await api.fetchPublicSupportStatus()
assert.equal(call.url, "/api/support/status")
assert.equal(call.options.skipAuth, true)
assert.equal(call.options.baseUrl, "https://support.example")
assert.equal(call.options.headers["X-Channel-Id"], "channel-a")
assert.equal("Authorization" in call.options.headers, false)
assert.equal(response.available, false)
})
test("assignment candidates use captured conversation and trust server eligibility, not idle alone", async () => {
let path
const api = await apiWith(async url => { path = url; return [{ online: false, serviceStatus: 0, eligible: false, reason: "offline" }] })
const items = await api.fetchAvailabilityAgents(19)
assert.equal(path, "/api/dashboard/support_availability/agents?conversationId=19")
assert.equal(items[0].eligible, false)
})
test("calendar save and preview normalize closed periods identically and retain explicit mode", async () => {
const calls = []
const api = await apiWith(async (url, options) => { calls.push({ url, body: JSON.parse(options.body) }); return base })
const draft = { ...base, mode: "legacy", exceptions: [{ date: "2026-12-25", closed: true, periods: [{ start: "09:00", end: "17:00" }] }] }
await api.updateSupportCalendar(draft)
await api.previewSupportCalendar(draft, "2026-12-25T12:00:00Z")
assert.equal(calls[0].body.exceptions[0].periods.length, 0)
assert.equal(calls[0].body.mode, "legacy")
assert.equal(calls[1].body.calendar.exceptions[0].periods.length, 0)
assert.equal(calls[1].body.at, "2026-12-25T12:00:00Z")
assert.equal(draft.exceptions[0].periods.length, 1)
})
test("a hung availability read has a bounded deadline and cleans up timers", async () => {
let timeout, cleared = false, signal
const api = await apiWith(async (_url, options) => new Promise((_, reject) => {
signal = options.signal
signal.addEventListener("abort", () => reject(new Error("aborted")))
}), { setTimeout(fn, delay) { assert.equal(delay, 10000); timeout = fn; return 1 }, clearTimeout(id) { assert.equal(id, 1); cleared = true } })
const pending = api.fetchMyAvailability()
timeout()
await assert.rejects(pending, /aborted/)
assert.equal(signal.aborted, true)
assert.equal(cleared, true)
})
test("unmount/account-scope cancellation aborts an in-flight availability read", async () => {
const controller = new AbortController()
const api = await apiWith(async (_url, options) => new Promise((_, reject) => options.signal.addEventListener("abort", () => reject(new Error("aborted")))))
const pending = api.fetchAvailabilityAgents(undefined, controller.signal)
controller.abort()
await assert.rejects(pending, /aborted/)
})
test("all generated manual statuses and blocking reasons have complete translations", async () => {
const enums = await load("./generated/enums.ts")
const mapping = await load("./support-availability-i18n.ts", { "@/lib/generated/enums": enums })
for (const locale of ["en-US", "ru-RU", "zh-CN"]) {
const catalog = JSON.parse(await readFile(new URL(`../messages/${locale}.json`, import.meta.url), "utf8"))
const t = key => { const value = key.split(".").reduce((value, part) => value?.[part], catalog); assert.equal(typeof value, "string", `${locale}: ${key}`); return value }
assert.equal(mapping.availabilityStatusOptions(t).length, 5)
for (const reason of ["eligible", "profile_disabled", "user_disabled", "team_disabled", "outside_hours", "offline", "busy", "break", "unavailable", "vacation", "auto_assign_disabled", "capacity"]) mapping.availabilityReasonLabel(reason, t)
}
})