test(01-03): add failing tests for Base64 codec + full round-trip [RED]

- round-trip.test.ts (3 tests): full pipeline EXPORT -> IMPORT -> MIGRATE
  -> WRAP -> UNWRAP -> IDB PUT -> IDB GET exercising every save layer
  file end-to-end (CORE-09 + CORE-04 + CORE-06 + CORE-07); plus DoS-cap
  rejection at MAX_IMPORT_BYTES + 1; plus malformed-Base64 rejection

RED phase per TDD plan-level gate. Tests fail because codec.ts does not
exist yet.
This commit is contained in:
2026-05-08 23:37:13 -04:00
parent 0b1425d4f6
commit bec0df1dc2
+87
View File
@@ -0,0 +1,87 @@
import { describe, it, expect, beforeEach } from 'vitest';
import 'fake-indexeddb/auto';
import { wrap, unwrap } from './envelope';
import { migrate, CURRENT_SCHEMA_VERSION } from './migrations';
import { exportToBase64, importFromBase64 } from './codec';
import { openSaveDB } from './db';
// CORE-09 + CORE-04 + CORE-06 + CORE-07: full save round-trip exercising
// every save layer file end-to-end. This is the load-bearing integration
// test for Phase 1 — if this passes, Phase 2 can reasonably trust that
// the save subsystem is wired correctly.
beforeEach(async () => {
// Same store-contents reset pattern as the unit tests — see db.test.ts
// and snapshots.test.ts for why we don't deleteDatabase.
const db = await openSaveDB();
for (const store of ['saves', 'save_snapshots'] as const) {
const all = await db.getAll(store);
for (const e of all) {
await db.delete(store, e.id);
}
}
});
describe('CORE-09 + CORE-04 + CORE-06 + CORE-07: full save round-trip', () => {
it('synthetic v0 envelope migrates, round-trips through Base64, validates, persists', async () => {
// Pretend a player had an old v0 save lying around (CONTEXT D-05 synthetic v0).
const v0Payload = { garden: [{ id: 'tile-1' }, { id: 'tile-2' }] };
// v0 envelope: schemaVersion 0, with a placeholder checksum that we won't
// verify (the v0 era didn't have our checksum scheme, but the schema
// accepts it because checksum just has to be 8 hex chars).
const v0Envelope = {
schemaVersion: 0,
payload: v0Payload,
checksum: '00000000', // 8-char hex placeholder
};
// EXPORT through Base64 codec
const exported = exportToBase64(v0Envelope);
expect(exported.length).toBeGreaterThan(0);
// IMPORT (simulating a fresh browser) — note: import returns a parsed
// envelope that PASSES our SaveEnvelopeSchema (schemaVersion 0 is allowed
// since z.number().nonnegative()).
const imported = importFromBase64(exported);
expect(imported.schemaVersion).toBe(0);
// MIGRATE the imported payload
const { payload, toVersion } = migrate(imported.payload, imported.schemaVersion);
expect(toVersion).toBe(CURRENT_SCHEMA_VERSION);
expect(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),
},
});
// WRAP with current version and a valid checksum, UNWRAP to verify
const v1Envelope = wrap(payload, toVersion);
expect(unwrap(v1Envelope)).toEqual(payload);
// PERSIST to IDB and read back (CORE-04)
const db = await openSaveDB();
await db.put('saves', {
id: 'main',
envelope: v1Envelope,
savedAt: new Date().toISOString(),
});
const retrieved = await db.get('saves', 'main');
expect(retrieved?.envelope).toEqual(v1Envelope);
expect(unwrap(retrieved!.envelope)).toEqual(payload);
});
it('rejects oversized Base64 import (DoS cap)', () => {
const huge = 'A'.repeat(50 * 1024 * 1024 + 1);
expect(() => importFromBase64(huge)).toThrow(/exceeds/);
});
it('rejects malformed Base64', () => {
expect(() => importFromBase64('not-valid-base64-)(*&^%$')).toThrow();
});
});