133 lines
7.0 KiB
JavaScript
133 lines
7.0 KiB
JavaScript
// 多维钩子(第二擅长 + 性格 → 副天赋并集 → 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);
|
||
}
|
||
});
|