Files
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

133 lines
7.0 KiB
JavaScript
Raw Permalink 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.
// 多维钩子(第二擅长 + 性格 → 副天赋并集 → computeRank)的单测。
//
// 守四件事:
// ① chip → subs 只落在【现有 19 项表达型】副天赋里(排除治学 hobby,防后人映到不存在的项)。
// ② mapChipsToSubs 纯函数:并集、去重、未知 id 优雅忽略、query 两种形态("a,b" / 数组)都摊平。
// ③ 回落:不传 talents/traits ⇒ 合成 subs 与纯职业【逐字相同】,computeRank 过全朝【逐格相同】。
// —— 这一条就是「不破 /zhiye/:id、611 基线不动」的机器证明。
// ④ 多维【真改落点】:产品经理 + 沉得住(→沉毅) 让【宋】从「白身」变成走通的科举正途;
// 任务示例 产品经理 + 会写 + 直 至少改变一朝 —— 位阶是 computeRank 真算的,比纯职业更细。
//
// 跑:node --test test/zhiye-compose.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import { tuiyanProfile, mapChipsToSubs, listChips, CHIPS, VALID_SUBS } from '../src/zhiye.js';
import { computeAxes, computeRank, listDynasties } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
// 与 routes/tianxia.js /zhiye/:id 里【逐字同源】的合成逻辑:职业 subs ∪ chip subs。
function composeSubs(zhiyeId, talents, traits) {
const base = tuiyanProfile(zhiyeId);
const { subs: extra, chips } = mapChipsToSubs(talents, traits);
const subs = base.subs.slice();
for (const s of extra) if (!subs.includes(s)) subs.push(s);
return { subs, chips };
}
// 一个 subs 过全朝的位阶指纹(真跑 computeRank,不用快照)。
function fingerprint(subs) {
const profile = { axes: computeAxes(subs) };
const out = {};
for (const d of listDynasties()) {
try {
const r = computeRank(profile, d.id);
out[d.id] = r.rank ? `${r.rank.title}|t${r.rank.tier}|${r.channel ? r.channel.id : '-'}` : null;
} catch { out[d.id] = 'ERR'; }
}
return out;
}
// ── ① 白名单:每个 chip 的每个 sub 都在现有表达型副天赋里 ──
test('★ chip → subs 只落在现有 19 项表达型副天赋里(不发明新 sub)', () => {
const valid = new Set(VALID_SUBS);
const expressive = SUB_REGISTRY.filter((item) => item.subtype !== 'hobby').map((item) => item.id).sort();
assert.equal(valid.size, 19, '白名单必须正好 19 项表达型副天赋');
assert.deepEqual([...valid].sort(), expressive, '白名单必须与 talents.js 的非 hobby 项逐一一致');
for (const c of CHIPS) {
assert.ok(c.kind === 'tuiyan', `${c.id} 必须标 kind:'tuiyan'(是推演不是史料)`);
assert.ok(typeof c.basis === 'string' && c.basis.length > 4, `${c.id} 必须有可反驳的 basis`);
assert.ok(c.subs.length > 0, `${c.id} 至少映一项`);
for (const s of c.subs) assert.ok(valid.has(s), `${c.id} 映到了不存在的副天赋: ${s}`);
}
});
// ── ② mapChipsToSubs 纯函数行为 ──
test('★ mapChipsToSubs:并集 + 去重 + 未知 id 忽略', () => {
// 会写(story,vivid) + 直(blunt)
const a = mapChipsToSubs('huixie', 'zhi');
assert.deepEqual(a.subs, ['story', 'vivid', 'blunt']);
assert.deepEqual(a.chips.map((c) => c.id), ['huixie', 'zhi']);
// 直(blunt) + 刚(blunt) ⇒ blunt 去重成一项;两个 chip 都回显
const b = mapChipsToSubs('', 'zhi,gang');
assert.deepEqual(b.subs, ['blunt']);
assert.deepEqual(b.chips.map((c) => c.id), ['zhi', 'gang']);
// 未知 id 优雅忽略,不抛错
const c = mapChipsToSubs('nope,huisuan', 'bogus');
assert.deepEqual(c.subs, ['datadriven', 'rigorous']);
assert.deepEqual(c.chips.map((c2) => c2.id), ['huisuan']);
});
test('★ mapChipsToSubs:query 两种形态("a,b" 字符串 / 数组)都摊平', () => {
const asString = mapChipsToSubs('huixie,chendezhu', 'zhi');
const asArray = mapChipsToSubs(['huixie', 'chendezhu'], ['zhi']);
assert.deepEqual(asString.subs, asArray.subs);
// 重复 chip id 也去重(数组里传两遍)
const dup = mapChipsToSubs(['huixie', 'huixie'], undefined);
assert.deepEqual(dup.chips.map((c) => c.id), ['huixie']);
});
// ── ③ 回落:不传 chip ⇒ 与纯职业逐字相同(不破 /zhiye/:id) ──
test('★★ 回落:不传 talents/traits ⇒ subs 与位阶都与纯职业【逐字相同】', () => {
for (const zid of ['chanpin', 'chengxuyuan', 'yisheng', 'jizhe']) {
const base = tuiyanProfile(zid).subs;
for (const args of [[undefined, undefined], ['', ''], [[], []], ['', undefined]]) {
const { subs, chips } = composeSubs(zid, args[0], args[1]);
assert.deepEqual(subs, base, `${zid} 回落 subs 必须逐字等于纯职业`);
assert.equal(chips.length, 0, `${zid} 回落不应有 chip`);
}
// 位阶指纹也必须逐格相同 —— 这才是「611 基线不动」的证明
assert.deepEqual(fingerprint(composeSubs(zid).subs), fingerprint(base));
}
});
// ── ④ 多维真改落点 ──
test('★★ 产品经理 + 沉得住(→沉毅) 让【宋】从白身变成走通的科举正途', () => {
const pure = fingerprint(tuiyanProfile('chanpin').subs);
const { subs, chips } = composeSubs('chanpin', 'chendezhu', '');
const multi = fingerprint(subs);
assert.deepEqual(chips.map((c) => c.name), ['沉得住']);
// 纯产品经理在宋是白身;加一笔「沉得住」后宋这一格【变了】且不再是白身
assert.ok(/白身/.test(pure.song), '前提:纯产品经理在宋应是白身');
assert.notEqual(multi.song, pure.song, '加沉得住后宋这一格必须变');
assert.ok(!/白身/.test(multi.song), '加沉得住后宋不再是白身(走通了科举正途)');
});
test('★★ 任务示例:产品经理 + 会写 + 直 —— 位阶真算、比纯职业更细(至少改一朝)', () => {
const base = tuiyanProfile('chanpin').subs;
const pure = fingerprint(base);
const { subs, chips } = composeSubs('chanpin', 'huixie', 'zhi');
const multi = fingerprint(subs);
assert.deepEqual(chips.map((c) => c.name), ['会写', '直']);
// 合成六轴:会写→wen、直→wu 都长起来了(真算,不是编)
const ax = computeAxes(subs);
assert.ok(ax.wen > computeAxes(base).wen, '会写应抬高文采轴');
assert.ok(ax.wu > computeAxes(base).wu, '直应抬高果决轴');
// 至少一朝的落点与纯职业不同
const changed = listDynasties().filter((d) => multi[d.id] !== pure[d.id]).map((d) => d.id);
assert.ok(changed.length >= 1, `多维应至少改变一朝,实测改变: ${changed.join(',')}`);
});
// ── listChips 分组(供前端渲染) ──
test('★ listChips 按 talent/trait 分组,每笔带 basis/kind', () => {
const { talents, traits, caveat } = listChips();
assert.ok(talents.length >= 6 && traits.length >= 6);
assert.ok(typeof caveat === 'string' && caveat.length > 10);
for (const c of [...talents, ...traits]) {
assert.ok(c.id && c.name && Array.isArray(c.subs) && c.subs.length > 0);
assert.equal(c.kind, 'tuiyan');
assert.ok(typeof c.basis === 'string' && c.basis.length > 4);
}
});