diff --git a/src/plugins/built-in/writing-studio/snapshot-manager.js b/src/plugins/built-in/writing-studio/snapshot-manager.js new file mode 100644 index 0000000..7b174d7 --- /dev/null +++ b/src/plugins/built-in/writing-studio/snapshot-manager.js @@ -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 }; diff --git a/tests/snapshot-manager.test.js b/tests/snapshot-manager.test.js new file mode 100644 index 0000000..56e930e --- /dev/null +++ b/tests/snapshot-manager.test.js @@ -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'); + }); +});