OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / test / unit / merge-split.test.js

256 lines · javascript ref:
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)
})