/) }) +test('AI document query keeps high-risk command guidance in trusted rendered markup', () => { + const frame = resolveWorkbenchIntentFrame('删除申请单草稿', { today: '2026-06-24' }) + const route = resolveWorkbenchIntentActionRoute(frame) + const intent = resolveAiDocumentQueryIntent(route.queryPrompt, { today: '2026-06-24' }) + const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, [{ + id: 'draft-application-1', + claim_no: 'AP-DRAFT-001', + document_type_code: 'application', + expense_type: 'travel_application', + status: 'draft', + reason: '测试申请草稿', + created_at: '2026-06-24T09:00:00Z' + }], { commandFrame: frame })) + + assert.match(rendered, /已查询到相关单据<\/h3>/) + assert.match(rendered, /
/) + assert.match(rendered, /系统不会直接删除相关单据/) + assert.match(rendered, /进入单据详情核对后再操作/) + assert.match(rendered, / /) + assert.match(rendered, /进入详情确认删除/) +}) + test('AI document query trusted html rejects unsafe card markup', () => { const rendered = renderAiConversationHtml([ '### 查询结果', diff --git a/web/tests/assistant-session-draft-delete.test.mjs b/web/tests/assistant-session-draft-delete.test.mjs index c6b5672..6293aaf 100644 --- a/web/tests/assistant-session-draft-delete.test.mjs +++ b/web/tests/assistant-session-draft-delete.test.mjs @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url' import { markAiWorkbenchConversationDraftDeleted, + markAiWorkbenchConversationDocumentDeleted, loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../src/utils/aiWorkbenchConversationStore.js' @@ -120,6 +121,54 @@ test('deleting an application draft marks AI workbench detail links as unavailab assert.equal(loadAiWorkbenchConversationHistory(user)[0].messages.length, 2) }) +test('deleted document detail links are disabled for reopened AI conversations', () => { + installWindowStub() + const user = { username: 'zhangsan@example.com' } + + saveAiWorkbenchConversation(user, { + id: 'conversation-document-delete-candidate', + title: '删除申请单草稿', + messages: [ + { + id: 'assistant-document-candidates', + role: 'assistant', + content: [ + '### 已查询到相关单据', + '', + '[进入详情确认删除](#ai-open-document-detail:claim_id%3Dclaim-draft-2%26claim_no%3DAP-DRAFT-002)' + ].join('\n') + } + ] + }) + + const nextHistory = markAiWorkbenchConversationDocumentDeleted(user, { + claimId: 'claim-draft-2', + claimNo: 'AP-DRAFT-002' + }) + const conversation = nextHistory.find((item) => item.id === 'conversation-document-delete-candidate') + + assert.ok(conversation) + assert.match(conversation.messages[0].content, /#ai-deleted-document-detail:/) + assert.doesNotMatch(conversation.messages[0].content, /#ai-open-document-detail:/) + assert.match(conversation.messages[0].content, /\[单据已删除\]/) + assert.match(conversation.messages.at(-1).content, /单据 AP-DRAFT-002 已经删除或不可访问/) + assert.match(conversation.messages.at(-1).content, /该历史入口已失效,请返回单据列表重新查询/) + assert.equal(loadAiWorkbenchConversationHistory(user)[0].messages.length, 2) +}) + +test('AI workbench validates document detail links before opening stale history entries', () => { + const aiMode = readFileSync( + fileURLToPath(new URL('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js', import.meta.url)), + 'utf8' + ) + + assert.match(aiMode, /fetchExpenseClaimDetail/) + assert.match(aiMode, /markAiWorkbenchConversationDocumentDeleted/) + assert.match(aiMode, /async function handleAiAnswerMarkdownClick/) + assert.match(aiMode, /await ensureAiDocumentDetailStillAvailable/) + assert.match(aiMode, /已将这条历史入口标记为不可查看/) +}) + test('saving a draft keeps the financial assistant open for continued work', () => { const appShellScript = readFileSync( fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)),