Scaffold TypeScript/Vitest project, add DESIGN.md (brief) and CLAUDE.md. Property tests cover escape round-trips, separator-freedom and order preservation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import fc from 'fast-check'
|
|
import { decodeIndexValue, encodeIndexValue, type IndexValue } from '../../src/core/encoding.js'
|
|
|
|
const separators = fc.constantFrom('/', ':', '.', '|', '#', '!')
|
|
|
|
const indexValue: fc.Arbitrary<IndexValue> = fc.oneof(
|
|
fc.constant(null),
|
|
fc.boolean(),
|
|
fc.double({ noNaN: true }),
|
|
fc.integer(),
|
|
fc.string({ unit: 'binary' }),
|
|
fc.string({ unit: fc.constantFrom('/', ':', '0', '1', 'a', '%', '~', '\0', '!') }),
|
|
)
|
|
|
|
const rank = (v: IndexValue) => (v === null ? 0 : v === false ? 1 : v === true ? 2 : typeof v === 'number' ? 3 : 4)
|
|
|
|
function compare(a: IndexValue, b: IndexValue): number {
|
|
const ra = rank(a)
|
|
const rb = rank(b)
|
|
if (ra !== rb) return ra - rb
|
|
if (typeof a === 'number' && typeof b === 'number') return a === b ? 0 : a < b ? -1 : 1
|
|
if (typeof a === 'string' && typeof b === 'string') return a === b ? 0 : a < b ? -1 : 1
|
|
return 0
|
|
}
|
|
|
|
const sign = (n: number) => (n < 0 ? -1 : n > 0 ? 1 : 0)
|
|
const cmpStr = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
|
|
|
describe('encodeIndexValue', () => {
|
|
it('round-trips (property)', () => {
|
|
fc.assert(
|
|
fc.property(indexValue, separators, (v, sep) => {
|
|
const decoded = decodeIndexValue(encodeIndexValue(v, sep), sep)
|
|
if (typeof v === 'number' && v === 0) expect(decoded).toBe(0) // -0 normalised
|
|
else expect(decoded).toEqual(v)
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('emits only characters above the separator (property)', () => {
|
|
fc.assert(
|
|
fc.property(indexValue, separators, (v, sep) => {
|
|
for (const c of encodeIndexValue(v, sep)) expect(c > sep).toBe(true)
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('preserves order, including when followed by a separator and suffix (property)', () => {
|
|
fc.assert(
|
|
fc.property(indexValue, indexValue, fc.string(), fc.string(), separators, (a, b, sa, sb, sep) => {
|
|
const ea = encodeIndexValue(a, sep)
|
|
const eb = encodeIndexValue(b, sep)
|
|
const expected = sign(compare(a, b))
|
|
expect(cmpStr(ea, eb)).toBe(expected)
|
|
if (expected !== 0) expect(cmpStr(ea + sep + sa, eb + sep + sb)).toBe(expected)
|
|
}),
|
|
{ numRuns: 2000 },
|
|
)
|
|
})
|
|
|
|
it('orders types null < false < true < numbers < strings', () => {
|
|
const values: IndexValue[] = ['', 'a', -Infinity, -1, 0, 1e300, Infinity, true, false, null]
|
|
const sorted = [...values].sort((a, b) => cmpStr(encodeIndexValue(a), encodeIndexValue(b)))
|
|
expect(sorted).toEqual([null, false, true, -Infinity, -1, 0, 1e300, Infinity, '', 'a'])
|
|
})
|
|
|
|
it('rejects NaN and non-index values', () => {
|
|
expect(() => encodeIndexValue(NaN)).toThrow()
|
|
expect(() => encodeIndexValue({} as never)).toThrow()
|
|
})
|
|
})
|