fix: make inbox confirmation retry safe
This commit is contained in:
@@ -19,6 +19,7 @@ const page = await browser.newPage({ viewport: { width: 1440, height: 1024 }, de
|
||||
const errors = []
|
||||
const failures = []
|
||||
const projectId = '019b0000-0000-7000-8000-000000000001'
|
||||
const secondProjectId = '019b0000-0000-7000-8000-000000000012'
|
||||
const taskSearchResultId = '019b0000-0000-7000-8000-000000000003'
|
||||
const inboxItemId = '019b0000-0000-7000-8000-000000000004'
|
||||
const inboxTaskSuggestionId = '019b0000-0000-7000-8000-000000000005'
|
||||
@@ -28,6 +29,8 @@ const secondInboxItemId = '019b0000-0000-7000-8000-000000000008'
|
||||
const secondInboxTaskSuggestionId = '019b0000-0000-7000-8000-000000000009'
|
||||
const secondInboxNoteSuggestionId = '019b0000-0000-7000-8000-000000000010'
|
||||
const secondInboxSourceSuggestionId = '019b0000-0000-7000-8000-000000000011'
|
||||
const thirdInboxItemId = '019b0000-0000-7000-8000-000000000013'
|
||||
const thirdInboxTaskSuggestionId = '019b0000-0000-7000-8000-000000000014'
|
||||
const unknownProjectId = '019b0000-0000-7000-8000-000000000099'
|
||||
const externalProjectId = '019b0000-0000-7000-8000-000000000088'
|
||||
const externalTaskId = '019b0000-0000-7000-8000-000000000089'
|
||||
@@ -43,8 +46,11 @@ let expectingInboxConfirmError = false
|
||||
let expectedInboxConfirmConsoleErrorCount = 0
|
||||
let expectingInboxRefreshError = false
|
||||
let expectedInboxRefreshConsoleErrorCount = 0
|
||||
let expectingInboxConflict = false
|
||||
let expectedInboxConflictConsoleErrorCount = 0
|
||||
let failNextInboxConfirm = true
|
||||
let failNextInboxWorkspaceRefresh = false
|
||||
let conflictNextInboxConfirm = false
|
||||
page.on('console', (message) => {
|
||||
if (message.type() !== 'error') return
|
||||
if (
|
||||
@@ -58,7 +64,7 @@ page.on('console', (message) => {
|
||||
if (
|
||||
expectingInboxConfirmError &&
|
||||
expectedInboxConfirmConsoleErrorCount === 0 &&
|
||||
message.text() === 'Failed to load resource: the server responded with a status of 500 (Internal Server Error)'
|
||||
message.text().includes('Failed to load resource')
|
||||
) {
|
||||
expectedInboxConfirmConsoleErrorCount += 1
|
||||
return
|
||||
@@ -71,6 +77,14 @@ page.on('console', (message) => {
|
||||
expectedInboxRefreshConsoleErrorCount += 1
|
||||
return
|
||||
}
|
||||
if (
|
||||
expectingInboxConflict &&
|
||||
expectedInboxConflictConsoleErrorCount === 0 &&
|
||||
message.text() === 'Failed to load resource: the server responded with a status of 409 (Conflict)'
|
||||
) {
|
||||
expectedInboxConflictConsoleErrorCount += 1
|
||||
return
|
||||
}
|
||||
errors.push(message.text())
|
||||
})
|
||||
|
||||
@@ -87,7 +101,7 @@ const visualCheckWorkspace = {
|
||||
},
|
||||
channels: [
|
||||
{ id: 'overview', projectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
||||
{ id: 'inbox', projectId, type: 'inbox', title: 'Inbox 消息流', icon: 'mail', count: 2, url: '', sortOrder: 1 },
|
||||
{ id: 'inbox', projectId, type: 'inbox', title: 'Inbox 消息流', icon: 'mail', count: 3, url: '', sortOrder: 1 },
|
||||
{ id: 'tasks', projectId, type: 'tasks', title: '工作计划', icon: 'list', count: 1, url: '', sortOrder: 2 },
|
||||
{ id: 'ai', projectId, type: 'ai_sessions', title: 'AI 助手', icon: 'robot', count: 0, url: '', sortOrder: 3 },
|
||||
{ id: 'notes', projectId, type: 'notes_sources', title: '笔记资料', icon: 'file', count: 0, url: '', sortOrder: 4 },
|
||||
@@ -118,6 +132,16 @@ const visualCheckWorkspace = {
|
||||
tag: '待处理',
|
||||
time: '2026-07-21T02:20:00Z',
|
||||
},
|
||||
{
|
||||
id: thirdInboxItemId,
|
||||
projectId,
|
||||
source: '手动收集',
|
||||
title: '延迟确认测试',
|
||||
summary: '确认期间切换项目时必须保留最新选择。',
|
||||
status: 'open',
|
||||
tag: '待处理',
|
||||
time: '2026-07-21T02:10:00Z',
|
||||
},
|
||||
],
|
||||
tasks: [],
|
||||
aiSessions: [],
|
||||
@@ -125,6 +149,29 @@ const visualCheckWorkspace = {
|
||||
cronPlans: [],
|
||||
}
|
||||
|
||||
const secondVisualCheckWorkspace = {
|
||||
project: {
|
||||
id: secondProjectId,
|
||||
name: '并行项目',
|
||||
identifier: 'parallel',
|
||||
icon: '并',
|
||||
background: '#722ED1',
|
||||
description: '用于验证异步操作不会恢复旧项目选择。',
|
||||
initials: '并行',
|
||||
unreadCount: 0,
|
||||
},
|
||||
channels: [
|
||||
{ id: 'second-overview', projectId: secondProjectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
||||
],
|
||||
tags: [],
|
||||
recentSessions: [],
|
||||
inbox: [],
|
||||
tasks: [],
|
||||
aiSessions: [],
|
||||
notesSources: [],
|
||||
cronPlans: [],
|
||||
}
|
||||
|
||||
await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const method = route.request().method()
|
||||
@@ -137,7 +184,7 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/v1/projects') {
|
||||
await route.fulfill({ json: [visualCheckWorkspace.project] })
|
||||
await route.fulfill({ json: [visualCheckWorkspace.project, secondVisualCheckWorkspace.project] })
|
||||
return
|
||||
}
|
||||
if (url.pathname === `/api/v1/projects/${projectId}/workspace`) {
|
||||
@@ -153,6 +200,11 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
||||
await route.fulfill({ json: visualCheckWorkspace })
|
||||
return
|
||||
}
|
||||
if (url.pathname === `/api/v1/projects/${secondProjectId}/workspace`) {
|
||||
workspaceRequests.push(url.pathname)
|
||||
await route.fulfill({ json: secondVisualCheckWorkspace })
|
||||
return
|
||||
}
|
||||
if (url.pathname === `/api/v1/inbox/${inboxItemId}/analyze` && method === 'POST') {
|
||||
inboxAnalyzeRequests.push(inboxItemId)
|
||||
if (inboxAnalyzeRequests.length === 1) await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
@@ -180,14 +232,30 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
if ([`/api/v1/inbox/${inboxItemId}/confirm`, `/api/v1/inbox/${secondInboxItemId}/confirm`].includes(url.pathname) && method === 'POST') {
|
||||
if (url.pathname === `/api/v1/inbox/${thirdInboxItemId}/analyze` && method === 'POST') {
|
||||
inboxAnalyzeRequests.push(thirdInboxItemId)
|
||||
await route.fulfill({
|
||||
json: {
|
||||
suggestions: [
|
||||
{ id: thirdInboxTaskSuggestionId, kind: 'task', title: '延迟确认任务', body: '验证跨项目异步选择。' },
|
||||
],
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if ([`/api/v1/inbox/${inboxItemId}/confirm`, `/api/v1/inbox/${secondInboxItemId}/confirm`, `/api/v1/inbox/${thirdInboxItemId}/confirm`].includes(url.pathname) && method === 'POST') {
|
||||
inboxConfirmRequests.push(route.request().postDataJSON())
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
if (failNextInboxConfirm) {
|
||||
failNextInboxConfirm = false
|
||||
await route.abort('failed')
|
||||
return
|
||||
}
|
||||
if (conflictNextInboxConfirm) {
|
||||
conflictNextInboxConfirm = false
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
json: { error: { code: 'internal_error', message: '确认创建失败,请稍后重试' } },
|
||||
status: 409,
|
||||
json: { error: { code: 'conflict', message: '确认状态发生冲突' } },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -639,9 +707,12 @@ if (await inboxChannelButton.count() === 0) {
|
||||
if (!await confirmButton.evaluate((button) => button.classList.contains('arco-btn-loading'))) {
|
||||
failures.push('Inbox confirmation must show loading while the write is pending')
|
||||
}
|
||||
if (!await confirmButton.isDisabled()) {
|
||||
failures.push('Inbox confirmation must remain disabled while the write is pending')
|
||||
}
|
||||
await page.waitForTimeout(300)
|
||||
if (!await inboxPage.getByText('确认创建失败,请稍后重试', { exact: true }).isVisible()) {
|
||||
failures.push('failed Inbox confirmation must show the API Chinese error')
|
||||
if (!await inboxPage.getByText('确认结果未知,服务端支持幂等处理,可安全重试', { exact: true }).isVisible()) {
|
||||
failures.push('a network-failed Inbox confirmation must explain that an idempotent retry is safe')
|
||||
}
|
||||
expectingInboxConfirmError = false
|
||||
if (expectedInboxConfirmConsoleErrorCount !== 1) {
|
||||
@@ -704,6 +775,53 @@ if (await inboxChannelButton.count() === 0) {
|
||||
failures.push(`expected one simulated Inbox refresh console error, got ${expectedInboxRefreshConsoleErrorCount}`)
|
||||
}
|
||||
await page.screenshot({ path: 'test-results/project-inbox-refresh-warning.png', fullPage: true })
|
||||
|
||||
const thirdInboxRow = inboxPage.locator('.mail-item', { hasText: '延迟确认测试' })
|
||||
await thirdInboxRow.click()
|
||||
await inboxPage.getByRole('button', { name: '分析内容', exact: true }).click()
|
||||
await inboxPage.getByRole('checkbox').first().waitFor({ state: 'attached', timeout: 2000 }).catch(() => {})
|
||||
const thirdConfirmButton = inboxPage.getByRole('button', { name: '确认创建', exact: true })
|
||||
await thirdConfirmButton.click()
|
||||
await page.waitForTimeout(50)
|
||||
if (!await thirdConfirmButton.isDisabled()) {
|
||||
failures.push('delayed Inbox confirmation must stay disabled before its response')
|
||||
}
|
||||
await page.locator('.project-button[title="并行项目"]').click()
|
||||
await page.waitForTimeout(700)
|
||||
const projectAfterDelayedConfirm = await page.locator('.project-title h5').textContent()
|
||||
if (projectAfterDelayedConfirm !== '并行项目') {
|
||||
failures.push(`delayed Inbox confirmation must preserve the latest project selection, got ${projectAfterDelayedConfirm}`)
|
||||
}
|
||||
if (!await page.locator('.project-button[title="并行项目"]').evaluate((button) => button.classList.contains('active'))) {
|
||||
failures.push('the project selected during Inbox confirmation must remain active after the request settles')
|
||||
}
|
||||
await page.locator('.project-button').first().click()
|
||||
await page.waitForTimeout(250)
|
||||
|
||||
await page.locator('.channel-button', { hasText: 'Inbox 消息流' }).click()
|
||||
const conflictInboxPage = page.locator('.project-inbox-page')
|
||||
await conflictInboxPage.locator('.mail-item', { hasText: '延迟确认测试' }).click()
|
||||
await conflictInboxPage.getByRole('button', { name: '分析内容', exact: true }).click()
|
||||
await conflictInboxPage.getByRole('checkbox').first().waitFor({ state: 'attached', timeout: 2000 }).catch(() => {})
|
||||
conflictNextInboxConfirm = true
|
||||
expectingInboxConflict = true
|
||||
const workspaceRequestsBeforeConflict = workspaceRequests.length
|
||||
const conflictConfirmButton = conflictInboxPage.getByRole('button', { name: '确认创建', exact: true })
|
||||
await conflictConfirmButton.click()
|
||||
await conflictInboxPage.getByText('确认状态发生冲突,工作区已刷新,请重新核对', { exact: true }).waitFor({ state: 'visible', timeout: 2000 }).catch(() => {})
|
||||
if (workspaceRequests.length <= workspaceRequestsBeforeConflict) {
|
||||
failures.push('a non-idempotency 409 must refresh the current workspace before reporting the conflict')
|
||||
}
|
||||
if (!await conflictConfirmButton.isDisabled()) {
|
||||
failures.push('a conflict refresh that finds the item processed must prevent another confirmation')
|
||||
}
|
||||
if (await conflictInboxPage.getByText('确认结果未知,服务端支持幂等处理,可安全重试', { exact: true }).count() !== 0) {
|
||||
failures.push('a 409 conflict must not be presented as a safe retryable network uncertainty')
|
||||
}
|
||||
expectingInboxConflict = false
|
||||
if (expectedInboxConflictConsoleErrorCount !== 1) {
|
||||
failures.push(`expected one simulated Inbox conflict console error, got ${expectedInboxConflictConsoleErrorCount}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user