import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import { DriveManager } from '../../lib/drive-manager.js'
import { NoteManager } from '../../lib/note-manager.js'
function tmpSetup () {
const dir = path.join(os.tmpdir(), 'hn-ms-' + Date.now() + '-' + Math.random().toString(36).slice(2))
fs.mkdirSync(dir, { recursive: true })
const store = new Corestore(dir)
const registry = new Hyperbee(store.get({ name: 'registry' }), {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
const dm = new DriveManager(store, registry)
return { store, registry, dm, dir }
}
function rmrf (dir) {
try {
for (const f of fs.readdirSync(dir)) {
const fp = path.join(dir, f)
const stat = fs.statSync(fp)
if (stat.isDirectory()) rmrf(fp)
else fs.unlinkSync(fp)
}
fs.rmdirSync(dir)
} catch {}
}
// ==================== MERGE ====================
test('merge — concatenates two notes with separator', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note1 = await nm.createNote('test', { title: 'Part 1', content: 'First content' })
const note2 = await nm.createNote('test', { title: 'Part 2', content: 'Second content' })
const merged = await nm.mergeNotes('test', note1.id, note2.id)
t.is(merged.id, note1.id, 'merged into note1')
t.ok(merged.content.includes('First content'))
t.ok(merged.content.includes('---'))
t.ok(merged.content.includes('Second content'))
// note2 is deleted
const gone = await nm.getNote('test', note2.id)
t.is(gone, null, 'note2 deleted')
// noteCount decremented
const meta = await nm.getNotebook('test')
t.is(meta.noteCount, 1)
await store.close()
rmrf(dir)
})
test('merge — with custom title', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note1 = await nm.createNote('test', { title: 'Alpha', content: 'A content' })
const note2 = await nm.createNote('test', { title: 'Beta', content: 'B content' })
const merged = await nm.mergeNotes('test', note1.id, note2.id, { title: 'Combined' })
t.is(merged.title, 'Combined')
await store.close()
rmrf(dir)
})
test('merge — combines tags (deduplicated)', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note1 = await nm.createNote('test', { title: 'A', content: 'A', tags: ['shared', 'a-only'] })
const note2 = await nm.createNote('test', { title: 'B', content: 'B', tags: ['shared', 'b-only'] })
const merged = await nm.mergeNotes('test', note1.id, note2.id)
t.is(merged.tags.length, 3)
t.ok(merged.tags.includes('shared'))
t.ok(merged.tags.includes('a-only'))
t.ok(merged.tags.includes('b-only'))
await store.close()
rmrf(dir)
})
test('merge — throws for nonexistent note', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note1 = await nm.createNote('test', { title: 'A', content: 'A' })
try {
await nm.mergeNotes('test', note1.id, 'nonexistent')
t.fail('should have thrown')
} catch (err) {
t.is(err.name, 'NotFoundError')
}
await store.close()
rmrf(dir)
})
// ==================== SPLIT ====================
test('split — splits at ## headings', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note = await nm.createNote('test', {
title: 'Big Note',
content: '## Introduction\nHello world\n\n## Methods\nDo stuff\n\n## Results\nGot results',
tags: ['research']
})
const created = await nm.splitNote('test', note.id)
t.is(created.length, 2, 'created 2 new notes (Methods + Results)')
// Original note keeps first section
const original = await nm.getNote('test', note.id)
t.ok(original.content.includes('Hello world'))
t.not(original.content.includes('Do stuff'))
// New notes have the remaining sections
const titles = created.map(n => n.title).sort()
t.ok(titles.includes('Methods'))
t.ok(titles.includes('Results'))
// Tags inherited
for (const n of created) {
t.alike(n.tags, ['research'])
}
// noteCount updated (was 1, now 3)
const meta = await nm.getNotebook('test')
t.is(meta.noteCount, 3)
await store.close()
rmrf(dir)
})
test('split — no headings returns empty', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note = await nm.createNote('test', { title: 'Simple', content: 'Just plain text, no headings.' })
const created = await nm.splitNote('test', note.id)
t.is(created.length, 0, 'nothing to split')
await store.close()
rmrf(dir)
})
test('split — single heading returns empty', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note = await nm.createNote('test', { title: 'One Section', content: '## Only Section\nContent here' })
const created = await nm.splitNote('test', note.id)
t.is(created.length, 0, 'single section, nothing to split')
await store.close()
rmrf(dir)
})
test('split — custom delimiter', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note = await nm.createNote('test', {
title: 'Deep Sections',
content: '### Part A\nContent A\n\n### Part B\nContent B\n\n### Part C\nContent C'
})
const created = await nm.splitNote('test', note.id, { delimiter: '### ' })
t.is(created.length, 2, 'split at ### headings')
await store.close()
rmrf(dir)
})
test('split — throws for nonexistent note', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
try {
await nm.splitNote('test', 'nonexistent')
t.fail('should have thrown')
} catch (err) {
t.is(err.name, 'NotFoundError')
}
await store.close()
rmrf(dir)
})
test('split — preserves content of each section', async (t) => {
const { store, registry, dm, dir } = tmpSetup()
await registry.ready()
const nm = new NoteManager(dm)
await nm.createNotebook('test', { name: 'Test' })
const note = await nm.createNote('test', {
title: 'Multi',
content: 'Preamble text\n\n## Section A\nA content line 1\nA content line 2\n\n## Section B\nB content'
})
const created = await nm.splitNote('test', note.id)
// Original keeps preamble
const original = await nm.getNote('test', note.id)
t.ok(original.content.includes('Preamble text'))
// Section A
const sectionA = created.find(n => n.title === 'Section A')
t.ok(sectionA)
t.ok(sectionA.content.includes('A content line 1'))
t.ok(sectionA.content.includes('A content line 2'))
// Section B
const sectionB = created.find(n => n.title === 'Section B')
t.ok(sectionB)
t.ok(sectionB.content.includes('B content'))
await store.close()
rmrf(dir)
})