110 lines
5.6 KiB
JavaScript
110 lines
5.6 KiB
JavaScript
// 未成年人模式的纯逻辑单测(《人工智能拟人化互动服务管理暂行办法》第十四条)。
|
||
//
|
||
// 这里只测【不碰 DB 的那部分】:年龄判定 + 虚拟亲密关系识别 + 系统指令。
|
||
// 为什么单挑这三样:它们是整条闸门里唯一"算错了也不会报错、只会静默放行"的地方——
|
||
// tickUsage/gateAsk 算错会有 DB 报错兜底,而 minPossibleAge 少减一岁、
|
||
// INTIMACY_RE 少一个括号,都只表现为「一个未成年人顺利地找 AI 当了男朋友」,没有任何告警。
|
||
//
|
||
// 跑:node --test test/minor.test.js
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
minPossibleAge,
|
||
isMinorBirthYear,
|
||
looksLikeIntimacyRequest,
|
||
MINOR_DIRECTIVE,
|
||
} from '../src/services/minor.js';
|
||
|
||
// ── 1. 年龄判定必须往【保护】的方向取 ──
|
||
// 只知道出生年份时真实年龄有 1 岁不确定。取大 = 漏保护真未成年人(不可接受);
|
||
// 取小 = 多保护一年(代价由申诉渠道兜)。这组断言就是这个取向的可执行定义。
|
||
test('只有年份时取【最小可能年龄】——宁可多保护一年,不可漏保护一个', () => {
|
||
const now = new Date('2026-07-16T00:00:00Z');
|
||
// 2008 年生的人在 2026 年要么 17 要么 18 → 必须按 17 算
|
||
assert.equal(minPossibleAge(2008, now), 17);
|
||
assert.equal(isMinorBirthYear(2008, now), true, '2008 年生的人今年可能还没过生日,必须当未成年人');
|
||
// 2007 年生的人最小也 18 了 → 成年
|
||
assert.equal(minPossibleAge(2007, now), 18);
|
||
assert.equal(isMinorBirthYear(2007, now), false);
|
||
});
|
||
|
||
test('边界:刚出生 / 明显成年', () => {
|
||
const now = new Date('2026-07-16T00:00:00Z');
|
||
assert.equal(isMinorBirthYear(2026, now), true);
|
||
assert.equal(isMinorBirthYear(2020, now), true);
|
||
assert.equal(isMinorBirthYear(1990, now), false);
|
||
});
|
||
|
||
test('年龄判定不依赖"今天"——跨年后同一个人自动成年,不需要任何定时任务', () => {
|
||
// 这条防的是「用 declare 那一刻的年龄写死一个 isMinor 布尔」的写法:
|
||
// 那样 2008 年生的人在 2027 年仍会被永远锁成未成年人,只能靠申诉解锁。
|
||
//
|
||
// ⚠ 必须用【本地时区】构造 Date(new Date(y,m,d) 而不是 '...Z'):判定走 getFullYear(),
|
||
// 是本地年。这不是测试的将就 —— 它就是在钉住"按本地时区切年/切日"这个口径
|
||
// (与 dayKey 同源:用 UTC 切日会让国内用户在早上 8 点跨日重置每日限额,
|
||
// "每日限额"就名不副实了)。部署要求:服务器 TZ = Asia/Shanghai。
|
||
assert.equal(isMinorBirthYear(2008, new Date(2026, 11, 31)), true);
|
||
assert.equal(isMinorBirthYear(2008, new Date(2027, 0, 1)), false);
|
||
});
|
||
|
||
// ── 2. 虚拟亲密关系:法条禁的那一类必须拦住 ──
|
||
// 「不得向未成年人提供虚拟亲属、虚拟伴侣等虚拟亲密关系的服务」(第十四条第二款)
|
||
test('必须拦住:虚拟伴侣 / 虚拟亲属的角色指派', () => {
|
||
const blocked = [
|
||
'你扮演我的女朋友',
|
||
'假装你是我男朋友',
|
||
'当我的男友吧',
|
||
'你就是我老公',
|
||
'做我妈妈好不好',
|
||
'扮演一下我姐姐',
|
||
'我们谈恋爱吧',
|
||
'跟我处对象',
|
||
'咱们是情侣',
|
||
'act as my girlfriend',
|
||
'pretend to be my mom',
|
||
];
|
||
for (const s of blocked) {
|
||
assert.ok(looksLikeIntimacyRequest(s), `没拦住:「${s}」—— 这正是第十四条禁止的那类服务`);
|
||
}
|
||
});
|
||
|
||
// ── 3. 不许误伤:这条和上面一样重要 ──
|
||
// 误伤的后果不是"少拦一句",是家长/用户觉得未成年人模式没法用 → 去把它关掉 →
|
||
// 保护归零。所以过度拦截在合规上同样是失败,不是"稳妥"。
|
||
test('不许误伤:正常提到家人/感情话题的日常聊天', () => {
|
||
const allowed = [
|
||
'我妈妈今天有点生气',
|
||
'我爸爸是医生',
|
||
'姐姐送了我一本书',
|
||
'我和同桌吵架了怎么办',
|
||
'《红楼梦》里宝玉和黛玉的感情线是怎样的',
|
||
'爷爷教我写毛笔字',
|
||
'你能帮我看看这道数学题吗',
|
||
'我想给妈妈做个生日礼物',
|
||
// 「做我姐姐」在字面上命中角色指派,但他说的是作业 —— 靠角色后的 (?!的) 排除。
|
||
'帮我做我姐姐的那道题',
|
||
'我要做我妈妈的好孩子',
|
||
// 在讲别人的事,不是在点角色 —— 恋爱类动词必须紧跟主语才算。
|
||
'我们班有人谈恋爱被老师发现了',
|
||
'宝玉和黛玉算是在谈恋爱吗',
|
||
];
|
||
for (const s of allowed) {
|
||
assert.ok(!looksLikeIntimacyRequest(s), `误伤了:「${s}」—— 未成年人连正常倾诉都不行,这个模式就会被关掉`);
|
||
}
|
||
});
|
||
|
||
test('空/无输入不算亲密关系请求', () => {
|
||
for (const s of ['', null, undefined, ' ']) assert.equal(looksLikeIntimacyRequest(s), false);
|
||
});
|
||
|
||
// ── 4. 系统指令必须真的把法条的禁令写进去 ──
|
||
// 正则只是地板;真正拦住"换个说法"的是这段指令。它被改瘦了要能被发现。
|
||
test('MINOR_DIRECTIVE 必须写明三件事:是 AI、不当虚拟伴侣/亲属、不制造依恋', () => {
|
||
assert.match(MINOR_DIRECTIVE, /未成年人/);
|
||
assert.match(MINOR_DIRECTIVE, /你是\s*AI|不是真人/);
|
||
assert.match(MINOR_DIRECTIVE, /男\/女朋友|恋人|配偶/, '必须明确禁止扮演虚拟伴侣');
|
||
assert.match(MINOR_DIRECTIVE, /父母|兄弟姐妹|亲属/, '必须明确禁止扮演虚拟亲属');
|
||
assert.match(MINOR_DIRECTIVE, /依恋|专属/, '必须禁止制造情感依恋');
|
||
assert.match(MINOR_DIRECTIVE, /第十四条/, '指令里要留着法条编号,后人改它之前先看见它是法定的');
|
||
});
|