feat: align controlled AI sessions and MVP controls
This commit is contained in:
@@ -24,6 +24,7 @@ const requiredFiles = [
|
||||
'src/api/mappers.tsx',
|
||||
'src/api/search.ts',
|
||||
'src/api/inbox.ts',
|
||||
'src/api/ai.ts',
|
||||
'scripts/api-client.test.mjs',
|
||||
]
|
||||
|
||||
@@ -59,7 +60,7 @@ if (existsSync('src/app/App.tsx')) {
|
||||
}
|
||||
|
||||
const apiFiles = existsSync('src/api')
|
||||
? ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts']
|
||||
? ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts', 'ai.ts']
|
||||
.map((name) => `src/api/${name}`)
|
||||
.filter((file) => existsSync(file))
|
||||
: []
|
||||
@@ -112,6 +113,48 @@ if (!inboxSource.includes('body: { suggestionIds }')) failures.push('inbox confi
|
||||
const inboxPageSource = existsSync('src/pages/projects/project-inbox.tsx') ? readFileSync('src/pages/projects/project-inbox.tsx', 'utf8') : ''
|
||||
if (inboxPageSource && !inboxPageSource.includes('确认创建')) failures.push('project inbox must expose the single confirmation action')
|
||||
|
||||
const aiApiSource = existsSync('src/api/ai.ts') ? readFileSync('src/api/ai.ts', 'utf8') : ''
|
||||
for (const required of ['/api/v1/projects/', '/ai-sessions', 'listAISessions', 'createAISession']) {
|
||||
if (!aiApiSource.includes(required)) failures.push(`AI API must include ${required}`)
|
||||
}
|
||||
if (/\b(?:task|note|source)Id\b/.test(aiApiSource)) failures.push('AI session responses must not expose automatic formal object IDs')
|
||||
|
||||
const aiPageSource = existsSync('src/pages/projects/project-ai.tsx') ? readFileSync('src/pages/projects/project-ai.tsx', 'utf8') : ''
|
||||
for (const required of ['AI 助手', '创建会话', 'loading', 'error']) {
|
||||
if (!aiPageSource.includes(required)) failures.push(`project AI page must include ${required}`)
|
||||
}
|
||||
for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息', 'IconAttachment', 'agent-send-button']) {
|
||||
if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains unsupported chat control ${forbidden}`)
|
||||
}
|
||||
|
||||
const unsupportedControls = [
|
||||
{
|
||||
file: 'src/pages/workspace-explore.tsx',
|
||||
required: '暂未开放',
|
||||
forbidden: ['同步数据源', '添加数据源', 'IconRefresh', 'IconEdit', 'IconDelete'],
|
||||
},
|
||||
{
|
||||
file: 'src/pages/projects/project-new-channel.tsx',
|
||||
required: '暂未开放',
|
||||
forbidden: ['保存频道', '<Input', '<Select', '<TextArea'],
|
||||
},
|
||||
{
|
||||
file: 'src/pages/projects/project-statusbar.tsx',
|
||||
forbidden: ['升级', '支付', 'billing-plan', '152GB', 'AI 空闲'],
|
||||
},
|
||||
{
|
||||
file: 'src/pages/projects/project-topbar.tsx',
|
||||
forbidden: ['停靠左边', '停靠右边', 'DockIcon', 'isDesktopRuntime'],
|
||||
},
|
||||
]
|
||||
for (const check of unsupportedControls) {
|
||||
const source = existsSync(check.file) ? readFileSync(check.file, 'utf8') : ''
|
||||
if (check.required && !source.includes(check.required)) failures.push(`${check.file} must show ${check.required}`)
|
||||
for (const forbidden of check.forbidden) {
|
||||
if (source.includes(forbidden)) failures.push(`${check.file} contains unsupported control ${forbidden}`)
|
||||
}
|
||||
}
|
||||
|
||||
const html = readFileSync('index.html', 'utf8')
|
||||
if (!html.includes('<html lang="zh-CN">')) failures.push('index language must be zh-CN')
|
||||
if (!html.includes('<title>森林AI</title>')) failures.push('document title must be 森林AI')
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user