'use strict' const assert = require('node:assert/strict') const { execFileSync } = require('node:child_process') const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const test = require('node:test') const { addWorktree, ensureGitRepo, listBranches, parseWorktrees, sanitizeBranch, switchBranch } = require('./git-worktree-ops.cjs') test('sanitizeBranch: spaces → hyphens, forbidden chars dropped, edges trimmed', () => { assert.equal(sanitizeBranch('beach vibes'), 'beach-vibes') assert.equal(sanitizeBranch('feat/cool thing'), 'feat/cool-thing') assert.equal(sanitizeBranch(' wip~^:? '), 'wip') assert.equal(sanitizeBranch('///'), '') }) test('parseWorktrees: main checkout + linked worktree', () => { const out = [ 'worktree /repo', 'HEAD abc123', 'branch refs/heads/main', '', 'worktree /repo/.worktrees/feat', 'HEAD def456', 'branch refs/heads/hermes/feat', '' ].join('\n') const trees = parseWorktrees(out) assert.equal(trees.length, 2) assert.equal(trees[0].path, '/repo') assert.equal(trees[0].branch, 'main') assert.equal(trees[1].path, '/repo/.worktrees/feat') assert.equal(trees[1].branch, 'hermes/feat') }) test('parseWorktrees: detached + locked flags', () => { const out = ['worktree /repo/wt', 'HEAD abc', 'detached', 'locked reason', ''].join('\n') const trees = parseWorktrees(out) assert.equal(trees.length, 1) assert.equal(trees[0].detached, true) assert.equal(trees[0].locked, true) assert.equal(trees[0].branch, null) }) test('parseWorktrees: empty input', () => { assert.deepEqual(parseWorktrees(''), []) }) test('ensureGitRepo: inits a plain dir with a root commit so worktrees branch', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-wt-')) const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim() try { await ensureGitRepo('git', dir) assert.match(git('rev-parse', '--verify', 'HEAD'), /^[0-9a-f]{7,}$/) // The whole point: a worktree can now branch off the seeded root commit. execFileSync('git', ['worktree', 'add', '-b', 'wt', path.join(dir, '.worktrees', 'wt')], { cwd: dir }) assert.ok(fs.existsSync(path.join(dir, '.worktrees', 'wt'))) // Idempotent: an already-committed repo gets no extra commit. await ensureGitRepo('git', dir) assert.equal(git('rev-list', '--count', 'HEAD'), '1') } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('switchBranch: switches a normal checkout branch', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-switch-')) const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim() try { await ensureGitRepo('git', dir) execFileSync('git', ['branch', 'feature'], { cwd: dir }) await switchBranch(dir, 'feature', 'git') assert.equal(git('branch', '--show-current'), 'feature') } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('listBranches: lists locals and flags the checked-out branch', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-')) try { await ensureGitRepo('git', dir) const current = execFileSync('git', ['branch', '--show-current'], { cwd: dir }).toString().trim() execFileSync('git', ['branch', 'feature'], { cwd: dir }) const branches = await listBranches(dir, 'git') const names = branches.map(b => b.name).sort() assert.deepEqual(names, [current, 'feature'].sort()) // The repo's own checkout is flagged; the unused branch is convertible. assert.equal(branches.find(b => b.name === current).checkedOut, true) assert.equal(branches.find(b => b.name === current).isDefault, true) assert.equal(fs.realpathSync(branches.find(b => b.name === current).worktreePath), fs.realpathSync(dir)) assert.equal(branches.find(b => b.name === 'feature').checkedOut, false) assert.equal(branches.find(b => b.name === 'feature').isDefault, false) assert.equal(branches.find(b => b.name === 'feature').worktreePath, null) } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('listBranches: flags a free default branch as default, not checked out', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-default-')) const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim() try { await ensureGitRepo('git', dir) const trunk = git('branch', '--show-current') execFileSync('git', ['switch', '-c', 'rawr'], { cwd: dir }) const branches = await listBranches(dir, 'git') const defaultBranch = branches.find(b => b.name === trunk) assert.equal(defaultBranch.checkedOut, false) assert.equal(defaultBranch.isDefault, true) assert.equal(defaultBranch.worktreePath, null) } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('listBranches: a branch claimed by a worktree is flagged checked out', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-wt-')) try { await ensureGitRepo('git', dir) execFileSync('git', ['branch', 'feature'], { cwd: dir }) // addWorktree converts the existing "feature" branch into a worktree. const result = await addWorktree(dir, { existingBranch: 'feature' }, 'git') assert.equal(result.branch, 'feature') assert.ok(fs.existsSync(result.path)) const branches = await listBranches(dir, 'git') assert.equal(branches.find(b => b.name === 'feature').checkedOut, true) } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('listBranches: empty on a non-repo path', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-nonrepo-')) try { assert.deepEqual(await listBranches(dir, 'git'), []) } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('addWorktree: existingBranch checks the branch out without a new branch', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-convert-')) const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim() try { await ensureGitRepo('git', dir) execFileSync('git', ['branch', 'cool/feature'], { cwd: dir }) const before = git('branch', '--list').split('\n').length const result = await addWorktree(dir, { existingBranch: 'cool/feature' }, 'git') // No new branch was created — only the existing one is checked out. assert.equal(git('branch', '--list').split('\n').length, before) assert.equal(result.branch, 'cool/feature') // Dir is named off the branch slug, nested under the main repo's .worktrees. assert.match(result.path, /[/\\]\.worktrees[/\\]cool-feature/) assert.equal( execFileSync('git', ['branch', '--show-current'], { cwd: result.path }).toString().trim(), 'cool/feature' ) } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) test('addWorktree: existing default branch switches the main checkout, not .worktrees/main', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-convert-default-')) const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim() try { await ensureGitRepo('git', dir) const trunk = git('branch', '--show-current') execFileSync('git', ['switch', '-c', 'rawr'], { cwd: dir }) const result = await addWorktree(dir, { existingBranch: trunk }, 'git') assert.equal(result.branch, trunk) assert.equal(fs.realpathSync(result.path), fs.realpathSync(dir)) assert.equal(git('branch', '--show-current'), trunk) assert.equal(fs.existsSync(path.join(dir, '.worktrees', trunk)), false) } finally { fs.rmSync(dir, { recursive: true, force: true }) } })