Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions tests/unit/tracker_serialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

import { describe, it } from 'node:test';
import assert from 'node:assert';
import { ModificationTracker } from '../../src/core/undo-history';
import { LabeledModification } from '../../src/core/types';

describe('ModificationTracker Serialization', () => {

it('should serialize and deserialize an empty tracker', () => {
const tracker = new ModificationTracker<LabeledModification>();
const serialized = tracker.serialize();

assert.ok(serialized instanceof Uint8Array);
assert.ok(serialized.length > 0);

const restored = ModificationTracker.deserialize<LabeledModification>(serialized);

assert.strictEqual(restored.entryCount, 0);
assert.strictEqual(restored.canStepBack, false);
assert.strictEqual(restored.canStepForward, false);
});

it('should serialize and deserialize a tracker with simple modifications', () => {
const tracker = new ModificationTracker<LabeledModification>();
const mod: LabeledModification = {
label: 'Test Mod',
description: 'Insert row',
modificationType: 'row_insert',
targetTable: 'users',
newValue: 'Alice'
};

tracker.record(mod);

const serialized = tracker.serialize();
const restored = ModificationTracker.deserialize<LabeledModification>(serialized);

assert.strictEqual(restored.entryCount, 1);

const lastMod = restored.stepBack();
assert.ok(lastMod);
assert.strictEqual(lastMod?.label, 'Test Mod');
assert.strictEqual(lastMod?.newValue, 'Alice');
});

it('should properly serialize and deserialize Uint8Array data', () => {
const tracker = new ModificationTracker<LabeledModification>();

// Create a Uint8Array with some data
const binaryData = new Uint8Array([1, 2, 3, 255, 0]);

const mod: LabeledModification = {
label: 'Binary Mod',
description: 'Update blob',
modificationType: 'cell_update',
targetTable: 'files',
priorValue: binaryData,
newValue: new Uint8Array([10, 20, 30])
};

tracker.record(mod);

const serialized = tracker.serialize();
const restored = ModificationTracker.deserialize<LabeledModification>(serialized);

assert.strictEqual(restored.entryCount, 1);

const lastMod = restored.stepBack();
assert.ok(lastMod);

// Verify priorValue
const restoredBinary = lastMod?.priorValue;
assert.ok(restoredBinary instanceof Uint8Array, 'priorValue should be Uint8Array');
assert.deepStrictEqual(restoredBinary, binaryData);

// Verify newValue
const restoredNewValue = lastMod?.newValue;
assert.ok(restoredNewValue instanceof Uint8Array, 'newValue should be Uint8Array');
assert.deepStrictEqual(restoredNewValue, new Uint8Array([10, 20, 30]));
});

it('should preserve checkpoint index', async () => {
const tracker = new ModificationTracker<LabeledModification>();
tracker.record({
label: '1', description: '1', modificationType: 'row_insert'
});

await tracker.createCheckpoint();

tracker.record({
label: '2', description: '2', modificationType: 'row_insert'
});

assert.strictEqual(tracker.hasUncommittedChanges(), true);

const serialized = tracker.serialize();
const restored = ModificationTracker.deserialize<LabeledModification>(serialized);

// Checkpoint index should be preserved (at 1)
// Total entries 2
assert.strictEqual(restored.entryCount, 2);

const uncommitted = restored.getUncommittedEntries();
assert.strictEqual(uncommitted.length, 1);
assert.strictEqual(uncommitted[0].label, '2');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test suite provides good coverage for the happy-path scenarios. However, it would be beneficial to add a test case for robustness to handle malformed payloads. The current deserialize implementation is susceptible to creating a corrupted ModificationTracker state if the serialized data, while being valid JSON, has properties with incorrect types (e.g., a string for timeline instead of an array). This could lead to unexpected runtime errors.

I recommend adding a test to ensure deserialize handles such invalid data gracefully, for instance by initializing a clean, empty tracker.

Here's a suggested test case to add to the suite:

it('should be robust against invalid payload structure', () => {
    const invalidPayload = {
        timeline: 'this-should-be-an-array',
        checkpointIndex: 'this-should-be-a-number'
    };
    const serialized = new TextEncoder().encode(JSON.stringify(invalidPayload));
    const restored = ModificationTracker.deserialize<LabeledModification>(serialized);

    // A robust implementation should result in a clean state, not a corrupted one.
    assert.strictEqual(restored.entryCount, 0, 'entryCount should be 0 for invalid timeline type');
    assert.strictEqual(restored.hasUncommittedChanges(), false, 'should have no uncommitted changes for invalid checkpointIndex type');
});

This test would fail with the current implementation, highlighting a fragility that should be addressed to make the deserialization process more resilient.

});