445a46139f
- checksum.test.ts: 6 tests covering crc32hex determinism + 8-char-hex format + canonicalJSON recursive key sort + array-order preservation (Pitfall 3) - envelope.test.ts: 9 tests covering wrap/unwrap round-trip + tamper detection + Zod schema validation (incl synthetic v0 schemaVersion 0) - migrations.test.ts: 6 tests covering CURRENT_SCHEMA_VERSION = 1 + the load-bearing synthetic v0 -> v1 shape per CONTEXT D-04 + future/negative version throws + spy-confirmed registry invocation (RESEARCH Pitfall 7) RED phase per TDD plan-level gate. Tests fail because impl files do not exist yet.
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
wrap,
|
|
unwrap,
|
|
SaveCorruptError,
|
|
SaveEnvelopeSchema,
|
|
type SaveEnvelope,
|
|
} from './envelope';
|
|
|
|
// Tests for the SaveEnvelope wrap/unwrap pair. The envelope is the load-bearing
|
|
// shape from CLAUDE.md: `{schemaVersion, payload, checksum}`. Tampering or
|
|
// lossy-storage corruption is detected via CRC-32 mismatch on unwrap.
|
|
|
|
describe('wrap', () => {
|
|
it('returns an envelope with schemaVersion, payload, and 8-char hex checksum', () => {
|
|
const env = wrap({ foo: 'bar' }, 1);
|
|
expect(env.schemaVersion).toBe(1);
|
|
expect(env.payload).toEqual({ foo: 'bar' });
|
|
expect(env.checksum).toMatch(/^[0-9a-f]{8}$/);
|
|
});
|
|
});
|
|
|
|
describe('unwrap', () => {
|
|
it('round-trips several payload shapes', () => {
|
|
const shapes: unknown[] = [
|
|
{ foo: 'bar' },
|
|
{ nested: { a: 1, b: { c: [1, 2, 3] } } },
|
|
{ garden: { tiles: [{ id: 'tile-1' }] }, plants: [] },
|
|
[1, 2, 3],
|
|
{ empty: {} },
|
|
];
|
|
for (const p of shapes) {
|
|
expect(unwrap(wrap(p, 1))).toEqual(p);
|
|
}
|
|
});
|
|
|
|
it('throws SaveCorruptError when checksum is tampered', () => {
|
|
const env = wrap({ x: 1 }, 1);
|
|
const tampered: SaveEnvelope<unknown> = { ...env, checksum: 'deadbeef' };
|
|
let caught: unknown = null;
|
|
try {
|
|
unwrap(tampered);
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
expect(caught).toBeInstanceOf(SaveCorruptError);
|
|
const err = caught as SaveCorruptError;
|
|
expect(err.expected).toBe('deadbeef');
|
|
expect(err.actual).toBe(env.checksum);
|
|
});
|
|
|
|
it('throws SaveCorruptError when payload is tampered (checksum mismatch)', () => {
|
|
const env = wrap({ x: 1 }, 1);
|
|
const tampered: SaveEnvelope<unknown> = { ...env, payload: { x: 2 } };
|
|
expect(() => unwrap(tampered)).toThrow(SaveCorruptError);
|
|
});
|
|
});
|
|
|
|
describe('SaveEnvelopeSchema', () => {
|
|
it('accepts a valid envelope', () => {
|
|
const env = wrap({ foo: 'bar' }, 1);
|
|
expect(SaveEnvelopeSchema.safeParse(env).success).toBe(true);
|
|
});
|
|
|
|
it('accepts schemaVersion 0 (synthetic v0 per CONTEXT D-05)', () => {
|
|
const env = { schemaVersion: 0, payload: {}, checksum: '00000000' };
|
|
expect(SaveEnvelopeSchema.safeParse(env).success).toBe(true);
|
|
});
|
|
|
|
it('rejects malformed envelopes (missing keys)', () => {
|
|
const noChecksum = { schemaVersion: 1, payload: {} };
|
|
const noVersion = { payload: {}, checksum: '00000000' };
|
|
expect(SaveEnvelopeSchema.safeParse(noChecksum).success).toBe(false);
|
|
expect(SaveEnvelopeSchema.safeParse(noVersion).success).toBe(false);
|
|
});
|
|
|
|
it('rejects malformed envelopes (non-hex checksum)', () => {
|
|
const bad = { schemaVersion: 1, payload: {}, checksum: 'NOT-HEX!' };
|
|
expect(SaveEnvelopeSchema.safeParse(bad).success).toBe(false);
|
|
});
|
|
|
|
it('rejects negative schemaVersion', () => {
|
|
const bad = { schemaVersion: -1, payload: {}, checksum: '00000000' };
|
|
expect(SaveEnvelopeSchema.safeParse(bad).success).toBe(false);
|
|
});
|
|
});
|