L2考核落地:人才测评整合为L2考核track,新增查重初筛与e2e验证
- 新增 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 项全绿
This commit is contained in:
@@ -29,16 +29,19 @@ export default async function globalSetup() {
|
||||
PORT: '3002',
|
||||
AUTH_PASSWORD: 'test123',
|
||||
ADMIN_TEST_TOKEN: 'true',
|
||||
// 隔离 e2e 测试库,避免污染真实评审数据
|
||||
DB_PATH: path.join(SERVER_DIR, 'data', 'e2e-test.db'),
|
||||
// 测试模式下 writeEnvVar 只更新进程内 env,避免改密用例污染真实 server/.env
|
||||
NODE_ENV: 'test',
|
||||
} as Record<string, string>;
|
||||
|
||||
const shell = process.platform === 'win32' ? { shell: 'cmd.exe' } : {};
|
||||
console.log('[setup] Starting backend server...');
|
||||
const server = spawn('npx.cmd', ['tsx', 'src/index.ts'], {
|
||||
cwd: SERVER_DIR,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env,
|
||||
shell: true,
|
||||
...shell,
|
||||
});
|
||||
server.stdout.on('data', (d: Buffer) => process.stdout.write(`[server] ${d}`));
|
||||
server.stderr.on('data', (d: Buffer) => process.stderr.write(`[server-err] ${d}`));
|
||||
@@ -49,7 +52,7 @@ export default async function globalSetup() {
|
||||
cwd: WEB_DIR,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env } as Record<string, string>,
|
||||
shell: true,
|
||||
...shell,
|
||||
});
|
||||
frontend.stdout.on('data', (d: Buffer) => process.stdout.write(`[web] ${d}`));
|
||||
frontend.stderr.on('data', (d: Buffer) => process.stderr.write(`[web-err] ${d}`));
|
||||
|
||||
@@ -5,9 +5,9 @@ import { test, expect, type Page, type APIRequestContext } from '@playwright/tes
|
||||
// 覆盖对象:
|
||||
// 1. 模板维度可声明「文件关键词」→ DIM_FILE_FILTERS 硬编码仅兑底
|
||||
// 2. 模板 content 成为评审指南第一来源(评审要点完整保留并透传)
|
||||
// 3. 人才测评 L2「功能完整性」盲区修复(isBuildRelatedDim)
|
||||
// 3. L2考核「功能完整性」构建封顶维度识别(isBuildRelatedDim)
|
||||
// 4. 阈值收拢命名常量后 pass_line / 及格线行为回归
|
||||
// 5. question_id 分组过滤(快照只含 common + 所选 Qn)
|
||||
// 5. L2考核 选题(selected_topic)与难度赋分 cap
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
const PASSWORD = 'test123';
|
||||
@@ -107,20 +107,21 @@ test.describe('项目创建与赛道自动标准', () => {
|
||||
await request.fetch(`${BASE}/projects/${pid}?force=true`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
|
||||
});
|
||||
|
||||
test('1.3 人才测评自动生成含「功能完整性」标准', async ({ request }) => {
|
||||
test('1.3 L2考核自动生成 7 维标准(功能完整性 30)', async ({ request }) => {
|
||||
const token = await apiLogin(request);
|
||||
const pid = await apiCreateProject(request, token, `人才-${UNIQUE}`, '人才测评');
|
||||
const pid = await apiCreateProject(request, token, `L2-${UNIQUE}`, 'L2考核');
|
||||
const r = await request.fetch(`${BASE}/projects/${pid}/standards`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const standards = await r.json();
|
||||
expect(standards).toHaveLength(1);
|
||||
const dims = standards[0].dimensions;
|
||||
expect(dims.some((d: any) => d.name === '功能完整性' && d.maxScore === 40)).toBe(true);
|
||||
expect(dims.some((d: any) => d.name === '功能完整性' && d.maxScore === 30)).toBe(true);
|
||||
|
||||
// 共通维度合计 100(L2 及格线 = 60)
|
||||
const commonTotal = dims.filter((d: any) => (d.group || 'common') === 'common').reduce((s: number, d: any) => s + d.maxScore, 0);
|
||||
expect(commonTotal).toBe(100);
|
||||
// 7 维合计 100(L2 合格线 = 60)
|
||||
const total = dims.reduce((s: number, d: any) => s + d.maxScore, 0);
|
||||
expect(total).toBe(100);
|
||||
expect(dims).toHaveLength(7);
|
||||
await request.fetch(`${BASE}/projects/${pid}?force=true`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
|
||||
});
|
||||
});
|
||||
@@ -273,50 +274,51 @@ test.describe('条目快照透传', () => {
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// 4. 人才测评:question_id 分组过滤 + 功能完整性
|
||||
// 4. L2考核:选题(selected_topic)+ 难度赋分 cap
|
||||
// ══════════════════════════════════════
|
||||
|
||||
test.describe('人才测评 question_id 分组', () => {
|
||||
test.describe('L2考核 选题与 cap', () => {
|
||||
let pid = '';
|
||||
let token = '';
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
token = await apiLogin(request);
|
||||
pid = await apiCreateProject(request, token, `人才Q-${UNIQUE}`, '人才测评');
|
||||
pid = await apiCreateProject(request, token, `L2Q-${UNIQUE}`, 'L2考核');
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
await request.fetch(`${BASE}/projects/${pid}?force=true`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
|
||||
});
|
||||
|
||||
test('4.1 建条目未选题目被拒绝(400)', async ({ request }) => {
|
||||
test('4.1 无效选题被拒绝(400)', async ({ request }) => {
|
||||
const r = await request.fetch(`${BASE}/projects/${pid}/entries`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
data: { title: '无题目', repo_url: `https://noq-${UNIQUE}.git` },
|
||||
data: { title: '无效选题', repo_url: `https://noid-${UNIQUE}.git`, selected_topic: '99' },
|
||||
});
|
||||
expect(r.status()).toBe(400);
|
||||
});
|
||||
|
||||
test('4.2 Q2 条目快照包含 common 与 [Q2] 维度(完整标准,评审时按题目过滤)', async ({ request }) => {
|
||||
const entry = await apiCreateEntry(request, token, pid, 'Q2选手', { question_id: 'Q2' });
|
||||
const dims = JSON.parse(entry.standard_snapshot);
|
||||
|
||||
const groups = new Set(dims.map((d: any) => d.group || 'common'));
|
||||
expect(groups.has('common')).toBe(true);
|
||||
expect(groups.has('Q2')).toBe(true);
|
||||
// 快照保留完整标准(含其他题目维度),评审时按 question_id 过滤
|
||||
expect(groups.has('Q3')).toBe(true);
|
||||
|
||||
// 功能完整性在 common 组且是构建封顶维度
|
||||
const fnDim = dims.find((d: any) => d.name === '功能完整性');
|
||||
expect(fnDim).toBeDefined();
|
||||
expect(fnDim.maxScore).toBe(40);
|
||||
test('4.2 命题 11(★★)→ cap=100;自选题 → cap=100', async ({ request }) => {
|
||||
const e1 = await apiCreateEntry(request, token, pid, '题11选手', { selected_topic: '11' });
|
||||
expect(e1.selected_topic).toBe('11');
|
||||
expect(e1.max_score_cap).toBe(100);
|
||||
const e2 = await apiCreateEntry(request, token, pid, '自选选手', { selected_topic: 'self' });
|
||||
expect(e2.max_score_cap).toBe(100);
|
||||
});
|
||||
|
||||
test('4.3 pass_line = 共通维度 60% = 60', async ({ request }) => {
|
||||
const entry = await apiCreateEntry(request, token, pid, 'Q1选手', { question_id: 'Q1' });
|
||||
expect(entry.pass_line).toBe(60);
|
||||
test('4.3 命题 03(★★★★)→ cap=110(难度赋分封顶)', async ({ request }) => {
|
||||
const entry = await apiCreateEntry(request, token, pid, '题03选手', { selected_topic: '03' });
|
||||
expect(entry.max_score_cap).toBe(110);
|
||||
});
|
||||
|
||||
test('4.4 快照为 7 维标准(全 common,功能完整性 30)', async ({ request }) => {
|
||||
const entry = await apiCreateEntry(request, token, pid, '快照选手', { selected_topic: '01' });
|
||||
const dims = JSON.parse(entry.standard_snapshot);
|
||||
expect(dims).toHaveLength(7);
|
||||
expect(dims.every((d: any) => (d.group || 'common') === 'common')).toBe(true);
|
||||
const fnDim = dims.find((d: any) => d.name === '功能完整性');
|
||||
expect(fnDim.maxScore).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -175,55 +175,62 @@ test.describe('PDF 下载', () => {
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// 4. 人才测评 L2/L3 分表展示
|
||||
// 4. L2考核:条目列表合格/不合格徽章展示
|
||||
// ══════════════════════════════════════
|
||||
|
||||
test.describe('人才测评 L2/L3 分表', () => {
|
||||
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: `l2l3-${UNIQUE}`, track: '人才测评' },
|
||||
headers: { Authorization: `Bearer ${token}` }, data: { name: `l2badge-${UNIQUE}`, track: 'L2考核' },
|
||||
});
|
||||
pid = (await proj.json()).id;
|
||||
// 删除自动标准,替换为自定义 Q2 标准
|
||||
const list = await request.get(`http://localhost:3002/api/projects/${pid}/standards`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
for (const s of await list.json()) {
|
||||
await request.delete(`http://localhost:3002/api/projects/${pid}/standards/${s.id}`, { headers: { Authorization: `Bearer ${token}` } });
|
||||
}
|
||||
await request.post(`http://localhost:3002/api/projects/${pid}/standards`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { name: 'l2-std', category_tag: '人才测评', content: '## 功能完整性(40分)\n## 设计文档(10分)\n## [Q2] LLM生成问卷(15分)' },
|
||||
});
|
||||
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
data: { title: 'l2l3-entry', repo_url: `file://C:\\l2-${UNIQUE}`, question_id: 'Q2' },
|
||||
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: 40, maxScore: 40, group: 'common' },
|
||||
{ name: '设计文档', score: 10, maxScore: 10, group: 'common' },
|
||||
{ name: 'LLM生成问卷', score: 15, maxScore: 15, group: 'Q2' },
|
||||
{ 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共通 与 L3追加 分表', async ({ page }) => {
|
||||
test('列表展示「合格」徽章(L2考核 判定)', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`/project/${pid}`);
|
||||
await page.click('.title-cell');
|
||||
await expect(page.getByText('L2共通评分')).toBeVisible();
|
||||
await expect(page.getByText('L3追加评分')).toBeVisible();
|
||||
await expect(page.getByText('合格', { exact: true })).toBeVisible();
|
||||
// 条目还展示选题徽章
|
||||
await expect(page.getByText('命题 11')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user