Files
Agent-System-Frontend/test/rdm-members-search.test.ts
T
2026-10-18 10:53:45 +00:00

126 lines
5.3 KiB
TypeScript

// 项目成员页检索(R2 契约):服务端先过滤后分页,前端只拼参数、映射候选与追加分页。
// node 环境无 DOM——决策逻辑全部落在 src/rdmManagement.ts 的纯函数上逐项断言。
import { describe, expect, it } from 'vitest'
import type { Project } from '../src/contracts'
import {
JOINED_REASON, MATCH_MODE_LABELS, MEMBER_PAGE_LIMIT, RDM_MEMBER_COMMANDS,
accountListParams, accountsToPickItems, appendMemberPage, memberListParams,
personLabel, rdmMembersReady, rdmProjectCommandError, roleOptionsPayload,
type RdmMemberRow,
} from '../src/rdmManagement'
const row = (owner: string, name?: string): RdmMemberRow =>
({ owner, name, role: 'member', addedBy: 'admin', addedAt: '' })
describe('accountListParams', () => {
it('passes roleId, match and cursor to rdm.account.list', () => {
expect(accountListParams('p1', { query: ' zhang ', cursor: 'c2', limit: 20, roleId: 'r-eng', match: 'exact' }))
.toEqual({ projectId: 'p1', query: 'zhang', cursor: 'c2', limit: 20, roleId: 'r-eng', match: 'exact' })
})
it('omits empty filters and defaults to fuzzy matching', () => {
expect(accountListParams('p1', { query: ' ', cursor: '', limit: 20, roleId: '' }))
.toEqual({ projectId: 'p1', limit: 20, match: 'fuzzy' })
})
it('labels both match modes in Chinese', () => {
expect(MATCH_MODE_LABELS).toEqual({ fuzzy: '模糊匹配', exact: '精确账号或姓名' })
})
})
describe('memberListParams', () => {
it('sends query, roleId and cursor with a page size of 50', () => {
expect(memberListParams('p1', { query: 'li', roleId: 'r-qa', cursor: 'n1' }))
.toEqual({ projectId: 'p1', query: 'li', roleId: 'r-qa', cursor: 'n1', limit: MEMBER_PAGE_LIMIT })
expect(MEMBER_PAGE_LIMIT).toBe(50)
})
it('loads the unfiltered first page when no filter is set', () => {
expect(memberListParams('p1', { query: '', roleId: '', cursor: '' })).toEqual({ projectId: 'p1', limit: 50 })
})
it('forwards the optional match and projectRole filters', () => {
expect(memberListParams('p1', { match: 'exact', projectRole: 'owner', limit: 10 }))
.toEqual({ projectId: 'p1', limit: 10, match: 'exact', projectRole: 'owner' })
})
})
describe('roleOptionsPayload', () => {
it('asks for member-purpose role options', () => {
expect(roleOptionsPayload('p1')).toEqual({ projectId: 'p1', purpose: 'members' })
})
})
describe('accountsToPickItems', () => {
it('drops invalid accounts', () => {
const items = accountsToPickItems([
{ owner: 'a', name: '甲', valid: true, joined: false },
{ owner: 'gone', name: '离职', valid: false, joined: false },
])
expect(items.map(it => it.id)).toEqual(['a'])
})
it('disables joined candidates with the 已加入项目 reason instead of a badge', () => {
const items = accountsToPickItems([
{ owner: 'a', name: '甲', valid: true, joined: true },
{ owner: 'b', valid: true, joined: false },
{ owner: 'c', name: '丙', valid: true },
], ['b'])
expect(items).toEqual([
{ id: 'a', title: '甲', subtitle: 'a', disabled: true, disabledReason: JOINED_REASON },
{ id: 'b', title: 'b', subtitle: 'b', disabled: true, disabledReason: '已加入项目' },
{ id: 'c', title: '丙', subtitle: 'c' },
])
expect(items.some(it => 'badge' in it)).toBe(false)
})
it('keeps name as title and account as subtitle so the selection shows both', () => {
const [it] = accountsToPickItems([{ owner: 'zhangsan', name: '张三', valid: true, joined: false }])
expect(it.title).toBe('张三')
expect(it.subtitle).toBe('zhangsan')
})
it('tolerates a missing account list', () => {
expect(accountsToPickItems(undefined)).toEqual([])
})
})
describe('personLabel', () => {
it('renders name · account', () => {
expect(personLabel({ owner: 'zhangsan', name: '张三' })).toBe('张三 · zhangsan')
})
it('falls back to the account when there is no name', () => {
expect(personLabel({ owner: 'zhangsan' })).toBe('zhangsan')
expect(personLabel({ owner: 'zhangsan', name: ' ' })).toBe('zhangsan')
expect(personLabel({ owner: 'zhangsan', name: 'zhangsan' })).toBe('zhangsan')
})
})
describe('appendMemberPage', () => {
it('appends the next page in order without duplicating accounts', () => {
const first = [row('a', '甲'), row('b')]
const got = appendMemberPage(first, [row('b', '乙-重复'), row('c', '丙')])
expect(got.map(m => m.owner)).toEqual(['a', 'b', 'c'])
expect(got[1].name).toBeUndefined()
expect(first.map(m => m.owner)).toEqual(['a', 'b'])
})
it('starts a fresh list from null and dedupes within a page', () => {
expect(appendMemberPage(null, [row('a'), row('a')]).map(m => m.owner)).toEqual(['a'])
expect(appendMemberPage(null, undefined)).toEqual([])
})
})
describe('rdm.role.options command gating', () => {
it('is a project read command, so archived projects can still load role filters', () => {
const archived = { id: 'p1', settings: { 'rdm.state': 'archived', 'rdm.projectId': 'rdm-1' } } as unknown as Project
expect(rdmProjectCommandError(archived, 'rdm.role.options')).toBeNull()
})
it('does not gate member page readiness', () => {
expect(RDM_MEMBER_COMMANDS).not.toContain('rdm.role.options')
expect(rdmMembersReady(RDM_MEMBER_COMMANDS.map(id => ({ id, enabled: true })))).toBe(true)
})
})