mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(web): preserve session not-found and member history behavior
This commit is contained in:
1 parent
5f76e65359
commit
1e7325197c
5 files changed
+135
-6
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import Fastify from 'fastify'
|
||||
import type { SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import { registerSessionRoutes } from './sessions'
|
||||
|
||||
function createMissingSessionAdapter(): SessionRuntimeAdapter {
|
||||
return {
|
||||
listSessionIds: () => [],
|
||||
openReadonly: () => null,
|
||||
openWritable: () => null,
|
||||
closeSession: () => {},
|
||||
getDbPath: (sessionId) => `/tmp/${sessionId}.db`,
|
||||
deleteSessionFile: () => false,
|
||||
ensureReadonly: () => {
|
||||
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
||||
},
|
||||
ensureWritable: () => {
|
||||
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('CLI Web session routes', () => {
|
||||
it('returns 404 when requesting a missing session by id', async () => {
|
||||
const app = Fastify()
|
||||
registerSessionRoutes(app, createMissingSessionAdapter())
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/_web/sessions/missing' })
|
||||
await app.close()
|
||||
|
||||
assert.equal(response.statusCode, 404)
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,11 @@ export function registerSessionRoutes(server: FastifyInstance, adapter: SessionR
|
||||
})
|
||||
|
||||
server.get<{ Params: { id: string } }>('/_web/sessions/:id', async (request) => {
|
||||
return sessionService.getAnalysisSession(adapter, request.params.id)
|
||||
const session = sessionService.getAnalysisSession(adapter, request.params.id)
|
||||
if (!session) {
|
||||
throw Object.assign(new Error(`Session not found: ${request.params.id}`), { statusCode: 404 })
|
||||
}
|
||||
return session
|
||||
})
|
||||
|
||||
server.delete<{ Params: { id: string } }>('/_web/sessions/:id', async (request, reply) => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { getMembersWithAliases, getMembersPaginated } from '../message-queries'
|
||||
import { getMemberNameHistory, getMembersWithAliases, getMembersPaginated } from '../message-queries'
|
||||
import type { DatabaseAdapter, PreparedStatement } from '../../interfaces'
|
||||
|
||||
// ==================== Mock helpers ====================
|
||||
@@ -157,6 +157,39 @@ function createMockDb(opts: { hasAliases?: boolean; hasAvatar?: boolean } = {}):
|
||||
}
|
||||
}
|
||||
|
||||
function createNameHistoryDb(opts: {
|
||||
hasHistoryTable: boolean
|
||||
historyRows?: Record<string, unknown>[]
|
||||
}): DatabaseAdapter {
|
||||
return {
|
||||
prepare(sql: string): PreparedStatement {
|
||||
return {
|
||||
get(...params: unknown[]) {
|
||||
if (sql.includes('sqlite_master') && params.includes('member_name_history')) {
|
||||
return { cnt: opts.hasHistoryTable ? 1 : 0 }
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
all() {
|
||||
if (sql.includes('FROM member_name_history')) {
|
||||
return opts.historyRows ?? []
|
||||
}
|
||||
if (sql.includes('FROM message')) {
|
||||
return [
|
||||
{ accountName: 'Alice', groupNickname: 'A', startTs: 1000, endTs: 1500 },
|
||||
{ accountName: 'Alice Chen', groupNickname: null, startTs: 2000, endTs: 2500 },
|
||||
]
|
||||
}
|
||||
return []
|
||||
},
|
||||
run() {
|
||||
return { changes: 0, lastInsertRowid: 0 }
|
||||
},
|
||||
}
|
||||
},
|
||||
} as unknown as DatabaseAdapter
|
||||
}
|
||||
|
||||
// ==================== getMembersWithAliases ====================
|
||||
|
||||
describe('getMembersWithAliases', () => {
|
||||
@@ -198,6 +231,30 @@ describe('getMembersWithAliases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== getMemberNameHistory ====================
|
||||
|
||||
describe('getMemberNameHistory', () => {
|
||||
it('derives history from message rows when member_name_history table is absent', () => {
|
||||
const result = getMemberNameHistory(createNameHistoryDb({ hasHistoryTable: false }), 1)
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ nameType: 'account_name', name: 'Alice', startTs: 1000, endTs: 1500 },
|
||||
{ nameType: 'group_nickname', name: 'A', startTs: 1000, endTs: 1500 },
|
||||
{ nameType: 'account_name', name: 'Alice Chen', startTs: 2000, endTs: 2500 },
|
||||
])
|
||||
})
|
||||
|
||||
it('derives history from message rows when member_name_history table is empty', () => {
|
||||
const result = getMemberNameHistory(createNameHistoryDb({ hasHistoryTable: true, historyRows: [] }), 1)
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ nameType: 'account_name', name: 'Alice', startTs: 1000, endTs: 1500 },
|
||||
{ nameType: 'group_nickname', name: 'A', startTs: 1000, endTs: 1500 },
|
||||
{ nameType: 'account_name', name: 'Alice Chen', startTs: 2000, endTs: 2500 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== getMembersPaginated ====================
|
||||
|
||||
describe('getMembersPaginated', () => {
|
||||
|
||||
@@ -453,6 +453,38 @@ export interface MemberNameHistoryEntry {
|
||||
endTs: number | null
|
||||
}
|
||||
|
||||
function getMemberNameHistoryFromMessages(db: DatabaseAdapter, memberId: number): MemberNameHistoryEntry[] {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
sender_account_name as accountName,
|
||||
sender_group_nickname as groupNickname,
|
||||
MIN(ts) as startTs,
|
||||
MAX(ts) as endTs
|
||||
FROM message
|
||||
WHERE sender_id = ?
|
||||
GROUP BY sender_account_name, sender_group_nickname
|
||||
ORDER BY startTs`
|
||||
)
|
||||
.all(memberId) as unknown as Array<{
|
||||
accountName: string | null
|
||||
groupNickname: string | null
|
||||
startTs: number
|
||||
endTs: number | null
|
||||
}>
|
||||
|
||||
const history: MemberNameHistoryEntry[] = []
|
||||
for (const row of rows) {
|
||||
if (row.accountName) {
|
||||
history.push({ nameType: 'account_name', name: row.accountName, startTs: row.startTs, endTs: row.endTs })
|
||||
}
|
||||
if (row.groupNickname) {
|
||||
history.push({ nameType: 'group_nickname', name: row.groupNickname, startTs: row.startTs, endTs: row.endTs })
|
||||
}
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
export interface MemberWithAliases {
|
||||
id: number
|
||||
platformId: string
|
||||
@@ -646,14 +678,16 @@ export function getConversationBetween(
|
||||
* Get name change history for a member
|
||||
*/
|
||||
export function getMemberNameHistory(db: DatabaseAdapter, memberId: number): MemberNameHistoryEntry[] {
|
||||
if (!hasTable(db, 'member_name_history')) return []
|
||||
if (!hasTable(db, 'member_name_history')) return getMemberNameHistoryFromMessages(db, memberId)
|
||||
|
||||
return db
|
||||
const history = db
|
||||
.prepare(
|
||||
`SELECT name_type as nameType, name, start_ts as startTs, end_ts as endTs
|
||||
FROM member_name_history WHERE member_id = ? ORDER BY start_ts DESC`
|
||||
)
|
||||
.all(memberId) as unknown as MemberNameHistoryEntry[]
|
||||
|
||||
return history.length > 0 ? history : getMemberNameHistoryFromMessages(db, memberId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,8 +96,8 @@ export function deleteMember(adapter: SessionRuntimeAdapter, sessionId: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member name change history from the member_name_history table.
|
||||
* Uses core's getMemberNameHistory (not the web.ts message-derived version).
|
||||
* Get member name change history.
|
||||
* Core prefers member_name_history and falls back to message-derived names for legacy sessions.
|
||||
*/
|
||||
export function getMemberNameHistory(adapter: SessionRuntimeAdapter, sessionId: string, memberId: number) {
|
||||
const db = adapter.ensureReadonly(sessionId)
|
||||
|
||||
Reference in new issue
Block a user