Files
agent-desk-ng/web/lib/stores/agent-conversations.test.mjs
mono 63e0457886 fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
2026-10-16 12:15:20 +00:00

128 lines
5.3 KiB
JavaScript

import assert from "node:assert/strict"
import test from "node:test"
import { deferred, loadTypeScriptModule } from "../test-support/load-typescript-module.mjs"
function createStore(initialize) {
let state
const setState = (update, replace = false) => {
const next = typeof update === "function" ? update(state) : update
state = replace ? next : { ...state, ...next }
}
state = initialize(setState, () => state)
const initial = state
return { getState: () => state, setState, getInitialState: () => initial }
}
function fixture(api = {}) {
let session = { user: { id: 1 }, accessToken: "test-session-a" }
const loaded = loadTypeScriptModule(new URL("./agent-conversations.ts", import.meta.url), {
zustand: { create: createStore },
"@/lib/api/agent": api,
"@/lib/auth": { readSession: () => session },
"@/lib/utils": { generateUUID: () => "test-id" },
"@/lib/im-message": { summarizeIMMessage: () => "test-summary" },
"@/lib/im-message-merge": {
cursorFromLoadedImMessages: (messages) => String(messages[0]?.id ?? ""),
hasMoreAfterLatestImMessageMerge: () => false,
mergeImMessagesByIdAsc: (left, right) => [...left, ...right],
parseImMessageCursorId: Number,
},
"@/lib/im-realtime-state": {
patchConversationList: (list) => list,
patchConversationListWithMessage: (list) => list,
},
"@/lib/agent-activity-state": {},
"@/lib/operator-presence-state": {},
})
return {
store: loaded.useAgentConversationsStore,
reset: loaded.resetAgentConversations,
set session(next) { session = next },
}
}
test("late conversation list cannot restore private data after reset/account change", async () => {
const old = deferred()
const f = fixture({ fetchAgentConversations: () => old.promise })
const loading = f.store.getState().loadConversations()
f.session = { user: { id: 2 }, accessToken: "test-session-b" }
f.reset()
f.store.setState({ conversations: [{ id: 20 }], selectedConversationId: 20 })
old.resolve({ results: [{ id: 10 }] })
await loading
assert.equal(f.store.getState().conversations[0].id, 20)
assert.equal(f.store.getState().selectedConversationId, 20)
})
test("same-account relogin invalidates old load generations too", async () => {
const old = deferred()
const f = fixture({ fetchAgentMessages: () => old.promise })
f.store.setState({ selectedConversationId: 10 })
const loading = f.store.getState().loadMessages(10)
f.reset()
f.store.setState({ selectedConversationId: 10, messages: [{ id: 200 }] })
old.resolve({ results: [{ id: 100 }] })
await loading
assert.equal(f.store.getState().messages[0].id, 200)
})
test("operator send binds its recipient, is single-flight, and does not patch the new selection", async () => {
const response = deferred()
const calls = []
const f = fixture({ sendAgentMessage: (payload) => { calls.push(payload); return response.promise } })
f.store.setState({ selectedConversationId: 10 })
assert.equal(await f.store.getState().sendMessage("Wrong recipient", 20), null)
const sending = f.store.getState().sendMessage("For A", 10)
assert.equal(await f.store.getState().sendMessage("Duplicate", 10), null)
f.store.setState({ selectedConversationId: 20, messages: [{ id: 200 }] })
response.resolve({ id: 100, conversationId: 10 })
await sending
assert.equal(calls.length, 1)
assert.equal(calls[0].conversationId, 10)
assert.equal(f.store.getState().messages[0].id, 200)
})
test("old send finally does not unlock a new account's pending send", async () => {
const old = deferred()
const f = fixture({ sendAgentMessage: () => old.promise })
f.store.setState({ selectedConversationId: 10 })
const sending = f.store.getState().sendMessage("For A", 10)
f.reset()
f.store.setState({ sending: true, selectedConversationId: 20 })
old.resolve({ id: 100, conversationId: 10 })
await sending
assert.equal(f.store.getState().sending, true)
})
test("attachment upload cannot send under a new account after identity change", async () => {
const upload = deferred()
let sends = 0
const f = fixture({
uploadAgentConversationAttachment: () => upload.promise,
sendAgentMessage: async () => { sends += 1; return { id: 100 } },
})
f.store.setState({ selectedConversationId: 10 })
const pending = f.store.getState().sendAttachment({}, 10)
f.session = { user: { id: 2 }, accessToken: "test-session-b" }
f.reset()
upload.resolve({ assetId: "test", filename: "test.txt" })
assert.equal(await pending, null)
assert.equal(sends, 0)
})
test("a fresh snapshot invalidates old pagination without leaving its loading flag stuck", async () => {
const oldPage = deferred()
const fresh = deferred()
const f = fixture({ fetchAgentMessages: (params) => params.cursor ? oldPage.promise : fresh.promise })
f.store.setState({ selectedConversationId: 10, messagesCursor: "100", messagesHasMore: true })
const older = f.store.getState().loadOlderMessages()
assert.equal(f.store.getState().messagesLoadingMore, true)
const refresh = f.store.getState().loadMessages(10)
fresh.resolve({ results: [{ id: 200 }], hasMore: true })
await refresh
oldPage.resolve({ results: [{ id: 50 }], hasMore: false })
await older
assert.equal(f.store.getState().messages[0].id, 200)
assert.equal(f.store.getState().messagesLoadingMore, false)
})