数字我 Digital Me:全量入库(大文件走 Git LFS)
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s

This commit is contained in:
2026-10-10 16:24:43 +08:00
commit 0593ff7e62
1893 changed files with 361723 additions and 0 deletions
@@ -0,0 +1,151 @@
// 家长监护的【强制闸】回归测试(《人工智能拟人化互动服务管理暂行办法》第十四条第二款)。
//
// ⚠ 这组测试存在的理由跟代码本身一样重要:commit 7c423f3 把这些闸门整个删掉过一次,
// 没有任何测试变红,所以没有人发现。删一个纯函数会被 guardian.test.js 拦住,但
// 「把 requireAdult 从 wallet 路由上摘掉」「gateAsk 里不再查屏蔽」这类删法,纯函数测试
// 一条都拦不住 —— 它们只在【接线】上体现。所以这里测的是接线:
// · 未成年人模式的账号,钱包是不是真的 403(限制充值消费)
// · 监护人屏蔽了某个分身后,gateAsk 是不是真的拦(屏蔽特定角色)
// · 成年人是不是完全不受影响(不误伤 —— 误伤了运营就会把整个功能关掉)
// 哪天有人再"顺手清理"一次,这里会红。
//
// 需要本地 MongoDB;连不上则整组 skip(不阻塞无 DB 的 CI),与 store-idor.test.js 同一口径。
process.env.NODE_ENV = 'test';
process.env.MONGO_DB = process.env.MONGO_DB_TEST || 'digital_me_test';
process.env.FEATURE_MINOR_MODE = 'on';
process.env.MINOR_DAILY_LIMIT_MIN = process.env.MINOR_DAILY_LIMIT_MIN || '60';
process.env.JWT_SECRET = process.env.JWT_SECRET || 'guardian-gate-test';
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import express from 'express';
import http from 'node:http';
let server, base, db, closeDb, mongoUp = true;
let ObjectId, signToken, gateAsk, G;
let kid, adult, twinA, twinB;
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'));
({ gateAsk } = await import('../src/services/minor.js'));
G = await import('../src/services/guardian.js');
const { walletRouter } = await import('../src/routes/wallet.js');
const app = express();
app.use(express.json());
app.use('/api/wallet', walletRouter);
server = http.createServer(app).listen(0);
await new Promise((ok) => server.once('listening', ok));
base = `http://127.0.0.1:${server.address().port}`;
kid = new ObjectId(); adult = new ObjectId();
twinA = new ObjectId(); twinB = new ObjectId();
await db.collection('users').insertOne({
_id: kid, name: '测试未成年人', birthYear: 2014,
minorMode: 'on', minorLocked: true,
// 不满 14 周岁 → 没有监护人同意的话 gateAsk 会先被第十四条第三款那道闸拦下,
// 就测不到本组要测的屏蔽闸了。这里补上,把测试对准要测的东西。
guardianConsent: { at: new Date(), level: 'attested' },
});
await db.collection('users').insertOne({ _id: adult, name: '测试成年人' });
await db.collection('twins').insertOne({ _id: twinA, displayName: '测试分身A', slug: `t-a-${Date.now()}` });
await db.collection('twins').insertOne({ _id: twinB, displayName: '测试分身B', slug: `t-b-${Date.now()}` });
});
after(async () => {
if (server) server.close();
if (mongoUp && db) {
const ids = [kid, adult].filter(Boolean);
if (ids.length) await db.collection('users').deleteMany({ _id: { $in: ids } });
const tids = [twinA, twinB].filter(Boolean);
if (tids.length) await db.collection('twins').deleteMany({ _id: { $in: tids } });
if (kid) {
await db.collection('guardian_blocks').deleteMany({ wardId: kid });
await db.collection('minor_usage').deleteMany({ userId: kid });
}
}
if (mongoUp && closeDb) await closeDb();
});
const walletGet = async (id) =>
fetch(`${base}/api/wallet/`, { headers: { authorization: `Bearer ${signToken({ id: id.toString(), email: 't@e.st' })}` } });
// ── 限制充值消费 ──
test('【第十四条第二款·限制充值消费】未成年人模式的账号:钱包 403', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
const res = await walletGet(kid);
const body = await res.json();
assert.equal(res.status, 403, 'requireAdult 被从 walletRouter 上摘掉了?');
assert.equal(body.error, 'minor-spending-blocked');
// 前端 errMsg 优先取 data.message —— 只回英文码的话用户看到的是一串机器码。
assert.match(body.message, /未成年人模式/);
});
test('不误伤:成年账号钱包照常 200', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
const res = await walletGet(adult);
assert.equal(res.status, 200, '误伤成年人 = 运营会把整个未成年人模式关掉,那才是最坏结局');
});
// ── 屏蔽特定角色 ──
test('【第十四条第二款·屏蔽特定角色】屏蔽后 gateAsk 服务端硬拦,解除后恢复', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
await db.collection('guardian_blocks').deleteMany({ wardId: kid });
const before_ = await gateAsk(kid.toString(), '你好啊', twinA);
assert.ok(!before_.blocked, '未屏蔽时应正常放行');
await G.blockTwin(adult.toString(), kid.toString(), twinA.toString());
const after_ = await gateAsk(kid.toString(), '你好啊', twinA);
assert.ok(after_.blocked, '★ 屏蔽后必须拦住 —— 前端藏起来是没用的,curl 直接就绕过去了');
assert.equal(after_.code, 'guardian-twin-blocked');
const other = await gateAsk(kid.toString(), '你好啊', twinB);
assert.ok(!other.blocked, '没被屏蔽的分身不能受影响');
await G.unblockTwin(kid.toString(), twinA.toString());
const restored = await gateAsk(kid.toString(), '你好啊', twinA);
assert.ok(!restored.blocked, '解除屏蔽后必须恢复');
});
test('屏蔽是幂等的(唯一索引 + 11000 当成功):重复点不报错、不产生第二条', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
await db.collection('guardian_blocks').deleteMany({ wardId: kid });
await G.blockTwin(adult.toString(), kid.toString(), twinA.toString());
await G.blockTwin(adult.toString(), kid.toString(), twinA.toString());
assert.equal((await G.listBlocks(kid.toString())).length, 1);
await db.collection('guardian_blocks').deleteMany({ wardId: kid });
});
test('不误伤:成年人(未开未成年人模式)即使库里有屏蔽记录也一律放行', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
await db.collection('guardian_blocks').insertOne({ wardId: adult, twinId: twinA, guardianId: kid, createdAt: new Date() });
const g = await gateAsk(adult.toString(), '你好', twinA);
assert.ok(!g.blocked && !g.notice, 'minorMode 未开时 gateAsk 应在读到 mode 后立刻返回,连屏蔽都不查');
await db.collection('guardian_blocks').deleteMany({ wardId: adult });
});
// ── 针对不同年龄段的时长上限 ──
test('【第十四条第二款·针对不同年龄段保护需要】监护人只能把上限调得更严,不能更松', async (t) => {
if (!mongoUp) return t.skip('无本地 MongoDB,跳过 DB 集成测试');
const { getMinorState } = await import('../src/services/minor.js');
await G.setWardDailyLimit(kid.toString(), 20);
assert.equal((await getMinorState(kid.toString())).dailyLimitMin, 20, '家长设的更严的值必须生效');
await G.setWardDailyLimit(kid.toString(), 999);
assert.equal(
(await getMinorState(kid.toString())).dailyLimitMin, 60,
'★ 家长不能把上限调得比平台默认更松 —— 否则等于给每个家长发了一个"关掉时长限制"的按钮'
);
await G.setWardDailyLimit(kid.toString(), 0);
assert.equal((await getMinorState(kid.toString())).dailyLimitMin, 60, '设 0 = 取消覆盖,回落平台默认');
});