2026-06-24 21:58:46 +08:00
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
|
|
import { useWorkbenchAiApplicationPreviewFlow } from '../src/composables/workbenchAiMode/useWorkbenchAiApplicationPreviewFlow.js'
|
|
|
|
|
|
|
|
|
|
|
|
function createRef(value) {
|
|
|
|
|
|
return { value }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createInlineMessage(role, content, options = {}) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: options.id || `msg-${Math.random().toString(16).slice(2)}`,
|
|
|
|
|
|
role,
|
|
|
|
|
|
content,
|
|
|
|
|
|
text: content,
|
|
|
|
|
|
paragraphs: String(content || '').split(/\n+/).filter(Boolean),
|
|
|
|
|
|
pending: Boolean(options.pending),
|
|
|
|
|
|
...options
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 20:17:36 +08:00
|
|
|
|
function buildApplicationPreviewFlowHarness(messages, options = {}) {
|
2026-06-24 21:58:46 +08:00
|
|
|
|
const conversationMessages = createRef(messages)
|
|
|
|
|
|
const applicationSubmitConfirmOpen = createRef(false)
|
|
|
|
|
|
const applicationSubmitConfirmContext = createRef(null)
|
|
|
|
|
|
const persisted = createRef(0)
|
|
|
|
|
|
|
|
|
|
|
|
const flow = useWorkbenchAiApplicationPreviewFlow({
|
|
|
|
|
|
activateInlineConversation: () => {},
|
|
|
|
|
|
applicationPreviewEditor: createRef({}),
|
|
|
|
|
|
applicationSubmitConfirmContext,
|
|
|
|
|
|
applicationSubmitConfirmOpen,
|
|
|
|
|
|
assistantDraft: createRef(''),
|
|
|
|
|
|
cancelApplicationPreviewEditor: () => {},
|
|
|
|
|
|
clearAiModeFiles: () => {},
|
|
|
|
|
|
closeWorkbenchDatePicker: () => {},
|
|
|
|
|
|
commitApplicationPreviewEditor: async () => true,
|
|
|
|
|
|
conversationId: createRef('conversation-context-submit'),
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationStarted: createRef(true),
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser: createRef({ username: 'zhangsan@example.com', name: '张三' }),
|
|
|
|
|
|
handleApplicationPreviewEditorKeydown: () => {},
|
|
|
|
|
|
inlineConversationAutoScrollPinned: createRef(true),
|
|
|
|
|
|
isApplicationPreviewEditing: createRef(false),
|
|
|
|
|
|
openApplicationPreviewEditor: () => {},
|
|
|
|
|
|
persistCurrentConversation: () => { persisted.value += 1 },
|
|
|
|
|
|
pushInlineApplicationActionUserMessage: (text) => {
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', text))
|
|
|
|
|
|
},
|
|
|
|
|
|
pushInlineUserMessage: (text) => {
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', text))
|
|
|
|
|
|
},
|
|
|
|
|
|
refreshApplicationPreviewEstimate: async (preview) => preview,
|
|
|
|
|
|
removeWorkbenchDateTag: () => {},
|
|
|
|
|
|
replaceInlineMessage: (id, nextMessage) => {
|
|
|
|
|
|
const index = conversationMessages.value.findIndex((item) => item.id === id)
|
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
|
conversationMessages.value.splice(index, 1, nextMessage)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
conversationMessages.value.push(nextMessage)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
resolveApplicationPreviewEditorDateMax: () => '',
|
|
|
|
|
|
resolveApplicationPreviewEditorDateMin: () => '',
|
|
|
|
|
|
resolveApplicationPreviewEditorControl: () => null,
|
|
|
|
|
|
resolveApplicationPreviewEditorOptions: () => [],
|
|
|
|
|
|
resolveInlineThinkingEvents: (message) => message?.stewardPlan?.thinkingEvents || [],
|
|
|
|
|
|
resolveLatestInlineUserPrompt: () => '2026-02-20 至 2026-02-23,去上海出差,交通火车,保存草稿',
|
|
|
|
|
|
scrollInlineConversationToBottom: () => {},
|
|
|
|
|
|
sending: createRef(false),
|
2026-06-29 20:17:36 +08:00
|
|
|
|
toast: () => {},
|
|
|
|
|
|
onApplicationActionCompleted: options.onApplicationActionCompleted
|
2026-06-24 21:58:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
applicationSubmitConfirmOpen,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
flow,
|
|
|
|
|
|
persisted
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 20:17:36 +08:00
|
|
|
|
test('workbench auto-saved application draft continues remaining steward task', async () => {
|
|
|
|
|
|
const originalFetch = globalThis.fetch
|
|
|
|
|
|
const requests = []
|
|
|
|
|
|
globalThis.fetch = async (url, options = {}) => {
|
|
|
|
|
|
const normalizedUrl = String(url)
|
|
|
|
|
|
if (normalizedUrl.includes('/reimbursements/application-preview-action')) {
|
|
|
|
|
|
const body = JSON.parse(String(options.body || '{}'))
|
|
|
|
|
|
requests.push({ url: normalizedUrl, body })
|
|
|
|
|
|
return {
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
async json() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
result: {
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
claim_id: 'claim-auto-saved-draft',
|
|
|
|
|
|
claim_no: 'AEW2DDAFL',
|
|
|
|
|
|
status: 'draft'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`unexpected request: ${normalizedUrl}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const continuedTasks = []
|
|
|
|
|
|
const remainingTasks = [{
|
|
|
|
|
|
task_id: 'task-reimbursement-2',
|
|
|
|
|
|
task_type: 'reimbursement',
|
|
|
|
|
|
assigned_agent: 'reimbursement_assistant',
|
|
|
|
|
|
summary: '报销昨天的业务招待费 2000 元',
|
|
|
|
|
|
ontology_fields: {
|
|
|
|
|
|
expense_type: 'entertainment',
|
|
|
|
|
|
amount: '2000元',
|
|
|
|
|
|
time_range: '2026-06-25',
|
|
|
|
|
|
reason: '业务招待费报销'
|
|
|
|
|
|
}
|
|
|
|
|
|
}]
|
|
|
|
|
|
const harness = buildApplicationPreviewFlowHarness([], {
|
|
|
|
|
|
onApplicationActionCompleted: (tasks, sourceMessage) => {
|
|
|
|
|
|
continuedTasks.push({ tasks, sourceMessage })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await harness.flow.startAiApplicationPreview('travel', '差旅费', '2月20-23日去上海出差3天,服务国网服务器部署,并且报销昨天的业务招待费2000元', {
|
|
|
|
|
|
autoSaveDraft: true,
|
|
|
|
|
|
stewardRemainingTasks: remainingTasks,
|
|
|
|
|
|
onApplicationActionCompleted: (tasks, sourceMessage) => {
|
|
|
|
|
|
continuedTasks.push({ tasks, sourceMessage, fromOptions: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(requests.length, 1)
|
|
|
|
|
|
assert.equal(continuedTasks.length, 1)
|
|
|
|
|
|
assert.deepEqual(continuedTasks[0].tasks, remainingTasks)
|
|
|
|
|
|
assert.equal(continuedTasks[0].sourceMessage.stewardRemainingTasks, remainingTasks)
|
|
|
|
|
|
assert.equal(continuedTasks[0].fromOptions, true)
|
|
|
|
|
|
assert.doesNotMatch(harness.conversationMessages.value.at(-1).content, /继续处理费用报销/)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
globalThis.fetch = originalFetch
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
test('workbench saved application draft can be submitted by contextual text without re-planning', async () => {
|
|
|
|
|
|
const originalFetch = globalThis.fetch
|
|
|
|
|
|
const requests = []
|
|
|
|
|
|
globalThis.fetch = async (url, options = {}) => {
|
|
|
|
|
|
const normalizedUrl = String(url)
|
|
|
|
|
|
if (normalizedUrl.includes('/reimbursements/claims')) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
async json() {
|
|
|
|
|
|
return { items: [] }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalizedUrl.includes('/reimbursements/application-preview-action')) {
|
|
|
|
|
|
const body = JSON.parse(String(options.body || '{}'))
|
|
|
|
|
|
requests.push({ url: normalizedUrl, body })
|
|
|
|
|
|
return {
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
async json() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
result: {
|
|
|
|
|
|
draft_payload: {
|
|
|
|
|
|
claim_id: 'claim-saved-draft',
|
|
|
|
|
|
claim_no: 'A20260220',
|
|
|
|
|
|
status: 'submitted',
|
|
|
|
|
|
approval_stage: '直属领导审批'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`unexpected request: ${normalizedUrl}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const previewMessage = createInlineMessage('assistant', '申请核对表', {
|
|
|
|
|
|
id: 'application-preview-1',
|
|
|
|
|
|
applicationPreview: {
|
|
|
|
|
|
readyToSubmit: true,
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
applicationType: '差旅费用申请',
|
|
|
|
|
|
time: '2026-02-20 至 2026-02-23',
|
|
|
|
|
|
location: '上海',
|
|
|
|
|
|
reason: '辅助国网仿生产服务器部署',
|
|
|
|
|
|
days: '4天',
|
|
|
|
|
|
transportMode: '火车',
|
|
|
|
|
|
amount: '1200元'
|
|
|
|
|
|
},
|
|
|
|
|
|
missingFields: [],
|
|
|
|
|
|
validationIssues: []
|
|
|
|
|
|
},
|
|
|
|
|
|
draftPayload: {
|
|
|
|
|
|
claim_id: 'claim-saved-draft',
|
|
|
|
|
|
claim_no: 'A20260220',
|
|
|
|
|
|
status: 'draft'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const harness = buildApplicationPreviewFlowHarness([
|
|
|
|
|
|
createInlineMessage('user', '2026-02-20 至 2026-02-23,去上海出差,交通火车,保存草稿'),
|
|
|
|
|
|
previewMessage,
|
|
|
|
|
|
createInlineMessage('assistant', '### 申请草稿已保存', {
|
|
|
|
|
|
draftPayload: previewMessage.draftPayload
|
|
|
|
|
|
})
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const handled = harness.flow.handleInlineApplicationPreviewTextAction(
|
|
|
|
|
|
'提交这个单据',
|
|
|
|
|
|
createRef(false)
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(handled, true)
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(harness.applicationSubmitConfirmOpen.value, false)
|
|
|
|
|
|
assert.equal(requests.length, 1)
|
|
|
|
|
|
assert.equal(requests[0].body.context_json.application_edit_claim_id, 'claim-saved-draft')
|
|
|
|
|
|
assert.equal(requests[0].body.context_json.application_edit_mode, true)
|
|
|
|
|
|
assert.match(harness.conversationMessages.value.at(-1).content, /申请单据已生成/)
|
|
|
|
|
|
assert.ok(harness.persisted.value > 0)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
globalThis.fetch = originalFetch
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|