fix: recover chat replies and organize setup navigation [skip ci]
Targeted source-only patch from reviewed local e370485. Adds bounded realtime/catch-up recovery and moves the setup wizard under System. Static checks only; regression tests, full build and runtime verification have not been run. No deployment. Separate hardening-review branch is not merged. Expected tree: 6235478b9a.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Modified for targeted chat realtime recovery; see docs/fixes/chat-realtime-recovery.md.
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
|
||||
@@ -97,3 +98,113 @@ test("beforeConnect failure triggers onConnectError and disconnect status", asyn
|
||||
assert.ok(statusList.includes("disconnected"), "status should include disconnected")
|
||||
manager.disconnect()
|
||||
})
|
||||
|
||||
test("a stuck CONNECTING socket times out and retries without reloading", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] })
|
||||
const sockets = []
|
||||
const manager = createRealtimeConnectionManager({
|
||||
connectTimeoutMs: 100,
|
||||
reconnectBaseDelayMs: 10,
|
||||
createSocket: () => { const socket = new MockWebSocket(); sockets.push(socket); return socket },
|
||||
})
|
||||
t.after(() => manager.disconnect())
|
||||
await manager.connect()
|
||||
t.mock.timers.tick(100)
|
||||
assert.equal(sockets[0].readyState, MockWebSocket.CLOSED)
|
||||
t.mock.timers.tick(10)
|
||||
assert.equal(sockets.length, 2)
|
||||
sockets[1].open()
|
||||
t.mock.timers.tick(100)
|
||||
assert.equal(manager.getSocket(), sockets[1])
|
||||
})
|
||||
|
||||
test("timed out authentication cannot open a late socket after a newer connection", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] })
|
||||
let resolveOld, attempts = 0, created = 0
|
||||
const old = new Promise((resolve) => { resolveOld = resolve })
|
||||
const manager = createRealtimeConnectionManager({
|
||||
connectTimeoutMs: 100,
|
||||
reconnectBaseDelayMs: 10,
|
||||
beforeConnect: () => ++attempts === 1 ? old : Promise.resolve(),
|
||||
createSocket: () => { created++; return new MockWebSocket() },
|
||||
})
|
||||
t.after(() => manager.disconnect())
|
||||
const first = manager.connect()
|
||||
t.mock.timers.tick(100)
|
||||
t.mock.timers.tick(10)
|
||||
await Promise.resolve()
|
||||
resolveOld()
|
||||
await first
|
||||
assert.equal(created, 1)
|
||||
})
|
||||
|
||||
test("late replaced socket close cannot clear the current heartbeat or subscription", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] })
|
||||
let closed = 0, pings = 0
|
||||
const sockets = []
|
||||
const manager = createRealtimeConnectionManager({
|
||||
pingIntervalMs: 10,
|
||||
createSocket: () => {
|
||||
const socket = new MockWebSocket()
|
||||
socket.close = () => { socket.readyState = MockWebSocket.CLOSED }
|
||||
socket.send = () => { pings++ }
|
||||
sockets.push(socket)
|
||||
return socket
|
||||
},
|
||||
onClose: () => { closed++ },
|
||||
})
|
||||
t.after(() => manager.disconnect())
|
||||
await manager.connect()
|
||||
sockets[0].open()
|
||||
await manager.connect()
|
||||
sockets[1].open()
|
||||
sockets[0].emit("close", {})
|
||||
t.mock.timers.tick(10)
|
||||
assert.equal(manager.getSocket(), sockets[1])
|
||||
assert.equal(closed, 0)
|
||||
assert.equal(pings, 1)
|
||||
})
|
||||
|
||||
test("an error without a close event detaches a connecting socket and retries", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] })
|
||||
const sockets = []
|
||||
const manager = createRealtimeConnectionManager({
|
||||
connectTimeoutMs: 100,
|
||||
reconnectBaseDelayMs: 10,
|
||||
createSocket: () => {
|
||||
const socket = new MockWebSocket()
|
||||
socket.close = () => { socket.readyState = MockWebSocket.CLOSING }
|
||||
sockets.push(socket)
|
||||
return socket
|
||||
},
|
||||
})
|
||||
t.after(() => manager.disconnect())
|
||||
await manager.connect()
|
||||
sockets[0].emit("error", {})
|
||||
assert.equal(manager.getSocket(), null)
|
||||
t.mock.timers.tick(10)
|
||||
assert.equal(sockets.length, 2)
|
||||
sockets[1].open()
|
||||
t.mock.timers.tick(100)
|
||||
assert.equal(manager.getSocket(), sockets[1])
|
||||
})
|
||||
|
||||
test("becoming ineligible during authentication clears the opening deadline", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout", "setInterval"] })
|
||||
let resolveAuth, allowed = true, errors = 0
|
||||
const auth = new Promise((resolve) => { resolveAuth = resolve })
|
||||
const manager = createRealtimeConnectionManager({
|
||||
connectTimeoutMs: 100,
|
||||
canReconnect: () => allowed,
|
||||
beforeConnect: () => auth,
|
||||
createSocket: () => assert.fail("ineligible connection must not create a socket"),
|
||||
onConnectError: () => { errors++ },
|
||||
})
|
||||
t.after(() => manager.disconnect())
|
||||
const connecting = manager.connect()
|
||||
allowed = false
|
||||
resolveAuth()
|
||||
await connecting
|
||||
t.mock.timers.tick(200)
|
||||
assert.equal(errors, 0)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user