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

+107
View File
@@ -0,0 +1,107 @@
import { test, expect, expectUnchanged, publishWorkflow, AI_REPLY, type WorkflowGraph } from '@e2e'
// A published workflow runs instead of the model: scripted steps, no AI calls.
test.describe('workflows', () => {
test('a one-message workflow greets once on load and answers each message once, without the AI @workflows', async ({ agent, adminApi, widget, llm }) => {
await publishWorkflow(adminApi, agent.id, {
nodes: [{ id: 'msg-1', node_type: 'message', name: 'Greeting', config: { message_text: 'Hello from our workflow.' } }],
})
await widget.embed()
await widget.open()
await expect(widget.agentMessages).toHaveText(['Hello from our workflow.'], AI_REPLY)
await widget.send('hi')
await expect(widget.agentMessages).toHaveText(['Hello from our workflow.', 'Hello from our workflow.'], AI_REPLY)
await expectUnchanged('agent messages', () => widget.agentMessages.allTextContents())
expect(await llm.requests(agent)).toHaveLength(0)
})
test('a workflow asks a question and branches on the answer @workflows', async ({ agent, adminApi, widget }) => {
await publishWorkflow(adminApi, agent.id, {
nodes: [
{ id: 'ask', node_type: 'user_input', name: 'Ask Plan', config: { prompt_message: 'Which plan are you on? (pro/basic)', confirmation_message: 'Thanks — {{ask_plan_input}} it is.' } },
{ id: 'is-pro', node_type: 'condition', name: 'Is Pro', config: { condition_expression: '{{ask_plan_input}} === pro' } },
{ id: 'pro', node_type: 'message', name: 'Pro', config: { message_text: 'Pro customers get priority support.' } },
{ id: 'basic', node_type: 'message', name: 'Basic', config: { message_text: 'Basic customers can upgrade any time.' } },
],
connections: [
{ id: 'c1', source_node_id: 'ask', target_node_id: 'is-pro' },
{ id: 'c2', source_node_id: 'is-pro', target_node_id: 'pro', label: 'true' },
{ id: 'c3', source_node_id: 'is-pro', target_node_id: 'basic', label: 'false' },
],
})
await widget.embed()
await widget.open()
await expect(widget.lastAgentMessage()).toHaveText('Which plan are you on? (pro/basic)', AI_REPLY)
await widget.send('pro')
await expect(widget.lastAgentMessage()).toHaveText('Pro customers get priority support.', AI_REPLY)
await expect(widget.agentMessages).toContainText(['Thanks — pro it is.'])
await expect(widget.agentMessages.filter({ hasText: 'Basic customers' })).toHaveCount(0)
})
// Plan AND seats, built the way the dashboard's condition builder saves it.
function planAndSeats(wired: Array<'true' | 'false'> = ['true', 'false']): WorkflowGraph {
return {
nodes: [
{ id: 'ask-plan', node_type: 'user_input', name: 'Ask Plan', config: { prompt_message: 'Which plan are you on?' } },
{ id: 'ask-seats', node_type: 'user_input', name: 'Ask Seats', config: { prompt_message: 'How many seats?' } },
{
id: 'big-pro', node_type: 'condition', name: 'Big Pro',
config: {
condition_groups: [{
id: 'g1', logicalOperator: 'AND', rules: [
{ id: 'r1', variable: 'ask_plan_input', operator: '===', value: 'pro' },
{ id: 'r2', variable: 'ask_seats_input', operator: '>', value: '3' },
],
}],
condition_expression: '({{ask_plan_input}} === "pro" AND {{ask_seats_input}} > 3)',
},
},
{ id: 'priority', node_type: 'message', name: 'Priority', config: { message_text: 'You get a dedicated account manager.' } },
{ id: 'standard', node_type: 'message', name: 'Standard', config: { message_text: 'Our team will get back to you.' } },
],
connections: [
{ id: 'c1', source_node_id: 'ask-plan', target_node_id: 'ask-seats' },
{ id: 'c2', source_node_id: 'ask-seats', target_node_id: 'big-pro' },
...wired.map((branch) => ({ id: `c-${branch}`, source_node_id: 'big-pro', target_node_id: branch === 'true' ? 'priority' : 'standard', label: branch })),
],
}
}
const answers = [
{ plan: 'pro', seats: '5', reply: 'You get a dedicated account manager.', why: 'both rules hold' },
{ plan: 'pro', seats: '2', reply: 'Our team will get back to you.', why: 'one rule fails' },
{ plan: 'basic" OR "a" === "a', seats: '5', reply: 'Our team will get back to you.', why: 'an answer that looks like a condition is just text' },
{ plan: 'pro', seats: 'lots', reply: 'Our team will get back to you.', why: 'a non-number is never "greater than"' },
]
for (const { plan, seats, reply, why } of answers) {
test(`a combined condition routes on both answers: ${why} @workflows`, async ({ agent, adminApi, widget }) => {
await publishWorkflow(adminApi, agent.id, planAndSeats())
await widget.embed()
await widget.open()
await expect(widget.lastAgentMessage()).toHaveText('Which plan are you on?', AI_REPLY)
await widget.send(plan)
await expect(widget.lastAgentMessage()).toHaveText('How many seats?', AI_REPLY)
await widget.send(seats)
await expect(widget.lastAgentMessage()).toHaveText(reply, AI_REPLY)
})
}
test('a condition with no branch for its result stops instead of taking the other one @workflows', async ({ agent, adminApi, widget, llm }) => {
await publishWorkflow(adminApi, agent.id, planAndSeats(['true']))
await widget.embed()
await widget.open()
await expect(widget.lastAgentMessage()).toHaveText('Which plan are you on?', AI_REPLY)
await widget.send('basic')
await expect(widget.lastAgentMessage()).toHaveText('How many seats?', AI_REPLY)
await widget.send('1')
await expectUnchanged('agent messages', () => widget.agentMessages.allTextContents())
await expect(widget.agentMessages.filter({ hasText: 'dedicated account manager' })).toHaveCount(0)
expect(await llm.requests(agent)).toHaveLength(0)
})
})