import { describe, expect, it } from 'vitest'; import { run, imports } from '../upstream/compile'; import { bundle } from '../upstream/project'; import { schemaCodecs, schemaRegistry } from '../upstream/schema'; import { generateCode } from '../../src/codegen/generator'; import { protobuf_getUnknownFields } from '../../src/runtime'; import { ProtobufCompiler } from '../../src/compiler/compiler'; import { withProject } from '../upstream/project'; import { join } from 'node:path'; describe('SnowLuma compatibility', () => { it('leaves generic data unions and nested business types outside codec discovery', () => { expect( run(` type Json = string | Json[]; interface Field {value:T; metadata:Json;} type Wrapper = Field & { extra?:Json }; function identity(value:Field) {return value;} function generic(field:Field>) {return identity(field);} function result(ok:boolean, value:T) {if(ok) return {ok:true, value}; return {ok:false, value};} const field:Field> = {value:{value:'ok', metadata:[]}, metadata:[]}; export const output = result(true, generic(field)).value.value.value; `).output, ).toBe('ok'); }); it('keeps unrelated callback aliases and inferred containers lazy', () => { expect( run(` type Json = string | Json[]; type Handler = (value:T) => void; interface Field {value:T; metadata:Json;} const field:Field = {value:'ok', metadata:[]}; function wrap(value:Field) {return {get(){return value.value;}};} const handler:Handler = (value) => {}; handler('x'); export const result = wrap(field).get(); `).result, ).toBe('ok'); }); it('propagates composite class bindings through generic factories', () => { expect( run(` interface Box { value:pb<1,T>; } class Codec { encode(value:T) { return protobuf_encode(value); } } function create() { return new Codec>(); } export const result = [...create().encode({value:7})]; `).result, ).toEqual([8, 7]); }); it('defers unrelated class type arguments but validates codec class schemas', () => { expect( run(` type Json = string | Json[]; class Field { value(): null { return null; } } function field() { return new Field(); } export const result = field().value(); `).result, ).toBeNull(); expect(() => run(` class Codec { encode(value:T) {return protobuf_encode(value);} } new Codec().encode({}); `), ).toThrow(/Unresolved type Missing/); }); it('infers matching generic containers without expanding unrelated recursive data', () => { expect( run(` type Json = string | Json[]; interface Field { value: T; metadata: Json; } function arrayField(field: Field) { return Object.assign({}, { get: () => field.value }); } function wrap(field: Field) { return arrayField(field); } const field: Field = { value:'ok', metadata:[] }; export const result = wrap(field).get(); `).result, ).toBe('ok'); }); it('does not resolve recursive data aliases while looking for class receivers', () => { expect( run(` type Data = { children: T[]; name: string }; type Element = Data; type Wrapped = T; function data(): Wrapped { return { children: [], name: 'ok' }; } export const result = data().name.toUpperCase(); `).result, ).toBe('OK'); }); it('does not resolve ordinary generic return values or data alias members as callbacks', () => { expect( run(` type Id = T; interface Data { value: string; children: Id[]; } const data: Id = {value:'ok', children:[]}; function decode(raw: Uint8Array, callback: () => T): T | null { return callback(); } const raw: Uint8Array = new Uint8Array(); const decoded = decode(raw, () => data); export const result = [decoded.value, data.value]; `).result, ).toEqual(['ok', 'ok']); }); it('does not specialize ambient library functions', async () => { const source = `import { expect } from './external'; const data: Record = {}; expect(data).check();`; await withProject( { 'external.d.ts': 'export declare function expect(value: T): { check(): void };', 'entry.ts': source, }, (directory) => { expect( new ProtobufCompiler().compile(source, join(directory, 'entry.ts')), ).toBeNull(); }, ); }); it('leaves ordinary calls and data receivers outside schema resolution', () => { expect( run(` interface Options { meta?: Record; } function options(value: Options): Options { return value; } const input: Options = { meta: { source: 'test' } }; const copy = options(input); function data(flag: boolean) { if (flag) return { value: 7 }; return { value: 8 }; } function bytes(value: Uint8Array): Uint8Array { return value; } const raw: Uint8Array = new Uint8Array([1]); export const result = [copy.meta.source, data(true).value, bytes(raw).length]; `).result, ).toEqual(['test', 7, 1]); }); it('keeps TypeScript optionality separate from explicit wire presence', () => { const result = run(` interface Msg { plain?: pb<1, uint_32>; flag?: pb<2, bool>; text?: pb<3, string>; data?: pb<4, bytes>; big?: pb<5, uint_64>; forced?: pb_optional<6, uint_32>; } export const wire = [...protobuf_encode({ plain: 0, flag: false, text: '', data: new Uint8Array(), big: 0n, forced: 0, })]; export const decoded = protobuf_decode(new Uint8Array(wire)); `); expect(result.wire).toEqual([48, 0]); expect(result.decoded).toEqual({ plain: null, flag: null, text: null, data: null, big: null, forced: 0, }); }); it('retains aliased markers and presence through exported generic wrappers', async () => { const result = await bundle({ 'schema.ts': `import type { pb as P, pb_optional as O, pb_repeated as R, uint_32 } from '@snowluma/proton'; export interface Box { value?: P<1, T>; } export interface Msg { plain?: P<1, uint_32>; forced?: O<2, uint_32>; items?: R<3, uint_32>; } export interface Outer { inner?: P<1, Box>; }`, 'barrel.ts': `export * from './schema';`, 'wrapper.ts': `${imports} import type {Outer} from './barrel'; export function encode(value:Outer) { return protobuf_encode>(value); }`, 'forward.ts': `import {encode} from './wrapper'; import type {Outer} from './barrel'; export function forward(value:Outer) { return encode(value); }`, 'entry.ts': `${imports} import type {Msg} from './barrel'; import {forward} from './forward'; export const wire = [...forward({inner:{value:{plain:0,forced:0,items:[0,7]}}})];`, }); expect(result.wire).toEqual([10, 8, 10, 6, 16, 0, 24, 0, 24, 7]); }); it.each([true, false])('resolves package exports with tsconfig=%s', async (withConfig) => { const result = await bundle({ ...(withConfig ? { 'tsconfig.json': JSON.stringify({ compilerOptions: { moduleResolution: 'bundler', module: 'esnext' }, }), } : {}), 'node_modules/@fake/schemas/package.json': JSON.stringify({ name: '@fake/schemas', type: 'module', exports: { '.': { types: './index.ts', default: './index.ts' } }, }), 'node_modules/@fake/schemas/index.ts': `export * from './schema';`, 'node_modules/@fake/schemas/schema.ts': `export interface Msg { value:pb<1,uint_32>; }`, 'entry.ts': `${imports} import type {Msg} from '@fake/schemas'; export const result = protobuf_decode(protobuf_encode({value:42}));`, }); expect(result).toEqual({ result: { value: 42 } }); }); const codec = schemaCodecs<{ value: number }>('interface Msg { value:pb<1,uint_32>; }'); it.each([ ['unterminated group', [0x13, 8, 7]], ['mismatched group', [0x13, 0x1c]], ['unexpected group end', [0x14]], ['invalid group tag', [0x13, 0]], ['group payload exceeds frame', [0x13, 0x1a, 4, 0x14]], ['group recursion limit', [...Array(66).fill(0x13), ...Array(66).fill(0x14)]], ] as const)('rejects %s', (_name, bytes) => { expect(() => codec.decode(Uint8Array.from(bytes))).toThrow(/protobuf/); }); it('skips nested groups and retains non-enumerable metadata', () => { const decoded = codec.decode(Uint8Array.from([0x13, 0x1b, 8, 99, 0x1c, 0x14, 8, 7])); expect(decoded).toEqual({ value: 7 }); expect(protobuf_getUnknownFields(decoded)).toEqual([ { fieldNumber: 2, wireType: 3, count: 1, totalByteLength: 5 }, ]); expect(JSON.stringify(decoded)).toBe('{"value":7}'); expect([...codec.encode(decoded)]).toEqual([8, 7]); }); it.each([ [NaN, 0], [0, NaN], [0.5, 1], [0, 0.5], [-1, 1], [1, 0], [0, 3], [0, Infinity], ])('rejects invalid decoder bounds %s..%s', (offset, end) => { const generated = generateCode(schemaRegistry('interface Msg { value:pb<1,uint_32>; }')); const decode = new Function(generated + '\nreturn protobuf_decode_Msg;')(); expect(() => decode(new Uint8Array([8, 1]), offset, end)).toThrow(/bounds/); }); });