Files
Friendship-and-chat-im/scripts/test-moderation-client.cjs
dzc 73d5a13762 feat:聊天后台撤回消息
fix:聊天禁用bug
2026-11-03 19:08:46 +08:00

141 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
'use strict'
// Exercises the installed mobile client's real store code without modifying or starting the app.
// Run: node --test scripts/test-moderation-client.cjs [CLIENT_REPO may be provided in the environment].
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const { test } = require('node:test')
const clientRoot = process.env.CLIENT_REPO || path.resolve(__dirname, '../../../Friendship-and-chat-uniapp')
const storePath = path.join(clientRoot, 'src/chat-store/chatStore.js')
const enumsPath = path.join(clientRoot, 'src/common/enums.js')
const clone = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value))
const chatInfo = { type: 'PRIVATE', targetId: 7 }
function harness() {
const memory = new Map()
const context = vm.createContext({
defineStore: (_name, definition) => definition,
useUserStore: () => ({ userInfo: { id: 42 } }),
useFriendStore: () => ({ findFriend: () => null }),
useGroupStore: () => ({ findGroup: () => null }),
console: { log() {} },
uni: {
setStorageSync: (key, value) => memory.set(key, clone(value)),
getStorageSync: (key) => clone(memory.get(key)),
removeStorageSync: (key) => memory.delete(key)
}
})
const enumSource = fs.readFileSync(enumsPath, 'utf8').replace(/export\s*\{[\s\S]*?\}\s*;?\s*$/, '')
vm.runInContext(enumSource, context, { filename: enumsPath })
const storeSource = fs.readFileSync(storePath, 'utf8')
.replace(/^import\s+.*?;?\s*$/gm, '')
.replace('export default defineStore', 'globalThis.definition = defineStore')
vm.runInContext(storeSource, context, { filename: storePath })
const definition = context.definition
const store = definition.state()
for (const [key, action] of Object.entries(definition.actions)) store[key] = action.bind(store)
for (const [key, getter] of Object.entries(definition.getters)) {
Object.defineProperty(store, key, { get: () => getter(store) })
}
return { store, memory }
}
function message(id, content = 'original private content') {
return { id, type: 0, status: 3, content, sendId: 7, recvId: 42, sendTime: 1000 + id, selfSend: false }
}
function quote(id) {
return { ...message(id, 'reply text'), quoteMessage: message(101) }
}
function conversation(messages, hotMinIdx = 0) {
return {
...chatInfo, showName: 'Sender', messages, hotMinIdx, unreadCount: 2,
readedMessageIdx: 0, lastContent: 'original private content', lastSendTime: 1101,
stored: false, hidden: false
}
}
function recall(selfSend = false) {
// The persisted event has its own ID; its legacy-client wire projection must use the source ID.
return { id: 101, content: '101', type: 10, status: 3, sendId: 7, recvId: 42, sendTime: 2000, selfSend }
}
function assertHidden(value, expectedText = '对方撤回了一条消息') {
assert.equal(value.type, 21)
assert.equal(value.status, 2)
assert.equal(value.content, expectedText)
}
for (const { name, split, messages } of [
{ name: 'hot source and hot quote', split: 0, messages: [message(101), quote(102)] },
{ name: 'cold source and hot quote', split: 1, messages: [message(101), quote(102)] },
{ name: 'cold source and cold quote', split: 2, messages: [message(101), quote(102), message(103, 'later message')] }
]) {
test(`legacy RECALL hides ${name} and persists both affected partitions`, () => {
const { store, memory } = harness()
const chat = conversation(clone(messages), split)
store.chats = [chat]
store.saveToStorage(true)
store.recallMessage(recall(), chatInfo)
assertHidden(chat.messages[0])
assertHidden(chat.messages[1].quoteMessage, '引用内容已撤回')
const cold = memory.get('chats-app-42-PRIVATE-7')?.messages || []
const hot = memory.get('chats-app-42-PRIVATE-7-hot')?.messages || []
const persisted = [...cold, ...hot]
assertHidden(persisted.find((item) => item.id === 101))
assertHidden(persisted.find((item) => item.id === 102).quoteMessage, '引用内容已撤回')
assert.equal(chat.unreadCount, 2)
assert.equal(chat.lastContent, '对方撤回了一条消息')
})
}
test('repeat notifications with status 3 do not add unread counts or duplicate rows', () => {
const { store, memory } = harness()
const chat = conversation([message(101), quote(102)], 1)
store.chats = [chat]
store.saveToStorage(true)
store.recallMessage(recall(), chatInfo)
store.recallMessage(recall(), chatInfo)
assert.equal(chat.unreadCount, 2)
assert.equal(chat.messages.length, 2)
assert.equal(memory.get('chats-app-42-PRIVATE-7-hot').unreadCount, 2)
assertHidden(chat.messages[0])
})
test('sender receives the same compatible projection and sees their own recall tip', () => {
const { store } = harness()
const chat = conversation([message(101)])
store.chats = [chat]
store.recallMessage(recall(true), chatInfo)
assertHidden(chat.messages[0], '你撤回了一条消息')
assert.equal(chat.unreadCount, 2)
})
test('offline buffer processing persists hidden source and quotes after refresh', () => {
const { store, memory } = harness()
store.initChats({ chats: [conversation([message(101), quote(102)], 2)], privateMsgMaxId: 102 })
store.setLoading(true)
store.recallMessage(recall(), chatInfo)
assert.equal(memory.size, 0)
store.setLoading(false)
store.refreshChats()
assertHidden(store.chats[0].messages[0])
assertHidden(store.chats[0].messages[1].quoteMessage, '引用内容已撤回')
const cold = memory.get('chats-app-42-PRIVATE-7').messages
assertHidden(cold[0])
assertHidden(cold[1].quoteMessage, '引用内容已撤回')
assert.equal(store.chats[0].unreadCount, 2)
})
test('unknown conversation is ignored without creating empty chats or unread counts', () => {
const { store, memory } = harness()
assert.doesNotThrow(() => store.recallMessage(recall(), chatInfo))
assert.equal(store.chats.length, 0)
assert.equal(memory.size, 0)
})