SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,203 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
ELEMENT_MANIFEST,
MessageElementValidationError,
assertValidMessageElement,
typesForDirection,
} from '../src/element-manifest';
// 对账测试(protocol 侧)—— 拿 element-manifest 核对「按 element.type 收敛」的
// 两个 protocol 方向:
// D 收·解 rich-body-decoder.ts 产出的 MessageElement 类型集合
// W 发·打包 element-builder.ts 的 switch 分支集合
// onebot 侧的 S/P 由 onebot/tests/element-manifest.test.ts 核对(包边界所限)。
//
// 这是一道架构护栏(fitness function):谁在这两处新增/删除一种元素方向,却没有
//同步更新 element-manifest,测试立即报红——把「漏写一个方向」从运行时兜底静默
// 变成 CI 当场可见。纯读源码、零运行时行为改动。
function readSrc(relFromTestDir: string): string {
const abs = fileURLToPath(new URL(relFromTestDir, import.meta.url));
const raw = readFileSync(abs, 'utf8');
// 剥掉块注释与行注释,避免注释里出现的类型字面量污染扫描结果。
return raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
}
/** 抽出所有匹配 `pattern` 第 1 捕获组的去重、排序类型名。 */
function extractTypes(src: string, pattern: RegExp): string[] {
const found = new Set<string>();
for (const m of src.matchAll(pattern)) {
found.add(m[1]!);
}
return [...found].sort();
}
const sorted = (s: Iterable<string>): string[] => [...s].sort();
describe('element-manifest 对账(protocol 侧:D 收·解 / W 发·打包)', () => {
it('D:rich-body-decoder 产出的元素类型 == 清单声明的 D=yes', () => {
const src = readSrc('../src/msg-push/rich-body-decoder.ts');
// 解码产出形如 `result.push({ type: 'image', ... })` / `{ type: 'reply', ... }`。
// 值域放宽到 `[A-Za-z0-9_]+`(而非仅小写):类型名当前虽全小写,但收紧到
// `[a-z]+` 会让未来的驼峰/下划线类型名被静默漏抓,护栏就形同虚设——正是本
// 重构要消灭的"漏一个方向、走 default 静默"失效模式,不能在护栏自身重演。
const handled = extractTypes(src, /(?<![A-Za-z])type:\s*'([A-Za-z0-9_]+)'/g);
expect(handled).toEqual(sorted(typesForDirection('D')));
});
it('W:element-builder 的 switch 分支 == 清单声明的 W=yes', () => {
const src = readSrc('../src/element-builder.ts');
const handled = extractTypes(src, /case\s*'([A-Za-z0-9_]+)'\s*:/g);
expect(handled).toEqual(sorted(typesForDirection('W')));
});
it('fields 是可执行封闭契约:每个声明字段都有 validator,越界字段会拒绝', () => {
const sampleValue = (field: string): unknown => {
if (field === 'replyElements') {
return [];
}
if (field === 'forwardNews') {
return [{ text: 'preview' }];
}
if (field === 'rows') {
return [{ buttons: [] }];
}
if (field === 'mediaNode') {
return {};
}
if (field === 'emojiId' || field === 'md5Hex') {
return 'ab'.repeat(16);
}
if (field === 'sha1Hex') {
return 'cd'.repeat(20);
}
if (field === 'flash' || field === 'noByteFallback' || field === 'large') {
return true;
}
if (field === 'botAppid') {
return '1';
}
if (
field === 'targetUin' ||
field === 'faceId' ||
field === 'fileSize' ||
field.startsWith('reply') ||
field === 'subType' ||
field === 'duration' ||
field === 'width' ||
field === 'height' ||
field === 'emojiPackageId' ||
field === 'sceneType' ||
field === 'forwardTSum' ||
field.endsWith('Format')
) {
return 1;
}
return 'value';
};
for (const [type, spec] of Object.entries(ELEMENT_MANIFEST)) {
expect(new Set(spec.fields).size, `${type} has duplicate fields`).toBe(
spec.fields.length,
);
for (const required of spec.requiredFields) {
expect(spec.fields).toContain(required);
}
const element: Record<string, unknown> = { type };
for (const field of spec.fields) {
element[field] = sampleValue(field);
}
expect(() => assertValidMessageElement(element), type).not.toThrow();
}
expect(() =>
assertValidMessageElement({ type: 'poke', subType: 1, faceId: 1 }),
).toThrowError(
expect.objectContaining({
name: 'MessageElementValidationError',
code: 'UNEXPECTED_FIELD',
elementType: 'poke',
field: 'faceId',
}),
);
expect(() => assertValidMessageElement({ type: 'reply' })).toThrowError(
expect.objectContaining({ code: 'MISSING_FIELD', field: 'replySeq' }),
);
expect(() => assertValidMessageElement({ type: 'toString' })).toThrowError(
expect.objectContaining({ code: 'UNKNOWN_TYPE', elementType: 'toString' }),
);
expect(() => assertValidMessageElement({ type: 'mface', emojiId: 'zz' }, 'W')).toThrowError(
expect.objectContaining({ code: 'INVALID_FIELD', field: 'emojiId' }),
);
expect(() =>
assertValidMessageElement(
{
type: 'image',
url: 'https://example.com/a.png',
md5Hex: 'not-md5',
},
'W',
),
).toThrowError(expect.objectContaining({ code: 'INVALID_FIELD', field: 'md5Hex' }));
});
it('returns a stable typed validation error for BAD_REQUEST mapping', () => {
try {
assertValidMessageElement({ type: 'unknown-segment' }, 'W');
expect.unreachable('validation should throw');
} catch (error) {
expect(error).toBeInstanceOf(MessageElementValidationError);
expect(error).toMatchObject({ code: 'UNKNOWN_TYPE', elementType: 'unknown-segment' });
}
});
it('rejects invalid wire ranges and incomplete fast-upload fingerprints', () => {
expect(() =>
assertValidMessageElement(
{
type: 'file',
fileId: 'fid',
fileSize: -1,
},
'P',
),
).toThrowError(
expect.objectContaining({
code: 'INVALID_FIELD',
field: 'fileSize',
}),
);
expect(() =>
assertValidMessageElement(
{
type: 'image',
url: 'https://example.com/fallback-must-not-be-used.png',
noByteFallback: true,
},
'W',
),
).toThrowError(
expect.objectContaining({
code: 'MISSING_FIELD',
field: 'md5Hex',
}),
);
});
});