/** * 行所有权(row ownership)工具测试 * * 背景(审计 A 类缺陷):Memory/KVStore/Hybrid 曾把内部行对象直接交给调用方, * 调用方一次原地修改就能改写存储、让索引与行失配(改过的行再也查不出来)。 * `cloneRow`/`cloneRows` 是这条不变量的唯一实现。 * * v0.8.0 审查:这三个函数原本放在 `engine/interface.ts`(被描述为"纯类型、 * 不纳入覆盖率")→ 真实实现代码逃过覆盖率统计。搬到 `engine/row_clone.ts` 后 * 覆盖率立刻暴露:退化路径(`cloneRowFallback`,即 `structuredClone` 不可用/ * 抛错时的逐层复制)**从未被测试**。本文件补上这两条路径。 */ import { describe, it, expect, afterEach } from '@jest/globals'; import { cloneRow, cloneRows } from '../../src/engine/row_clone'; const originalStructuredClone = (globalThis as { structuredClone?: unknown }).structuredClone; afterEach(() => { (globalThis as { structuredClone?: unknown }).structuredClone = originalStructuredClone; }); describe('cloneRow — structuredClone 可用', () => { it('深拷贝普通对象与嵌套结构,修改副本不影响源对象', () => { const source = { id: 'a', v: 1, nested: { deep: [1, 2, { x: true }] } }; const copy = cloneRow(source); expect(copy).toEqual(source); expect(copy).not.toBe(source); expect(copy.nested).not.toBe(source.nested); copy.nested.deep.push(99); copy.v = 42; expect(source.v).toBe(1); expect(source.nested.deep).toHaveLength(3); }); it('JSON 安全值深拷贝且逐层独立(行的实际契约)', () => { // 引擎的读路径只承载 validateRow 之后的 JSON 安全值 —— `date` 列在 schema 层 // 就要求**字符串**(`src/table/validation.ts` 的 'date' 分支),因此行里不会 // 出现 Date 实例。这里断言的是真实契约:值相等 + 逐层不共享引用。 // (不在此处断言 Date 语义:jsdom 的 structuredClone 是 JSON 化的 polyfill, // 会把 Date 变成字符串;真实浏览器/Node 的 structuredClone 保留 Date, // 而退化路径的实现也显式保留 —— 见下方 fallback 用例。) const source = { id: 'a', when: '2026-09-15T00:00:00.000Z', nested: { k: [1, 2] } }; const copy = cloneRow(source); expect(copy).toEqual(source); expect(copy.nested.k).not.toBe(source.nested.k); copy.nested.k.push(3); expect(source.nested.k).toEqual([1, 2]); }); it('非对象输入原样返回(null / 数字 / 字符串不参与拷贝)', () => { expect(cloneRow(null as never)).toBeNull(); expect(cloneRow(7 as never)).toBe(7); expect(cloneRow('x' as never)).toBe('x'); }); }); describe('cloneRow — structuredClone 不可用(退化路径)', () => { it('无 structuredClone 时逐层复制:嵌套对象 / 数组 / Date / TypedArray / ArrayBuffer', () => { (globalThis as { structuredClone?: unknown }).structuredClone = undefined; const source = { id: 'a', nested: { list: [1, { k: 'v' }] }, when: new Date('2026-01-02T03:04:05.000Z'), bytes: new Uint8Array([1, 2, 3]), raw: new Uint8Array([9, 8]).buffer, plain: 'str', num: 5, flag: false, nil: null, }; const copy = cloneRow(source); expect(copy).toEqual(source); expect(copy).not.toBe(source); expect(copy.nested).not.toBe(source.nested); expect(copy.nested.list).not.toBe(source.nested.list); expect(copy.when).toBeInstanceOf(Date); expect(copy.when.getTime()).toBe(source.when.getTime()); expect(copy.bytes).toBeInstanceOf(Uint8Array); expect(Array.from(copy.bytes)).toEqual([1, 2, 3]); expect(copy.bytes).not.toBe(source.bytes); expect(copy.raw).toBeInstanceOf(ArrayBuffer); expect(copy.raw).not.toBe(source.raw); expect(new Uint8Array(copy.raw as ArrayBuffer)).toEqual(new Uint8Array([9, 8])); // 深拷贝语义:改副本不影响源 copy.nested.list.push({ k: 'mutated' }); (copy.bytes as Uint8Array)[0] = 99; expect(source.nested.list).toHaveLength(2); expect(source.bytes[0]).toBe(1); }); it('structuredClone 抛错(含不可克隆值)时也退化到逐层复制', () => { (globalThis as { structuredClone?: unknown }).structuredClone = () => { throw new Error('could not be cloned'); }; const fn = (): number => 1; const source = { id: 'a', fn, nested: { k: 1 } }; const copy = cloneRow(source); expect(copy.id).toBe('a'); expect(copy.fn).toBe(fn); // 函数按引用保留(不可克隆值的合理退化) expect(copy.nested).toEqual({ k: 1 }); expect(copy.nested).not.toBe(source.nested); }); it('退化路径对原始值同样原样返回', () => { (globalThis as { structuredClone?: unknown }).structuredClone = undefined; expect(cloneRow(null as never)).toBeNull(); expect(cloneRow(3 as never)).toBe(3); }); }); describe('cloneRows — 批量拷贝', () => { it('每行都是独立副本(改一行不影响其它行,也不影响源数组)', () => { const rows = [{ id: 'a', tags: ['x'] }, { id: 'b', tags: ['y'] }]; const copies = cloneRows(rows); expect(copies).toEqual(rows); expect(copies).toHaveLength(2); copies[0].tags.push('mutated'); expect(rows[0].tags).toEqual(['x']); expect(copies[0]).not.toBe(rows[0]); expect(copies[1]).not.toBe(rows[1]); }); it('退化路径同样逐行深拷贝', () => { (globalThis as { structuredClone?: unknown }).structuredClone = undefined; const rows = [{ id: 'a', nested: { k: 1 } }]; const copies = cloneRows(rows); copies[0].nested.k = 2; expect(rows[0].nested.k).toBe(1); }); });