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

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

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import { test, expect, AI_REPLY } from '@e2e'
interface People { items: Array<{ id: string; email: string | null }> }
interface Person { name: string | null; email: string | null; lead_stage: string; captured_attributes: Record<string, unknown> }
test.describe('lead capture', () => {
test.use({ agentSettings: { leadCapture: true } })
test('a visitor who shares their details shows up as a lead, with what they told us @leads', async ({ agent, widget, llm, peoplePage, adminApi }) => {
const { email, name } = widget.customer
await llm.script(agent, (s) => s
.when(/@/).captureLead('Thanks! Someone from sales will reach out.', { email, name, company: 'Acme Corp' })
.otherwise().reply('Happy to set that up — what is your email?'))
await widget.embed()
await widget.open()
await widget.send('I would like a demo')
// A visitor answers the question once it is asked; replies to back-to-back
// messages are produced concurrently and may arrive in either order.
await expect(widget.lastAgentMessage()).toHaveText('Happy to set that up — what is your email?', AI_REPLY)
await widget.send(`Sure, I am ${name}, ${email}`)
await expect(widget.lastAgentMessage()).toHaveText('Thanks! Someone from sales will reach out.', AI_REPLY)
await peoplePage.goto()
await peoplePage.findLead(email)
await expect(peoplePage.person(email)).toContainText('Lead')
const [found] = (await adminApi.get<People>('/people', { params: { search: email } })).items
const person = await adminApi.get<Person>(`/people/${found.id}`)
expect(person).toMatchObject({ name, email })
expect(JSON.stringify(person.captured_attributes)).toContain('Acme Corp')
})
})
+58
View File
@@ -0,0 +1,58 @@
import { test, expect, channels, findPerson, phoneNumber, AI_REPLY } from '@e2e'
// The People directory's edit rules, at the API the drawer uses.
test.describe('People', () => {
test('a contact can be renamed, given a phone and marked as a customer @leads', async ({ agent, llm, adminApi, request }) => {
await llm.script(agent, (s) => s.otherwise().reply('Thanks for writing in.'))
const chat = (await channels.Email({ api: adminApi, request, agentId: agent.id })).chatWith('Tess')
await chat.send('Hello')
await expect.poll(() => chat.replies(), AI_REPLY).toHaveLength(1)
const person = await findPerson(adminApi, chat.address)
const phone = phoneNumber()
await adminApi.patch(`/people/${person.id}`, { data: { full_name: 'Tess Tester', phone } })
await adminApi.post(`/people/${person.id}/mark-customer`)
expect(await adminApi.get(`/people/${person.id}`)).toMatchObject({ name: 'Tess Tester', phone, lead_stage: 'customer' })
})
test('a phone number must be international and can belong to only one person @leads', async ({ agent, llm, adminApi, request }) => {
await llm.script(agent, (s) => s.otherwise().reply('Thanks for writing in.'))
const inbox = await channels.Email({ api: adminApi, request, agentId: agent.id })
const ann = inbox.chatWith('Ann')
const ben = inbox.chatWith('Ben')
await ann.send('Hello')
await ben.send('Hello')
await expect.poll(() => ann.replies(), AI_REPLY).toHaveLength(1)
await expect.poll(() => ben.replies(), AI_REPLY).toHaveLength(1)
const annPerson = await findPerson(adminApi, ann.address)
const benPerson = await findPerson(adminApi, ben.address)
const local = await adminApi.raw('PATCH', `/people/${annPerson.id}`, { data: { phone: '07700 900123' } })
expect(local.status()).toBe(400)
expect(await local.text()).toContain('international format')
const shared = phoneNumber()
await adminApi.patch(`/people/${annPerson.id}`, { data: { phone: shared } })
const taken = await adminApi.raw('PATCH', `/people/${benPerson.id}`, { data: { phone: shared } })
expect(taken.status()).toBe(400)
expect(await taken.text()).toContain('already belongs to another person')
})
test('once a WhatsApp contact has given a real email, their number can no longer be changed away @leads', async ({ makeAgent, llm, adminApi, request }) => {
// Their phone is now the only thing an incoming WhatsApp message can find them by.
const agent = await makeAgent({ leadCapture: true })
const email = `wes.${Date.now()}@example.com`
await llm.script(agent, (s) => s.otherwise().captureLead('Thanks, noted.', { email, name: 'Wes' }))
const chat = (await channels.WhatsApp({ api: adminApi, request, agentId: agent.id })).chatWith('Wes')
await chat.send(`It is ${email}`)
await expect.poll(() => chat.replies(), AI_REPLY).toHaveLength(1)
const person = await findPerson(adminApi, email)
const response = await adminApi.raw('PATCH', `/people/${person.id}`, { data: { phone: phoneNumber() } })
expect(response.status()).toBe(400)
expect(await response.text()).toContain('how their WhatsApp messages find them')
expect((await adminApi.get<{ phone: string }>(`/people/${person.id}`)).phone).toContain(chat.waId)
})
})