Files
digital-me-history/digital-me-backend/test/zhengming.test.js
T
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

300 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 问鼎 · 重要证明 —— 单测。钉三条铁律,一条都不许松:
// ① 递交=待审核,【对定品零影响】(自评上传绝不自动生效);
// ② 只有【超管审核 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 === '待审台可见'), '待审台列出这一条');
});