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.
This commit is contained in:
147
web/lib/api/support-availability.transport.test.mjs
Normal file
147
web/lib/api/support-availability.transport.test.mjs
Normal file
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { deferred, loadTypeScriptModule } from "../test-support/load-typescript-module.mjs"
|
||||
|
||||
const helpers = loadTypeScriptModule(new URL("../support-availability.ts", import.meta.url), {}, { Date, Intl, Set })
|
||||
|
||||
// Execute real API wrappers with offline transport and a deterministic deadline clock.
|
||||
function fixture(request, initialConfig = {}) {
|
||||
const timers = new Map()
|
||||
let timerId = 0
|
||||
let runtimeConfig = initialConfig
|
||||
const api = loadTypeScriptModule(new URL("./support-availability.ts", import.meta.url), {
|
||||
"@/lib/api/client": { request },
|
||||
"@/lib/support-availability": helpers,
|
||||
"@/lib/sdk/runtime-config": { readSupportChatRuntimeConfig: () => runtimeConfig },
|
||||
}, {
|
||||
AbortController,
|
||||
process: { env: {} },
|
||||
setTimeout(callback, delay) {
|
||||
const id = ++timerId
|
||||
timers.set(id, { callback, delay })
|
||||
return id
|
||||
},
|
||||
clearTimeout(id) { timers.delete(id) },
|
||||
})
|
||||
return {
|
||||
api,
|
||||
timers,
|
||||
setRuntimeConfig(value) { runtimeConfig = value },
|
||||
}
|
||||
}
|
||||
|
||||
test("calendar reads normalize nullable collections without changing the transport result", async () => {
|
||||
const transportResult = {
|
||||
teamId: 7, mode: "legacy", timezone: "", effectiveTimezone: "UTC", deploymentTimezone: "UTC",
|
||||
weekly: null, exceptions: [{ date: "2026-12-31", closed: true, periods: null }],
|
||||
}
|
||||
const f = fixture(async () => transportResult)
|
||||
const calendar = await f.api.fetchSupportCalendar(7)
|
||||
assert.equal(calendar.weekly.length, 0)
|
||||
assert.equal(calendar.exceptions[0].periods.length, 0)
|
||||
assert.equal(transportResult.weekly, null)
|
||||
assert.equal(transportResult.exceptions[0].periods, null)
|
||||
assert.equal(f.timers.size, 0)
|
||||
|
||||
transportResult.exceptions = null
|
||||
assert.equal((await f.api.fetchSupportCalendar(7)).exceptions.length, 0)
|
||||
})
|
||||
|
||||
test("an already-aborted caller signal is not replaced with a live deadline signal", async () => {
|
||||
const caller = new AbortController()
|
||||
caller.abort()
|
||||
const f = fixture(async (_path, options) => {
|
||||
assert.equal(options.signal.aborted, true)
|
||||
throw new Error("cancelled fixture request")
|
||||
})
|
||||
await assert.rejects(f.api.fetchMyAvailability(caller.signal), /cancelled fixture request/)
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test("the next availability read can recover after a deadline abort", async () => {
|
||||
let calls = 0
|
||||
const f = fixture((_path, options) => {
|
||||
if (++calls > 1) return Promise.resolve({ userId: 42, eligible: false, reason: "offline" })
|
||||
return new Promise((_, reject) => options.signal.addEventListener("abort", () => reject(new Error("fixture deadline")), { once: true }))
|
||||
})
|
||||
const first = f.api.fetchMyAvailability()
|
||||
const deadline = [...f.timers.values()][0]
|
||||
assert.equal(deadline.delay, 10000)
|
||||
deadline.callback()
|
||||
await assert.rejects(first, /fixture deadline/)
|
||||
assert.equal(f.timers.size, 0)
|
||||
const next = await f.api.fetchMyAvailability()
|
||||
assert.equal(next.userId, 42)
|
||||
assert.equal(next.eligible, false)
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test("concurrent availability reads own separate abort signals and deadlines", async () => {
|
||||
const pending = []
|
||||
const f = fixture((_path, options) => {
|
||||
const response = deferred()
|
||||
options.signal.addEventListener("abort", () => response.reject(new Error("fixture deadline")), { once: true })
|
||||
pending.push({ ...response, signal: options.signal })
|
||||
return response.promise
|
||||
})
|
||||
const first = f.api.fetchMyAvailability()
|
||||
const second = f.api.fetchAvailabilityAgents(19)
|
||||
assert.equal(f.timers.size, 2)
|
||||
assert.notEqual(pending[0].signal, pending[1].signal)
|
||||
;[...f.timers.values()][0].callback()
|
||||
await assert.rejects(first, /fixture deadline/)
|
||||
assert.equal(pending[1].signal.aborted, false)
|
||||
pending[1].resolve([])
|
||||
assert.equal((await second).length, 0)
|
||||
assert.equal(f.timers.size, 0)
|
||||
})
|
||||
|
||||
test("settled reads remove the caller's abort listener on both success and failure", async () => {
|
||||
for (const fail of [false, true]) {
|
||||
const listeners = new Set()
|
||||
const signal = {
|
||||
aborted: false,
|
||||
addEventListener(event, listener) { assert.equal(event, "abort"); listeners.add(listener) },
|
||||
removeEventListener(event, listener) { assert.equal(event, "abort"); assert.equal(listeners.delete(listener), true) },
|
||||
}
|
||||
const f = fixture(async () => {
|
||||
assert.equal(listeners.size, 1)
|
||||
if (fail) throw new Error("fixture transport failure")
|
||||
return null
|
||||
})
|
||||
const pending = f.api.fetchMyAvailability(signal)
|
||||
if (fail) await assert.rejects(pending, /fixture transport failure/)
|
||||
else assert.equal(await pending, null)
|
||||
assert.equal(listeners.size, 0)
|
||||
assert.equal(f.timers.size, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test("each public check reads the current runtime channel and endpoint rather than caching the prior identity", async () => {
|
||||
const calls = []
|
||||
const f = fixture(async (path, options) => {
|
||||
calls.push({ path, options })
|
||||
return { available: false, reason: "temporarily_absent", timezone: "UTC" }
|
||||
}, { channelId: "channel-a", apiBaseUrl: "https://first.example.invalid/", baseUrl: "https://unused.example.invalid/" })
|
||||
await f.api.fetchPublicSupportStatus()
|
||||
f.setRuntimeConfig({ channelId: "channel-b", baseUrl: "https://second.example.invalid/" })
|
||||
await f.api.fetchPublicSupportStatus()
|
||||
assert.deepEqual(calls.map(({ options }) => options.baseUrl), ["https://first.example.invalid", "https://second.example.invalid"])
|
||||
assert.deepEqual(calls.map(({ options }) => options.headers["X-Channel-Id"]), ["channel-a", "channel-b"])
|
||||
for (const { path, options } of calls) {
|
||||
assert.equal(path, "/api/support/status")
|
||||
assert.equal(options.skipAuth, true)
|
||||
assert.deepEqual(Object.keys(options.headers), ["X-Channel-Id"])
|
||||
assert.equal(options.body, undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test("successful public HTTP requests never manufacture human availability", async () => {
|
||||
for (const reason of ["outside_hours", "temporarily_absent", "capacity"]) {
|
||||
const response = { available: false, reason, timezone: "UTC", nextOpeningAt: "2026-12-31T09:00:00Z" }
|
||||
const f = fixture(async () => response)
|
||||
assert.equal(await f.api.fetchPublicSupportStatus(), response)
|
||||
assert.equal(response.available, false)
|
||||
assert.equal(f.timers.size, 0)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user