refactor(web): consume api v1 contracts
This commit is contained in:
@@ -21,6 +21,8 @@ const requiredFiles = [
|
||||
'src/api/client.ts',
|
||||
'src/api/projects.ts',
|
||||
'src/api/mappers.tsx',
|
||||
'src/api/search.ts',
|
||||
'src/api/inbox.ts',
|
||||
]
|
||||
|
||||
const failures = requiredFiles.filter((file) => !existsSync(file)).map((file) => `missing ${file}`)
|
||||
@@ -54,6 +56,47 @@ if (existsSync('src/app/App.tsx')) {
|
||||
}
|
||||
}
|
||||
|
||||
const apiFiles = existsSync('src/api')
|
||||
? ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts']
|
||||
.map((name) => `src/api/${name}`)
|
||||
.filter((file) => existsSync(file))
|
||||
: []
|
||||
const apiSource = apiFiles.map((file) => readFileSync(file, 'utf8')).join('\n')
|
||||
for (const forbidden of [
|
||||
{ pattern: /\bID\s*\?\s*:/, label: 'optional PascalCase ID compatibility field' },
|
||||
{ pattern: /\bName\s*\?\s*:/, label: 'optional PascalCase Name compatibility field' },
|
||||
{ pattern: /['"`]\/api\/projects/, label: 'legacy /api/projects path' },
|
||||
{
|
||||
pattern: /\b(?:project|task|inbox)Id\s*\??\s*:\s*(?:string\s*\|\s*)?number\b/i,
|
||||
label: 'numeric project/task/inbox identity',
|
||||
},
|
||||
]) {
|
||||
if (forbidden.pattern.test(apiSource)) failures.push(`src/api contains ${forbidden.label}`)
|
||||
}
|
||||
|
||||
const clientSource = existsSync('src/api/client.ts') ? readFileSync('src/api/client.ts', 'utf8') : ''
|
||||
if (!clientSource.includes("'/api/v1/auth/login'")) failures.push('login must use /api/v1/auth/login')
|
||||
if (!clientSource.includes('export class ApiError extends Error')) failures.push('client must export ApiError')
|
||||
|
||||
const projectsSource = existsSync('src/api/projects.ts') ? readFileSync('src/api/projects.ts', 'utf8') : ''
|
||||
if (!projectsSource.includes("'/api/v1/projects'")) failures.push('project API must use /api/v1/projects')
|
||||
if (/['"`]\/api\/(?!v1\/)/.test(apiSource)) failures.push('src/api paths must use the /api/v1 prefix')
|
||||
|
||||
for (const match of apiSource.matchAll(/export type (\w+DTO)\s*=\s*\{([\s\S]*?)\n\}/g)) {
|
||||
const [, name, body] = match
|
||||
if (/^\s*\w+\s*\?\s*:/m.test(body)) failures.push(`${name} fields must be required`)
|
||||
if (/^\s*[A-Z]\w*\s*:/m.test(body)) failures.push(`${name} fields must use camelCase`)
|
||||
if (/^\s*(?:id|\w+Id)\s*:\s*number\b/im.test(body)) failures.push(`${name} identities must be strings`)
|
||||
}
|
||||
|
||||
const searchSource = existsSync('src/api/search.ts') ? readFileSync('src/api/search.ts', 'utf8') : ''
|
||||
if (searchSource && !searchSource.includes("'/api/v1/search'")) failures.push('search API must use /api/v1/search')
|
||||
|
||||
const inboxSource = existsSync('src/api/inbox.ts') ? readFileSync('src/api/inbox.ts', 'utf8') : ''
|
||||
for (const path of ['/api/v1/projects/', '/api/v1/inbox/']) {
|
||||
if (inboxSource && !inboxSource.includes(path)) failures.push(`inbox API must use ${path}`)
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
@@ -18,13 +18,14 @@ const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1024 }, deviceScaleFactor: 1 })
|
||||
const errors = []
|
||||
const failures = []
|
||||
const projectId = '019b0000-0000-7000-8000-000000000001'
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') errors.push(message.text())
|
||||
})
|
||||
|
||||
const visualCheckWorkspace = {
|
||||
project: {
|
||||
id: 1,
|
||||
id: projectId,
|
||||
name: '森林项目',
|
||||
identifier: 'forest',
|
||||
icon: '森',
|
||||
@@ -34,15 +35,15 @@ const visualCheckWorkspace = {
|
||||
unreadCount: 2,
|
||||
},
|
||||
channels: [
|
||||
{ id: 'overview', projectID: 1, type: 'overview', title: '概况', icon: 'home', sortOrder: 0 },
|
||||
{ id: 'tasks', projectID: 1, type: 'tasks', title: '工作计划', icon: 'list', count: 1, sortOrder: 1 },
|
||||
{ id: 'ai', projectID: 1, type: 'ai_sessions', title: 'AI 助手', icon: 'robot', sortOrder: 2 },
|
||||
{ id: 'notes', projectID: 1, type: 'notes_sources', title: '笔记资料', icon: 'file', sortOrder: 3 },
|
||||
{ id: 'cron', projectID: 1, type: 'cron', title: '计划任务', icon: 'clock', sortOrder: 4 },
|
||||
{ id: 'overview', projectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
||||
{ id: 'tasks', projectId, type: 'tasks', title: '工作计划', icon: 'list', count: 1, url: '', sortOrder: 1 },
|
||||
{ id: 'ai', projectId, type: 'ai_sessions', title: 'AI 助手', icon: 'robot', count: 0, url: '', sortOrder: 2 },
|
||||
{ id: 'notes', projectId, type: 'notes_sources', title: '笔记资料', icon: 'file', count: 0, url: '', sortOrder: 3 },
|
||||
{ id: 'cron', projectId, type: 'cron', title: '计划任务', icon: 'clock', count: 0, url: '', sortOrder: 4 },
|
||||
],
|
||||
tags: ['all', '产品'],
|
||||
tags: [{ id: '019b0000-0000-7000-8000-000000000002', name: '产品' }],
|
||||
recentSessions: [
|
||||
{ id: 'session-1', title: '移动导航会话', summary: '检查抽屉关闭行为', updatedAt: '今天 10:00', references: [] },
|
||||
{ id: 'session-1', projectId, title: '移动导航会话', summary: '检查抽屉关闭行为', updatedAt: '2026-07-21T02:00:00Z', references: [] },
|
||||
],
|
||||
inbox: [],
|
||||
tasks: [],
|
||||
@@ -51,25 +52,25 @@ const visualCheckWorkspace = {
|
||||
cronPlans: [],
|
||||
}
|
||||
|
||||
await page.route('http://localhost:9150/api/**', async (route) => {
|
||||
await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.pathname === '/api/status') {
|
||||
await route.fulfill({ json: { timestamp_ms: Date.now() } })
|
||||
if (url.pathname === '/api/v1/status') {
|
||||
await route.fulfill({ json: { timestamp: new Date().toISOString() } })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/auth/login') {
|
||||
if (url.pathname === '/api/v1/auth/login') {
|
||||
await route.fulfill({ json: { token: 'visual-check-token' } })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/projects') {
|
||||
await route.fulfill({ json: [{ ID: 1, Name: visualCheckWorkspace.project.name }] })
|
||||
if (url.pathname === '/api/v1/projects') {
|
||||
await route.fulfill({ json: [visualCheckWorkspace.project] })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/projects/1/workspace') {
|
||||
if (url.pathname === `/api/v1/projects/${projectId}/workspace`) {
|
||||
await route.fulfill({ json: visualCheckWorkspace })
|
||||
return
|
||||
}
|
||||
await route.fulfill({ status: 404, json: { error: `unmocked visual check route: ${url.pathname}` } })
|
||||
await route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '视觉检查未配置该接口' } } })
|
||||
})
|
||||
|
||||
const collectLoginMetrics = async () => page.evaluate(() => {
|
||||
|
||||
Reference in New Issue
Block a user