import { test } from 'node:test'; import assert from 'node:assert/strict'; import { validate, applyDefaults } from '../../packages/protocol/dist/index.js'; test('validate:基本类型', () => { assert.deepEqual(validate('x', { type: 'string' }), []); assert.deepEqual(validate(1, { type: 'integer' }), []); assert.deepEqual(validate(1.5, { type: 'number' }), []); assert.deepEqual(validate(true, { type: 'boolean' }), []); assert.deepEqual(validate([], { type: 'array' }), []); assert.deepEqual(validate({}, { type: 'object' }), []); assert.ok(validate(1, { type: 'string' }).length > 0); assert.ok(validate('x', { type: 'integer' }).length > 0); assert.ok(validate(1.5, { type: 'integer' }).length > 0, '非整数应报错'); }); test('validate:enum', () => { const schema = { type: 'string', enum: ['a', 'b'] }; assert.deepEqual(validate('a', schema), []); assert.ok(validate('c', schema).length > 0, '不在枚举内应报错'); }); test('validate:数值范围', () => { const schema = { type: 'number', minimum: 0, maximum: 10 }; assert.deepEqual(validate(5, schema), []); assert.deepEqual(validate(0, schema), []); assert.ok(validate(-1, schema).length > 0); assert.ok(validate(11, schema).length > 0); }); test('validate:字符串长度与 pattern', () => { const schema = { type: 'string', minLength: 2, maxLength: 5, pattern: '^[a-z]+$' }; assert.deepEqual(validate('abc', schema), []); assert.ok(validate('a', schema).length > 0, '过短'); assert.ok(validate('abcdef', schema).length > 0, '过长'); assert.ok(validate('ab1', schema).length > 0, '不匹配 pattern'); }); test('validate:数组长度与 items 类型', () => { const schema = { type: 'array', minItems: 1, maxItems: 3, items: { type: 'integer' } }; assert.deepEqual(validate([1, 2], schema), []); assert.ok(validate([], schema).length > 0, '空数组'); assert.ok(validate([1, 2, 3, 4], schema).length > 0, '超长'); assert.ok(validate([1, 'x'], schema).length > 0, 'item 类型错'); }); test('validate:required 与 additionalProperties', () => { const schema = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'], additionalProperties: false, }; assert.deepEqual(validate({ name: 'x', age: 1 }, schema), []); assert.ok(validate({ age: 1 }, schema).length > 0, '缺 required'); assert.ok(validate({ name: 'x', extra: 1 }, schema).length > 0, '多余字段'); }); test('validate:嵌套路径正确', () => { const schema = { type: 'object', properties: { filter: { type: 'object', properties: { pattern: { type: 'string' } } } }, }; assert.deepEqual(validate({ filter: { pattern: 'x' } }, schema), []); const errs = validate({ filter: { pattern: 1 } }, schema); assert.equal(errs.length, 1); assert.ok(errs[0].includes('$.filter.pattern'), '错误应带嵌套路径'); }); test('applyDefaults:填补缺失字段,不覆盖已有值', () => { const schema = { type: 'object', properties: { limit: { type: 'integer', default: 20 }, recursive: { type: 'boolean', default: false }, }, }; const out = applyDefaults({ limit: 5 }, schema); assert.equal(out.limit, 5, '已有值不被覆盖'); assert.equal(out.recursive, false, '缺失字段填默认值'); }); // --------------------------------------------------------------------------- // A-P0-1(2026-10-25):原型键绕过 additionalProperties:false —— fail-open 方向的守卫缺陷 // // 根因:`key in schema.properties` 沿**原型链**查找,而 schema.properties 是普通 // 对象字面量(继承 Object.prototype)。调用方传 {"constructor":1} 等键时 // `key in properties` 为 true → 不判 additional property → **校验放行**。 // 与 server.ts B-03(client_id:"constructor" 取到函数当密钥打死进程)是同一错误 // 模式在不同文件复发。修法:Object.prototype.hasOwnProperty.call。 // // 本用例即「注入即红」:把实现改回 `in`,下面三条必须全部失败。 // --------------------------------------------------------------------------- test('A-P0-1:原型键(constructor/toString/valueOf/hasOwnProperty)必须被判 additional property', () => { const schema = { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false, }; for (const protoKey of ['constructor', 'toString', 'valueOf', 'hasOwnProperty', '__proto__', 'isPrototypeOf']) { const errs = validate({ [protoKey]: 1 }, schema); assert.ok( errs.length > 0, `传入原型键 ${protoKey} 必须被判 additional property(in 运算符会沿原型链放行它 —— fail-open)`, ); assert.match(errs.join(';'), new RegExp(protoKey), `错误应指出是哪个键:${errs.join('; ')}`); } // 正常键不受影响(防「修过头把合法字段也拒了」) assert.deepEqual(validate({ name: 'ok' }, schema), []); }); test('A-P0-1:真实 manifest 能力(system.agent.update)同样拒原型键', async () => { const { findCapability } = await import('../../packages/protocol/dist/index.js'); const cap = findCapability('system.agent.update'); assert.ok(cap, '能力应存在'); // 合法调用必须照过(回归保护:修守卫不能误伤正常请求) assert.deepEqual(validate({ url: 'http://x/y', sha256: 'a'.repeat(64) }, cap.params_schema), []); // 原型键必须被拒 for (const protoKey of ['constructor', 'toString']) { const errs = validate({ url: 'http://x/y', sha256: 'a'.repeat(64), [protoKey]: 'x' }, cap.params_schema); assert.ok(errs.length > 0, `${protoKey} 必须被拒`); } });