feat: align controlled AI sessions and MVP controls

This commit is contained in:
2026-07-21 19:19:36 +08:00
parent 9fa6744516
commit 82829645c2
19 changed files with 850 additions and 465 deletions

View File

@@ -31,6 +31,8 @@ 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 aiSessionId = '019b0000-0000-7000-8000-000000000015'
const createdAISessionId = '019b0000-0000-7000-8000-000000000016'
const unknownProjectId = '019b0000-0000-7000-8000-000000000099'
const externalProjectId = '019b0000-0000-7000-8000-000000000088'
const externalTaskId = '019b0000-0000-7000-8000-000000000089'
@@ -40,6 +42,7 @@ const workspaceRequests = []
const projectPatchRequests = []
const inboxAnalyzeRequests = []
const inboxConfirmRequests = []
const aiSessionRequests = []
let expectingProjectPatchError = false
let expectedProjectPatchConsoleErrorCount = 0
let expectingInboxConfirmError = false
@@ -55,6 +58,17 @@ let failNextInboxWorkspaceRefresh = false
let conflictNextInboxConfirm = false
let validationNextInboxConfirm = false
let invalidResponseNextInboxConfirm = false
const controlledAISessions = [
{
id: aiSessionId,
projectId,
title: '项目风险梳理',
context: '只维护会话上下文,不创建正式对象。',
status: 'ready',
createdAt: '2026-07-21T03:00:00Z',
updatedAt: '2026-07-21T03:00:00Z',
},
]
page.on('console', (message) => {
if (message.type() !== 'error') return
if (
@@ -217,6 +231,27 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
await route.fulfill({ json: secondVisualCheckWorkspace })
return
}
if (url.pathname === `/api/v1/projects/${projectId}/ai-sessions` && method === 'GET') {
await new Promise((resolve) => setTimeout(resolve, 120))
await route.fulfill({ json: controlledAISessions })
return
}
if (url.pathname === `/api/v1/projects/${projectId}/ai-sessions` && method === 'POST') {
const input = route.request().postDataJSON()
aiSessionRequests.push(input)
const created = {
id: createdAISessionId,
projectId,
title: input.title,
context: input.context,
status: 'ready',
createdAt: '2026-07-21T03:10:00Z',
updatedAt: '2026-07-21T03:10:00Z',
}
controlledAISessions.unshift(created)
await route.fulfill({ status: 201, json: created })
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))
@@ -487,6 +522,10 @@ await page.locator('.dashboard-button[title="探索"]').click()
await page.waitForTimeout(500)
await page.screenshot({ path: 'test-results/workspace-explore-react-acro-light.png', fullPage: true })
const workspaceExploreMetrics = await collectMetrics()
const workspaceExploreControls = await page.evaluate(() => ({
emptyState: document.querySelector('.workspace-explore-page')?.textContent ?? '',
buttonCount: document.querySelectorAll('.workspace-explore-page button').length,
}))
await page.locator('.project-button').first().click()
await page.waitForTimeout(500)
@@ -924,13 +963,22 @@ if (await inboxChannelButton.count() === 0) {
const channelPageChecks = []
for (const channel of [
{ label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' },
{ label: 'AI 助手', pageClass: 'project-ai-page', expectedHeading: '选择专家,开始对话' },
{ label: 'AI 助手', pageClass: 'project-ai-page', expectedHeading: 'AI 助手' },
{ label: '笔记资料', pageClass: 'project-notes-page', expectedHeading: '笔记资料' },
{ label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' },
{ label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' },
]) {
await page.locator('.channel-button', { hasText: channel.label }).click()
await page.waitForTimeout(250)
if (channel.label === 'AI 助手') {
const aiPage = page.locator('.project-ai-page')
await aiPage.getByText('项目风险梳理', { exact: true }).waitFor({ state: 'visible', timeout: 2000 })
await aiPage.locator('input').fill('新会话视觉检查')
await aiPage.locator('textarea').fill('只创建受控会话入口')
await aiPage.getByRole('button', { name: '创建会话', exact: true }).click()
await aiPage.getByText('新会话视觉检查', { exact: true }).waitFor({ state: 'visible', timeout: 2000 })
await page.screenshot({ path: 'test-results/project-ai-controlled.png', fullPage: true })
}
channelPageChecks.push(await page.evaluate((expected) => {
const pageNode = document.querySelector(`.${expected.pageClass}`)
const heading = pageNode?.querySelector('h2, h3, h4, h5')?.textContent ?? ''
@@ -944,6 +992,13 @@ for (const channel of [
}, channel))
}
const unsupportedControlMetrics = await page.evaluate(() => ({
topbarLabels: [...document.querySelectorAll('.topbar-actions button')].map((button) => button.getAttribute('aria-label') ?? button.textContent?.trim()),
statusbarText: document.querySelector('.statusbar')?.textContent ?? '',
newChannelText: document.querySelector('.project-new-channel-page')?.textContent ?? '',
newChannelButtonCount: document.querySelectorAll('.project-new-channel-page button').length,
}))
await page.getByRole('button', { name: '切换深色模式', exact: true }).click()
await page.screenshot({ path: 'test-results/project-react-acro-dark.png', fullPage: true })
@@ -953,12 +1008,15 @@ await server.close()
console.log(JSON.stringify({
workspaceMetrics,
workspaceExploreMetrics,
workspaceExploreControls,
projectMetrics,
mobileProjectMetrics,
channelSidebarHoverMetrics,
stageHoverMetrics,
channelListHoverMetrics,
channelPageChecks,
unsupportedControlMetrics,
aiSessionRequests,
errors,
}, null, 2))
@@ -1038,6 +1096,8 @@ if (
}
if (!workspaceExploreMetrics.workspaceExploreButtonActive) failures.push('expected fixed workspace explore button to become active when selected')
if (!workspaceExploreControls.emptyState.includes('暂未开放')) failures.push('workspace explore must present the unsupported feature as 暂未开放')
if (workspaceExploreControls.buttonCount !== 0) failures.push(`workspace explore must not render active controls, got ${workspaceExploreControls.buttonCount}`)
if (workspaceExploreMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace explore page')
if (workspaceExploreMetrics.channelSidebar) failures.push(`expected workspace explore to hide channel sidebar, got ${JSON.stringify(workspaceExploreMetrics.channelSidebar)}`)
if (workspaceExploreMetrics.inspector) failures.push(`expected workspace explore to hide inspector, got ${JSON.stringify(workspaceExploreMetrics.inspector)}`)
@@ -1110,6 +1170,23 @@ for (const check of channelPageChecks) {
}
}
if (aiSessionRequests.length !== 1) failures.push(`expected one controlled AI session request, got ${aiSessionRequests.length}`)
if (aiSessionRequests.length === 1) {
const requestKeys = Object.keys(aiSessionRequests[0]).sort()
if (JSON.stringify(requestKeys) !== JSON.stringify(['context', 'title'])) {
failures.push(`AI session create must send only title/context, got ${JSON.stringify(requestKeys)}`)
}
}
if (unsupportedControlMetrics.topbarLabels.some((label) => label?.includes('停靠'))) {
failures.push(`topbar must not expose dock controls, got ${JSON.stringify(unsupportedControlMetrics.topbarLabels)}`)
}
if (/升级|支付|AI 空闲|GB 可用/.test(unsupportedControlMetrics.statusbarText)) {
failures.push(`statusbar contains unsupported product state: ${unsupportedControlMetrics.statusbarText}`)
}
if (!unsupportedControlMetrics.newChannelText.includes('暂未开放') || unsupportedControlMetrics.newChannelButtonCount !== 0) {
failures.push(`new channel must be a non-interactive 暂未开放 state, got ${JSON.stringify(unsupportedControlMetrics)}`)
}
if (failures.length) {
throw new Error(failures.join('\n'))
}