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.
This commit is contained in:
127
web/lib/stores/agent-conversations.test.mjs
Normal file
127
web/lib/stores/agent-conversations.test.mjs
Normal file
@@ -0,0 +1,127 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user