feat(writing-studio): add snapshot manager with diff and prune

Amit Haridas
This commit is contained in:
2026-04-23 22:55:12 +05:30
parent 4da5b7b9c4
commit 2a6f0fc302
2 changed files with 146 additions and 0 deletions
@@ -0,0 +1,73 @@
class SnapshotManager {
/**
* @param {object} store - { get(key), set(key, value) }
* @param {string} storeKey - settings key for snapshots
*/
constructor(store, storeKey = 'plugins.writing-studio.snapshots') {
this.store = store;
this.storeKey = storeKey;
}
_getAll() {
const raw = this.store.get(this.storeKey);
return raw ? JSON.parse(raw) : [];
}
_saveAll(snaps) {
this.store.set(this.storeKey, JSON.stringify(snaps));
}
create(content, label = 'manual') {
const snaps = this._getAll();
const snap = {
id: 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
timestamp: new Date().toISOString(),
content,
wordCount: content.split(/\s+/).filter(Boolean).length,
label
};
snaps.unshift(snap);
this._saveAll(snaps);
return snap;
}
list() {
return this._getAll();
}
getById(id) {
return this._getAll().find(s => s.id === id) || null;
}
restore(id) {
const snap = this.getById(id);
if (!snap) throw new Error('Snapshot not found');
return snap.content;
}
delete(id) {
const snaps = this._getAll().filter(s => s.id !== id);
this._saveAll(snaps);
}
diff(id, currentContent) {
const snap = this.getById(id);
if (!snap) throw new Error('Snapshot not found');
const oldLines = snap.content.split('\n');
const newLines = currentContent.split('\n');
const oldSet = new Set(oldLines);
const newSet = new Set(newLines);
let added = 0;
let removed = 0;
for (const line of newLines) { if (!oldSet.has(line)) added++; }
for (const line of oldLines) { if (!newSet.has(line)) removed++; }
return { added, removed };
}
prune(keepCount) {
const snaps = this._getAll();
this._saveAll(snaps.slice(0, keepCount));
}
}
module.exports = { SnapshotManager };
+73
View File
@@ -0,0 +1,73 @@
const { SnapshotManager } = require('../src/plugins/built-in/writing-studio/snapshot-manager');
describe('SnapshotManager', () => {
let manager;
let store;
beforeEach(() => {
store = {};
manager = new SnapshotManager({
get: (key) => store[key],
set: (key, value) => { store[key] = value; }
});
});
test('create stores snapshot with timestamp, content, wordCount', () => {
const snap = manager.create('Hello world this is a test', 'auto');
expect(snap).toHaveProperty('id');
expect(snap.content).toBe('Hello world this is a test');
expect(snap.wordCount).toBe(6);
expect(snap.label).toBe('auto');
});
test('list returns snapshots ordered newest first', () => {
manager.create('first', 'auto');
manager.create('second', 'auto');
const list = manager.list();
expect(list.length).toBe(2);
expect(list[0].content).toBe('second');
});
test('getById returns specific snapshot', () => {
const snap = manager.create('find me', 'manual');
const found = manager.getById(snap.id);
expect(found.content).toBe('find me');
});
test('getById returns null for missing id', () => {
expect(manager.getById('nope')).toBeNull();
});
test('restore returns content of snapshot', () => {
const snap = manager.create('restore this', 'manual');
expect(manager.restore(snap.id)).toBe('restore this');
});
test('restore throws for missing snapshot', () => {
expect(() => manager.restore('nope')).toThrow('Snapshot not found');
});
test('delete removes a snapshot', () => {
const snap = manager.create('delete me', 'auto');
manager.delete(snap.id);
expect(manager.getById(snap.id)).toBeNull();
});
test('diff returns added/removed line counts', () => {
const snap = manager.create('line one\nline two\nline three', 'auto');
const result = manager.diff(snap.id, 'line one\nline modified\nline three\nline four');
expect(result.added).toBe(2);
expect(result.removed).toBe(1);
});
test('diff throws for missing snapshot', () => {
expect(() => manager.diff('nope', 'new content')).toThrow('Snapshot not found');
});
test('prune keeps only the N most recent snapshots', () => {
for (let i = 0; i < 10; i++) manager.create('snap ' + i, 'auto');
manager.prune(5);
expect(manager.list().length).toBe(5);
expect(manager.list()[0].content).toBe('snap 9');
});
});