- 新增 L2考核 track(7维标准/100分):选题难度赋分cap、功能完整性地板线、合格判定 - 人才测评整合进 L2考核:移除 L2/L3 两级认定与 question_id 机制 - 新增 l2-topics 选题元数据服务与 config/l2-topics.json(11命题题+自选题) - 新增查重初筛 plagiarism-detect(MD5精确比对+归一化相似度+提交行为,仅告警) - 修复 L2 维度 DIM_FILE_FILTERS 缺失导致 AI 协作记录证据漏喂 - e2e:修复 Windows spawn、DB 隔离,L2 用例 27 项全绿
237 lines
11 KiB
TypeScript
237 lines
11 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
||
|
||
const PASSWORD = 'test123';
|
||
const UNIQUE = Date.now().toString(36);
|
||
|
||
async function login(page: Page) {
|
||
await page.goto('/login');
|
||
await page.fill('input[type="password"]', PASSWORD);
|
||
await page.click('button[type="submit"]');
|
||
await page.waitForURL('/');
|
||
}
|
||
|
||
async function apiToken(request: any): Promise<string> {
|
||
const res = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
|
||
const { token } = await res.json();
|
||
return token;
|
||
}
|
||
|
||
// ══════════════════════════════════════
|
||
// 1. 成果物 Tab(DeliverablesView)
|
||
// ══════════════════════════════════════
|
||
|
||
test.describe('成果物 Tab', () => {
|
||
let pid = '';
|
||
let token = '';
|
||
|
||
test.beforeAll(async ({ request }) => {
|
||
token = await apiToken(request);
|
||
const proj = await request.post('http://localhost:3002/api/projects', {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: `deliv-${UNIQUE}`, track: '赛道一' },
|
||
});
|
||
pid = (await proj.json()).id;
|
||
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: 'd-std', content: '## 场景价值(8分)' },
|
||
});
|
||
await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { title: 'deliv-entry', repo_url: `file://C:\\deliv-${UNIQUE}` },
|
||
});
|
||
});
|
||
|
||
test.afterAll(async ({ request }) => {
|
||
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
|
||
});
|
||
|
||
test('初始化一覧 → 勾选 → 提交率更新 → CSV 下载', async ({ page }) => {
|
||
await login(page);
|
||
await page.goto(`/project/${pid}`);
|
||
await page.getByRole('button', { name: '成果物' }).click();
|
||
await expect(page.getByText('成果物确认')).toBeVisible();
|
||
|
||
// 初始化一覧
|
||
await page.getByRole('button', { name: '初始化一覧' }).click();
|
||
await expect(page.locator('.entry-table input[type="checkbox"]').first()).toBeVisible();
|
||
|
||
// 勾选第一个成果物 → 提交率 +1(经 API 验证)
|
||
const before = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/deliverables/summary`, {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
});
|
||
const beforeData = await before.json();
|
||
await page.locator('.entry-table input[type="checkbox"]').first().check();
|
||
await expect.poll(async () => {
|
||
const r = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/deliverables/summary`, {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
});
|
||
return (await r.json()).totalSubmitted;
|
||
}).toBe(beforeData.totalSubmitted + 1);
|
||
|
||
// CSV 下载
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: '下载CSV' }).click();
|
||
const download = await downloadPromise;
|
||
expect(download.suggestedFilename()).toContain('deliverables');
|
||
});
|
||
});
|
||
|
||
// ══════════════════════════════════════
|
||
// 2. 评审进度时间线(DetailPanel,D7)
|
||
// ══════════════════════════════════════
|
||
|
||
test.describe('评审进度时间线', () => {
|
||
let pid = ''; let eid = ''; let token = '';
|
||
|
||
test.beforeAll(async ({ request }) => {
|
||
token = await apiToken(request);
|
||
const proj = await request.post('http://localhost:3002/api/projects', {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: `prog-${UNIQUE}`, track: '赛道一' },
|
||
});
|
||
pid = (await proj.json()).id;
|
||
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: 'p-std', content: '## 场景价值(8分)' },
|
||
});
|
||
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { title: 'prog-entry', repo_url: `file://C:\\prog-${UNIQUE}` },
|
||
});
|
||
eid = (await entry.json()).id;
|
||
// 启动评审(外部路径 → 秒级 clone_fail),写入 progress_log
|
||
await request.post(`http://localhost:3002/api/projects/${pid}/entries/${eid}/start`, {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
});
|
||
await expect.poll(async () => {
|
||
const r = await request.get(`http://localhost:3002/api/projects/${pid}/entries/${eid}`, {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
});
|
||
return (await r.json()).status;
|
||
}).toBe('clone_fail');
|
||
});
|
||
|
||
test.afterAll(async ({ request }) => {
|
||
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
|
||
});
|
||
|
||
test('详情面板显示评审进度时间线(含克隆步骤)', async ({ page }) => {
|
||
await login(page);
|
||
await page.goto(`/project/${pid}`);
|
||
await page.click('.title-cell');
|
||
const progress = page.locator('details', { has: page.getByText('评审进度') });
|
||
await expect(progress).toBeVisible();
|
||
await expect(progress).toContainText('不允许克隆外部路径');
|
||
});
|
||
});
|
||
|
||
// ══════════════════════════════════════
|
||
// 3. PDF 下载(单条目报告 / 汇总报告)
|
||
// ══════════════════════════════════════
|
||
|
||
test.describe('PDF 下载', () => {
|
||
let pid = ''; let eid = ''; let token = '';
|
||
|
||
test.beforeAll(async ({ request }) => {
|
||
token = await apiToken(request);
|
||
const proj = await request.post('http://localhost:3002/api/projects', {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: `pdf-${UNIQUE}`, track: '赛道一' },
|
||
});
|
||
pid = (await proj.json()).id;
|
||
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: 'pdf-std', content: '## 架构设计(10分)' },
|
||
});
|
||
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { title: 'pdf-entry', repo_url: `file://C:\\pdf-${UNIQUE}` },
|
||
});
|
||
eid = (await entry.json()).id;
|
||
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { dimensions: [{ name: '架构设计', score: 8, maxScore: 10 }] },
|
||
});
|
||
});
|
||
|
||
test.afterAll(async ({ request }) => {
|
||
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
|
||
});
|
||
|
||
test('单条目报告 PDF 下载', async ({ page }) => {
|
||
await login(page);
|
||
await page.goto(`/project/${pid}`);
|
||
await page.click('.title-cell');
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: '下载报告' }).click();
|
||
const download = await downloadPromise;
|
||
// 服务端 Content-Disposition 中文文件名在部分平台会被搅乱,这里只断言"触发了 .pdf 下载"
|
||
expect(download.suggestedFilename().toLowerCase()).toContain('.pdf');
|
||
});
|
||
|
||
test('汇总报告 PDF 下载', async ({ page }) => {
|
||
await login(page);
|
||
await page.goto(`/project/${pid}`);
|
||
await page.getByRole('button', { name: '汇总' }).click();
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: '下载汇总PDF' }).click();
|
||
const download = await downloadPromise;
|
||
expect(download.suggestedFilename().toLowerCase()).toContain('.pdf');
|
||
});
|
||
});
|
||
|
||
// ══════════════════════════════════════
|
||
// 4. L2考核:条目列表合格/不合格徽章展示
|
||
// ══════════════════════════════════════
|
||
|
||
test.describe('L2考核 合格/不合格徽章', () => {
|
||
let pid = ''; let eid = ''; let token = '';
|
||
|
||
test.beforeAll(async ({ request }) => {
|
||
token = await apiToken(request);
|
||
const proj = await request.post('http://localhost:3002/api/projects', {
|
||
headers: { Authorization: `Bearer ${token}` }, data: { name: `l2badge-${UNIQUE}`, track: 'L2考核' },
|
||
});
|
||
pid = (await proj.json()).id;
|
||
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { title: 'l2-entry', repo_url: `file://C:\\l2-${UNIQUE}`, selected_topic: '11' },
|
||
});
|
||
eid = (await entry.json()).id;
|
||
// 播种评审结果:总分 70 ≥60 且 功能完整性 25 ≥15 → 合格
|
||
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/force-review`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: {
|
||
dimensions: [
|
||
{ name: '功能完整性', score: 25, maxScore: 30 },
|
||
{ name: '设计文档', score: 8, maxScore: 10 },
|
||
{ name: '测试用例与测试结果', score: 7, maxScore: 10 },
|
||
{ name: 'AI协作过程记录', score: 10, maxScore: 15 },
|
||
{ name: '技术选型与范式运用', score: 10, maxScore: 15 },
|
||
{ name: '代码质量+README', score: 6, maxScore: 10 },
|
||
{ name: '业务场景理解与需求分析', score: 4, maxScore: 10 },
|
||
],
|
||
},
|
||
});
|
||
// 触发合格判定(PUT /report 重算 final_level)
|
||
await request.put(`http://localhost:3002/api/projects/${pid}/entries/${eid}/report`, {
|
||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
data: { dimensions: [
|
||
{ name: '功能完整性', score: 25, maxScore: 30 },
|
||
{ name: '设计文档', score: 8, maxScore: 10 },
|
||
{ name: '测试用例与测试结果', score: 7, maxScore: 10 },
|
||
{ name: 'AI协作过程记录', score: 10, maxScore: 15 },
|
||
{ name: '技术选型与范式运用', score: 10, maxScore: 15 },
|
||
{ name: '代码质量+README', score: 6, maxScore: 10 },
|
||
{ name: '业务场景理解与需求分析', score: 4, maxScore: 10 },
|
||
] },
|
||
});
|
||
});
|
||
|
||
test.afterAll(async ({ request }) => {
|
||
await request.delete(`http://localhost:3002/api/projects/${pid}?force=true`, { headers: { Authorization: `Bearer ${token}` } });
|
||
});
|
||
|
||
test('列表展示「合格」徽章(L2考核 判定)', async ({ page }) => {
|
||
await login(page);
|
||
await page.goto(`/project/${pid}`);
|
||
await expect(page.getByText('合格', { exact: true })).toBeVisible();
|
||
// 条目还展示选题徽章
|
||
await expect(page.getByText('命题 11')).toBeVisible();
|
||
});
|
||
});
|