| … | … | @@ -0,0 +1,75 @@ |
| 1 | import test from 'brittle' |
| 2 | import fs from 'bare-fs' |
| 3 | import path from 'bare-path' |
| 4 | import os from 'bare-os' |
| 5 | import Corestore from 'corestore' |
| 6 | import Hyperbee from 'hyperbee' |
| 7 | import { DriveManager } from '../../lib/drive-manager.js' |
| 8 | import { NoteManager } from '../../lib/note-manager.js' |
| 9 | |
| 10 | function tmpSetup () { |
| 11 | const dir = path.join(os.tmpdir(), 'hn-rename-' + Date.now() + '-' + Math.random().toString(36).slice(2)) |
| 12 | fs.mkdirSync(dir, { recursive: true }) |
| 13 | const store = new Corestore(dir) |
| 14 | const registry = new Hyperbee(store.get({ name: 'registry' }), { keyEncoding: 'utf-8', valueEncoding: 'json' }) |
| 15 | const dm = new DriveManager(store, registry) |
| 16 | return { store, registry, dm, dir } |
| 17 | } |
| 18 | |
| 19 | function rmrf (dir) { |
| 20 | try { |
| 21 | for (const f of fs.readdirSync(dir)) { |
| 22 | const fp = path.join(dir, f) |
| 23 | if (fs.statSync(fp).isDirectory()) rmrf(fp) |
| 24 | else fs.unlinkSync(fp) |
| 25 | } |
| 26 | fs.rmdirSync(dir) |
| 27 | } catch {} |
| 28 | } |
| 29 | |
| 30 | test('renameNotebook — happy path moves metadata + all notes, removes old', async (t) => { |
| 31 | const { registry, dm, dir } = tmpSetup() |
| 32 | await registry.ready() |
| 33 | const nm = new NoteManager(dm) |
| 34 | |
| 35 | await nm.createNotebook('work', { name: 'Work Notes' }) |
| 36 | await nm.createNote('work', { title: 'One', content: '# One' }) |
| 37 | await nm.createNote('work', { title: 'Two', content: '# Two' }) |
| 38 | const before = (await nm.listNotes('work')).length |
| 39 | t.ok(before >= 2, 'at least the two created notes are present') |
| 40 | |
| 41 | const renamed = await nm.renameNotebook('work', 'tasks') |
| 42 | t.is(renamed.slug, 'tasks', 'returns new slug (newMeta in scope)') |
| 43 | |
| 44 | t.ok(await nm.getNotebook('tasks'), 'new notebook exists') |
| 45 | t.absent(await nm.getNotebook('work'), 'old notebook removed') |
| 46 | const notes = await nm.listNotes('tasks') |
| 47 | t.is(notes.length, before, 'all notes copied to new notebook (count preserved)') |
| 48 | |
| 49 | clearInterval(nm._keyCacheCleanupInterval) |
| 50 | rmrf(dir) |
| 51 | }) |
| 52 | |
| 53 | test('renameNotebook — Phase-2 delete failure leaves the OLD notebook intact (no data loss)', async (t) => { |
| 54 | const { registry, dm, dir } = tmpSetup() |
| 55 | await registry.ready() |
| 56 | const nm = new NoteManager(dm) |
| 57 | |
| 58 | await nm.createNotebook('work', { name: 'Work Notes' }) |
| 59 | await nm.createNote('work', { title: 'One', content: '# One' }) |
| 60 | await nm.createNote('work', { title: 'Two', content: '# Two' }) |
| 61 | const before = (await nm.listNotes('work')).length |
| 62 | |
| 63 | // Force the commit-phase delete to fail AFTER the copy succeeded. |
| 64 | nm.deleteNotebook = async () => { throw new Error('simulated delete failure') } |
| 65 | |
| 66 | await t.exception(() => nm.renameNotebook('work', 'tasks'), /removing old/, 'rename surfaces the failure') |
| 67 | |
| 68 | // The critical property: the original notebook + its notes are NOT corrupted. |
| 69 | t.ok(await nm.getNotebook('work'), 'old notebook still exists after failed commit') |
| 70 | const oldNotes = await nm.listNotes('work') |
| 71 | t.is(oldNotes.length, before, 'old notes intact — no data loss') |
| 72 | |
| 73 | clearInterval(nm._keyCacheCleanupInterval) |
| 74 | rmrf(dir) |
| 75 | }) |