test(01-03): add failing tests for save core (checksum, envelope, migrations) [RED]

- 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.
This commit is contained in:
2026-05-08 23:27:34 -04:00
parent 1e99356b27
commit 445a46139f
3 changed files with 185 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from 'vitest';
import { migrate, CURRENT_SCHEMA_VERSION, migrations } from './migrations';
// Tests for the forward-only migration registry. The synthetic v0 → v1
// migration (CONTEXT D-05) is the load-bearing one — Phase 4's real
// migrate_v1_to_v2 will follow the exact same shape.
describe('CURRENT_SCHEMA_VERSION', () => {
it('is 1 in Phase 1 (sanity)', () => {
expect(CURRENT_SCHEMA_VERSION).toBe(1);
});
});
describe('migrate (synthetic v0 → v1 per CONTEXT D-04 + D-05)', () => {
it('synthetic v0 payload migrates to v1 shape', () => {
const v0 = { garden: [{ id: 'tile-1' }, { id: 'tile-2' }] };
const result = migrate(v0, 0);
expect(result.toVersion).toBe(1);
expect(result.payload).toMatchObject({
garden: { tiles: [{ id: 'tile-1' }, { id: 'tile-2' }] },
plants: [],
harvestedFragmentIds: [],
lastTickAt: expect.any(Number),
settings: {
musicVolume: expect.any(Number),
ambientVolume: expect.any(Number),
sfxVolume: expect.any(Number),
},
});
});
it('migrating from v1 is a no-op (returns payload unchanged at toVersion 1)', () => {
const v1 = {
garden: { tiles: [] },
plants: [],
harvestedFragmentIds: [],
lastTickAt: 1234567890,
settings: { musicVolume: 0.7, ambientVolume: 0.5, sfxVolume: 0.8 },
};
const result = migrate(v1, 1);
expect(result.toVersion).toBe(1);
expect(result.payload).toEqual(v1);
});
it('throws when fromVersion is in the future (no migration registered)', () => {
expect(() => migrate({}, 99)).toThrow();
});
it('throws when fromVersion is negative', () => {
expect(() => migrate({}, -1)).toThrow();
});
it('invokes migrations[1] exactly once when migrating v0 → v1', () => {
const original = migrations[1];
const spy = vi.fn(original);
migrations[1] = spy;
try {
migrate({ garden: [] }, 0);
expect(spy).toHaveBeenCalledTimes(1);
} finally {
migrations[1] = original;
}
});
});