// 问鼎 · 重要证明 —— 单测。钉三条铁律,一条都不许松: // ① 递交=待审核,【对定品零影响】(自评上传绝不自动生效); // ② 只有【超管审核 verified】才进 effortBonus(用户不能自评生效·防冒充); // ③ 生效力度(level)由审核给,不认自评 claimLevel。 // 另钉:属主隔离(IDOR)、超管门(非超管审核=403)、防刷(条数上限)、撤回只能撤没审的、零游戏味文案墙。 // // 纯函数层恒跑(verifiedLevel/effortBonus/office5d 效果);DB 集成层需本地 MongoDB,连不上整组 skip。 process.env.NODE_ENV = 'test'; process.env.MONGO_DB = process.env.MONGO_DB_TEST || 'digital_me_test'; import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; import express from 'express'; import http from 'node:http'; import { verifiedLevel, MAX_ITEMS, parseVerdict, verifyZhengming } from '../src/services/zhengming.js'; import { effortBonus, computeOffice5D, zhengmingRating } from '../src/office5d.js'; import { computeAxes } from '../src/dynasties.js'; import { subsFromAttrs } from '../src/attributes.js'; import { assertNoGameMechanics } from '../src/decisions.js'; // ══════════════════════════════════════════════════════════════════════════ // 纯函数层(无 DB,恒跑) // ══════════════════════════════════════════════════════════════════════════ test('铁律③ verifiedLevel:只认 status===verified,取已验证里最高 level', () => { assert.equal(verifiedLevel(null), 0, '空 twin → 0'); assert.equal(verifiedLevel({}), 0, '无 zhengming → 0'); assert.equal(verifiedLevel({ zhengming: [] }), 0, '空数组 → 0'); // 待审核/驳回/自评 level 一律不算 —— 这是防刷的全部 assert.equal(verifiedLevel({ zhengming: [{ status: 'pending', level: 3 }] }), 0, '待审核 level 不算'); assert.equal(verifiedLevel({ zhengming: [{ status: 'rejected', level: 3 }] }), 0, '驳回不算'); // 只有 verified 才算,取最高 assert.equal(verifiedLevel({ zhengming: [ { status: 'verified', level: 1 }, { status: 'verified', level: 3 }, { status: 'pending', level: 3 }, ] }), 3, '取已验证里最高'); // level 越界夹紧 assert.equal(verifiedLevel({ zhengming: [{ status: 'verified', level: 99 }] }), 3, 'level 上限 3'); assert.equal(verifiedLevel({ zhengming: [{ status: 'verified', level: -5 }] }), 0, 'level 下限 0'); }); test('问鼎独立·zhengmingRating:已核验证明 0..3【自成一维,不再进改命 effortBonus】', () => { // ★ 2026-07-24 拆维后:改命(effortBonus)=打卡/王/劳/知识,【不含证明】 assert.equal(effortBonus({ zhengming: 3 }), 0, '问鼎不再进改命 effortBonus'); assert.equal(effortBonus({ gaimingPoints: 6 }), 2, '改命·打卡档满 +2'); assert.equal(effortBonus({ gaimingPoints: 6, lao: 30 }), 3, '改命仍由打卡+熬时间等驱动,满 3'); // zhengmingRating:只认核定 level 0..3(自评/待审核=0,同 verifiedLevel 口径) assert.equal(zhengmingRating({}), 0, '无 → 0'); assert.equal(zhengmingRating({ zhengming: 1 }), 1); assert.equal(zhengmingRating({ zhengming: 3 }), 3); assert.equal(zhengmingRating({ zhengming: 99 }), 3, '上限 3'); assert.equal(zhengmingRating({ zhengming: -5 }), 0, '下限 0'); }); test('问鼎独立·office5d:证明抬【问鼎(zheng)】这一维、不动改命;★无死维:改命满了问鼎照样推得动 tier', () => { const base = { professionLei: 'wen', axes: computeAxes(subsFromAttrs(['poetic', 'story', 'vivid'], ['warm'])), credentials: { zhicheng: 2 }, jiashi: 1, wang: 0, lao: 0, knowledge: 0, }; const without = computeOffice5D({ ...base, zhengming: 0 }, 'tang'); const withZ = computeOffice5D({ ...base, zhengming: 3 }, 'tang'); // 问鼎(zheng)品第:有证明的更高;★ 改命(gai)品第不受它影响(已拆开、不再共用) assert.ok(withZ.parts.pins.zheng.rank > without.parts.pins.zheng.rank, '证明抬高问鼎品第'); assert.equal(withZ.parts.pins.zheng.rank, 3); assert.equal(without.parts.pins.zheng.rank, 0); assert.equal(withZ.parts.pins.gai.rank, without.parts.pins.gai.rank, '证明不再动改命品第(已独立成维)'); // parts.zhengming 顶层仍在(level + 是否顶到宗师顶,供前端标「照见巅峰」) assert.equal(withZ.parts.zhengming.level, 3); assert.equal(typeof withZ.parts.zhengming.atCeiling, 'boolean'); // tier 单调不降 + 不越宗师顶 assert.ok(withZ.rank.tier >= without.rank.tier, 'tier 单调:证明只会抬不会降'); assert.ok(withZ.rank.tier <= withZ.parts.ceilTier, '证明不越宗师顶'); // ★★ 无死维核心:改命打卡已满(gaimingPoints=6)时,问鼎 0→3 仍能把 tier 往上推 —— 修掉「改命满则问鼎死」。 const dc = { professionLei: 'wen', axes: computeAxes(subsFromAttrs(['poetic'], ['warm'])), credentials: { zhicheng: 2 }, jiashi: 1, gaimingPoints: 6 }; const dcNoZ = computeOffice5D({ ...dc, zhengming: 0 }, 'tang'); const dcZ = computeOffice5D({ ...dc, zhengming: 3 }, 'tang'); assert.ok(dcZ.rank.tier > dcNoZ.rank.tier, '改命满时问鼎(lv3)仍推得动 tier(问鼎非死维)'); }); test('分身核验·parseVerdict:抠得出裁决取之,抠不出/形状不对一律 null(留 pending·不错认)', () => { // 查实 assert.deepEqual(parseVerdict('{"verified":true,"level":3,"reason":"官网可查"}'), { verified: true, level: 3, reason: '官网可查' }); // 查否 → level 归 0 assert.deepEqual(parseVerdict('{"verified":false,"level":3,"reason":"查不到"}'), { verified: false, level: 0, reason: '查不到' }); // LLM 裹了自然语言也能抠出第一个 JSON assert.equal(parseVerdict('好的,我核实了:{"verified":true,"level":1,"reason":"x"} 以上')?.verified, true); // verified 缺失/非布尔 → null(不下结论) assert.equal(parseVerdict('{"level":3}'), null); assert.equal(parseVerdict('{"verified":"yes"}'), null); // 抠不出 JSON / 空 → null assert.equal(parseVerdict('查不到相关信息'), null); assert.equal(parseVerdict(''), null); assert.equal(parseVerdict(null), null); // verified:true 但 level 越界 → 夹到 [1,3] assert.equal(parseVerdict('{"verified":true,"level":99}')?.level, 3); assert.equal(parseVerdict('{"verified":true,"level":0}')?.level, 1); }); test('零游戏味:office5d 含证明的输出不带禁词(养成/解锁/成就/进度/百分比…)', () => { const r = computeOffice5D({ professionLei: 'wen', axes: computeAxes(subsFromAttrs(['poetic', 'story'], ['warm'])), credentials: {}, jiashi: 0, zhengming: 3, }, 'tang'); assertNoGameMechanics(JSON.stringify(r)); // 复用全站那一份禁词清单,不另写 }); // ══════════════════════════════════════════════════════════════════════════ // DB 集成层(需本地 MongoDB;连不上整组 skip) // ══════════════════════════════════════════════════════════════════════════ let server, base, db, closeDb, ObjectId, signToken, config; const OWNER = { id: null, email: 'zm-owner@test.local' }; const OTHER = { id: null, email: 'zm-other@test.local' }; const ADMIN = { id: null, email: 'zm-admin@test.local' }; let twinId, otherTwinId, mongoUp = true; const CLERK = ['rigorous', 'datadriven', 'concise', 'shortfirst', 'patient', 'calm']; before(async () => { const dbm = await import('../src/db.js'); closeDb = dbm.closeDb; try { db = await dbm.connectDb(); } catch { mongoUp = false; return; } ({ ObjectId } = await import('mongodb')); ({ signToken } = await import('../src/middleware/auth.js')); ({ config } = await import('../src/config.js')); // ★ 造一个超管(审核台要它):直接往 config 里塞(isSuperAdmin 调用时才读)。 if (!config.security.superAdmins.includes(ADMIN.email)) config.security.superAdmins.push(ADMIN.email); const { shishuRouter } = await import('../src/routes/shishu.js'); const { adminRouter } = await import('../src/routes/admin.js'); OWNER.id = new ObjectId(); OTHER.id = new ObjectId(); ADMIN.id = new ObjectId(); await db.collection('users').insertMany([ { _id: OWNER.id, email: OWNER.email }, { _id: OTHER.id, email: OTHER.email }, { _id: ADMIN.id, email: ADMIN.email }, ]); const t = await db.collection('twins').insertOne({ ownerId: OWNER.id, displayName: '某', profession: '', talents: { sub_talents: CLERK }, isPublic: false, }); twinId = t.insertedId; const t2 = await db.collection('twins').insertOne({ ownerId: OTHER.id, displayName: '他', talents: { sub_talents: CLERK }, isPublic: false, }); otherTwinId = t2.insertedId; const app = express(); app.use(express.json()); app.use('/api/shishu', shishuRouter); app.use('/api/admin', adminRouter); server = http.createServer(app).listen(0); await new Promise((ok) => server.once('listening', ok)); base = `http://127.0.0.1:${server.address().port}`; }); after(async () => { if (server) server.close(); if (mongoUp) { await db.collection('twins').deleteMany({ _id: { $in: [twinId, otherTwinId] } }); await db.collection('users').deleteMany({ _id: { $in: [OWNER.id, OTHER.id, ADMIN.id] } }); if (closeDb) await closeDb(); } }); const auth = (u = OWNER) => ({ authorization: `Bearer ${signToken({ id: u.id.toString(), email: u.email })}` }); const get = (p, u) => fetch(base + p, { headers: auth(u) }); const post = (p, body, u) => fetch(base + p, { method: 'POST', headers: { 'content-type': 'application/json', ...auth(u) }, body: JSON.stringify(body || {}) }); const del = (p, u) => fetch(base + p, { method: 'DELETE', headers: auth(u) }); const resetZM = () => db.collection('twins').updateOne({ _id: twinId }, { $set: { zhengming: [] } }); test('铁律① 递交=待审核,对定品零影响;审核通过后才生效', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); // 递交一条 const r = await post(`/api/shishu/zhengming/${twinId}`, { title: '某国家级奖项', domain: 'wen', claimLevel: 3, note: '证书编号 X' }); assert.equal(r.status, 200); const rec = await r.json(); assert.equal(rec.status, 'pending', '落库即待审核'); assert.equal(rec.level, 0, '待审核 level=0(对定品零影响)'); // 此刻 verifiedLevel 仍是 0 —— 定品不受影响 let twin = await db.collection('twins').findOne({ _id: twinId }); assert.equal(verifiedLevel(twin), 0, '未审核 → verifiedLevel 0'); // 超管审核通过,核定 level 2 const rv = await post(`/api/admin/zhengming/${twinId}/${rec.id}/review`, { status: 'verified', level: 2 }, ADMIN); assert.equal(rv.status, 200); twin = await db.collection('twins').findOne({ _id: twinId }); assert.equal(verifiedLevel(twin), 2, '审核通过后 verifiedLevel=2(这一刻才生效)'); }); test('铁律② 超管门:非超管不能审核(403)', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); const r = await post(`/api/shishu/zhengming/${twinId}`, { title: 'A' }); const rec = await r.json(); // 分身主人自己审 → 403(不能自评生效) const self = await post(`/api/admin/zhengming/${twinId}/${rec.id}/review`, { status: 'verified', level: 3 }, OWNER); assert.equal(self.status, 403, '本人不能自审'); // 陌生人审 → 403 const other = await post(`/api/admin/zhengming/${twinId}/${rec.id}/review`, { status: 'verified', level: 3 }, OTHER); assert.equal(other.status, 403); // 审核后仍是 pending(没被非法改动) const twin = await db.collection('twins').findOne({ _id: twinId }); assert.equal(twin.zhengming[0].status, 'pending'); assert.equal(verifiedLevel(twin), 0); }); test('属主隔离(IDOR):拿不到/递交不了别人的分身', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); // OTHER 递交到 OWNER 的分身 → 404 const r = await post(`/api/shishu/zhengming/${twinId}`, { title: '偷' }, OTHER); assert.equal(r.status, 404, '别人的分身递交不了'); // OTHER 列 OWNER 的分身 → 404 const l = await get(`/api/shishu/zhengming/${twinId}`, OTHER); assert.equal(l.status, 404, '别人的分身列不了'); }); test('防刷:条数到上限拒收(409)', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); // 直接塞满到上限 const full = Array.from({ length: MAX_ITEMS }, (_, i) => ({ id: 'x' + i, title: 't', status: 'pending', level: 0 })); await db.collection('twins').updateOne({ _id: twinId }, { $set: { zhengming: full } }); const r = await post(`/api/shishu/zhengming/${twinId}`, { title: '再来一条' }); assert.equal(r.status, 409, '到上限拒收'); }); test('撤回:只能撤没审的;审过的撤不动', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); const p = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '待审的' })).json(); const v = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '将审的' })).json(); await post(`/api/admin/zhengming/${twinId}/${v.id}/review`, { status: 'verified', level: 1 }, ADMIN); // 撤待审的 → 掉 const d1 = await del(`/api/shishu/zhengming/${twinId}/${p.id}`); assert.equal(d1.status, 200); // 撤已审的 → 撤不动(仍在) await del(`/api/shishu/zhengming/${twinId}/${v.id}`); const twin = await db.collection('twins').findOne({ _id: twinId }); const ids = (twin.zhengming || []).map((z) => z.id); assert.ok(!ids.includes(p.id), '待审的已撤走'); assert.ok(ids.includes(v.id), '已审的撤不动、留痕'); }); test('分身核验·查实就认(自动):mock 查实→verified+level+by:ai;查否→rejected;不下结论→留 pending', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); // 递交三条(注意:路由里 submit 会 detach 触发真 llmVerify,但测试环境无 DASHSCOPE key → 留 pending, // 不影响;这里用【注入 mock verifier】直接、确定性地测编排。) const a = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '查得到的顶尖奖' })).json(); const b = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '查不到的' })).json(); const c = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '一时查不准的' })).json(); // 查实 → verified + level + by:'ai' await verifyZhengming(twinId, a.id, async () => ({ verified: true, level: 3, reason: '官网可查' })); // 查否 → rejected await verifyZhengming(twinId, b.id, async () => ({ verified: false, level: 0, reason: '同名不同人' })); // 不下结论(null) → 保持 pending await verifyZhengming(twinId, c.id, async () => null); const twin = await db.collection('twins').findOne({ _id: twinId }); const byId = Object.fromEntries((twin.zhengming || []).map((z) => [z.id, z])); assert.equal(byId[a.id].status, 'verified'); assert.equal(byId[a.id].level, 3); assert.equal(byId[a.id].by, 'ai'); assert.equal(byId[b.id].status, 'rejected'); assert.equal(byId[b.id].level, 0); assert.equal(byId[c.id].status, 'pending', '不下结论 → 留 pending,等重核/人工兜底'); // 生效:已查实的 level 3 进 verifiedLevel assert.equal(verifiedLevel(twin), 3); // 幂等:对已 verified 的再核,不改(skipped) const again = await verifyZhengming(twinId, a.id, async () => ({ verified: false, level: 0 })); assert.ok(again.skipped, '已核过的不再动'); const twin2 = await db.collection('twins').findOne({ _id: twinId }); assert.equal(twin2.zhengming.find((z) => z.id === a.id).status, 'verified', '幂等:仍是 verified'); }); test('分身核验·reverify 路由:卡在 pending 的可再核一次(属主)', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); const rec = await (await post(`/api/shishu/zhengming/${twinId}`, { title: '再核测试' })).json(); // 无 DASHSCOPE key → 真 llmVerify 留 pending;reverify 路由应 200(走真核验,仍 pending) const r = await post(`/api/shishu/zhengming/${twinId}/${rec.id}/reverify`, {}); assert.equal(r.status, 200); // 别人的分身重核不了 const other = await post(`/api/shishu/zhengming/${twinId}/${rec.id}/reverify`, {}, OTHER); assert.equal(other.status, 404); }); test('审核台:列出待审 + 递交需标题 + note 上限', async (t) => { if (!mongoUp) return t.skip('无本地 MongoDB'); await resetZM(); // 无标题 → 400 const noTitle = await post(`/api/shishu/zhengming/${twinId}`, { note: '只有说明' }); assert.equal(noTitle.status, 400); // 递交一条 → 出现在超管待审台 await post(`/api/shishu/zhengming/${twinId}`, { title: '待审台可见' }); const pend = await (await get('/api/admin/zhengming/pending', ADMIN)).json(); assert.ok(pend.items.some((x) => x.twinId === twinId.toString() && x.title === '待审台可见'), '待审台列出这一条'); });