docs: 交付文档、维护指引与端到端回归

补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
This commit is contained in:
WangShen007 committed 2026-10-21 21:40:00 +08:00
1 parent 18732801b5
commit 6cd904d460
216 files changed
+37038

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import { test, expect, channels, createGroup, connectEmail, listChannelAccounts, unique, AI_REPLY } from '@e2e'
// What an Agent-role teammate may do, checked at the API — the UI hiding a link
// proves nothing about who the server lets through.
/** Admin-only operations, each with a body the endpoint would accept from an admin. */
const ADMIN_ONLY: Array<{ method: string; path: string; data?: Record<string, unknown> }> = [
{ method: 'GET', path: '/ai/config' },
{ method: 'POST', path: '/ai/setup', data: { model_type: 'OPENAI', model_name: 'gpt-4o', api_key: 'sk-test' } },
{ method: 'GET', path: '/users' },
{ method: 'POST', path: '/users', data: { email: 'blocked@e2e.example.com', full_name: 'Blocked', password: 'E2e-Blocked-Passw0rd!', role_id: 1 } },
{ method: 'GET', path: '/roles' },
{ method: 'GET', path: '/groups' },
{ method: 'POST', path: '/groups', data: { name: 'Blocked group' } },
{ method: 'GET', path: '/agent/list' },
{ method: 'POST', path: '/agent', data: { name: 'Blocked agent', instructions: ['x'] } },
{ method: 'GET', path: '/widget-apps' },
{ method: 'GET', path: '/tickets/settings' },
{ method: 'PUT', path: '/tickets/settings', data: {} },
{ method: 'POST', path: '/channels/telegram', data: { bot_token: '1:blocked' } },
{ method: 'GET', path: '/mcp-tools' },
]
test.describe('Agent role', () => {
test('is refused every admin operation by the API @team', async ({ makeMember }) => {
const agent = await makeMember({ role: 'Agent' })
const refused = await Promise.all(ADMIN_ONLY.map(async ({ method, path, data }) =>
`${method} ${path} → ${(await agent.api.raw(method, path, { data })).status()}`))
expect(refused).toEqual(ADMIN_ONLY.map(({ method, path }) => `${method} ${path} → 403`))
// The same token works where an agent belongs.
expect((await agent.api.raw('GET', '/chats/recent')).status()).toBe(200)
})
test('sees the connected channels for the inbox, but never their webhook secrets @team', async ({ adminApi, makeMember }) => {
const inbox = await connectEmail(adminApi, `${unique('support')}@e2e.example.com`)
expect(inbox.webhook_url).toContain('token=')
const agent = await makeMember({ role: 'Agent' })
const seen = (await listChannelAccounts(agent.api)).find((a) => a.id === inbox.id)
expect(seen).toBeDefined()
expect(seen?.webhook_url ?? null).toBeNull()
})
test('with "assigned only" sees their own group\'s handed-over chats and not another group\'s @team @handover', async ({ adminApi, makeMember, makeAgent, llm, request }) => {
// Two teams, each with an AI agent that hands over to it.
const groupA = await createGroup(adminApi, unique('Team A'), [])
const groupB = await createGroup(adminApi, unique('Team B'), [])
const alice = await makeMember({ role: 'Agent', groupIds: [groupA], seeAllAiChats: false })
await makeMember({ role: 'Agent', groupIds: [groupB] }) // someone online in B, so B's handover happens
const agentA = await makeAgent({ transferToHuman: true }, [groupA])
const agentB = await makeAgent({ transferToHuman: true }, [groupB])
for (const agent of [agentA, agentB]) {
await llm.script(agent, (s) => s.otherwise().handOver('Let me connect you with a teammate.'))
}
const forA = (await channels.WhatsApp({ api: adminApi, request, agentId: agentA.id })).chatWith(unique('Ana'))
const forB = (await channels.WhatsApp({ api: adminApi, request, agentId: agentB.id })).chatWith(unique('Ben'))
await forA.send('I need a person')
await forB.send('I need a person')
await expect.poll(() => forA.replies(), AI_REPLY).toHaveLength(1)
await expect.poll(() => forB.replies(), AI_REPLY).toHaveLength(1)
const visibleTo = async () => (await alice.api.get<Array<{ agent: { id: string }; session_id: string }>>('/chats/recent', { params: { limit: 100 } }))
const agentsAliceSees = (await visibleTo()).map((c) => String(c.agent.id))
expect(agentsAliceSees).toContain(agentA.id)
expect(agentsAliceSees).not.toContain(agentB.id)
// Opening B's chat directly is refused too, not just hidden from the list.
const allChats = await adminApi.get<Array<{ agent: { id: string }; session_id: string }>>('/chats/recent', { params: { limit: 100, agent_id: agentB.id } })
const bSession = allChats.find((c) => String(c.agent.id) === agentB.id)!.session_id
expect([403, 404]).toContain((await alice.api.raw('GET', `/chats/${bSession}`)).status())
})
})
+31
View File
@@ -0,0 +1,31 @@
import { test, expect, storageStatePath, unique, LoginPage, Sidebar } from '@e2e'
test.describe('team', () => {
test('an admin adds an agent who can sign in to the inbox @team', async ({ humanAgentsPage, anonymousPage }) => {
const id = unique('agent')
const member = { name: `Alex ${id}`, email: `${id}@e2e.example.com`, password: 'E2e-New-Agent-Passw0rd!', role: 'Agent' as const }
await humanAgentsPage.goto()
await humanAgentsPage.invite(member)
await expect(humanAgentsPage.member(member.email)).toBeVisible()
const login = new LoginPage(anonymousPage)
await login.goto()
await login.signIn(member.email, member.password)
await expect(anonymousPage).toHaveURL(/\/conversations/)
})
})
test.describe('as a human agent', () => {
test.use({ storageState: storageStatePath('human-agent') })
test('the Agent role does not see admin areas in the navigation @team', async ({ page }) => {
// Not /conversations: the inbox collapses the sidebar to icons.
await page.goto('/people')
const sidebar = new Sidebar(page)
await expect(sidebar.link('Inbox')).toBeVisible()
for (const adminArea of ['AI Agents', 'Human Agents', 'Knowledge', 'AI Configuration', 'Organization']) {
await expect(sidebar.link(adminArea)).toBeHidden()
}
})
})