import { describe, expect, it } from 'vitest' import { buildBranchNamePrompt, humanizeBranchSlug, isAutoGeneratedCreatureBranchName, sanitizeBranchSlug, stripConfiguredBranchPrefix } from './branch-name-from-work' describe('sanitizeBranchSlug', () => { it('lowercases and kebab-cases free-form model output', () => { expect(sanitizeBranchSlug('Fix the auth bug')).toBe('fix-the-auth-bug') }) it('caps at four words', () => { expect(sanitizeBranchSlug('add dark mode toggle to settings page')).toBe('add-dark-mode-toggle') }) it('honors a custom word cap', () => { expect(sanitizeBranchSlug('add dark mode toggle', 2)).toBe('add-dark') }) it('collapses punctuation, quotes, and slashes into single hyphens', () => { expect(sanitizeBranchSlug('"feat/login_flow!!"')).toBe('feat-login-flow') }) it('returns empty string when nothing usable remains', () => { expect(sanitizeBranchSlug(' !!! ___ ')).toBe('') expect(sanitizeBranchSlug('')).toBe('') }) }) describe('isAutoGeneratedCreatureBranchName', () => { it('matches a creature name case-insensitively', () => { expect(isAutoGeneratedCreatureBranchName('Nautilus')).toBe(true) expect(isAutoGeneratedCreatureBranchName('octopus')).toBe(true) }) it('matches numbered collision variants', () => { expect(isAutoGeneratedCreatureBranchName('Nautilus-2')).toBe(true) expect(isAutoGeneratedCreatureBranchName('Seahorse-17')).toBe(true) }) it('rejects user-chosen and work-derived names', () => { expect(isAutoGeneratedCreatureBranchName('fix-auth-bug')).toBe(false) expect(isAutoGeneratedCreatureBranchName('my-feature')).toBe(false) expect(isAutoGeneratedCreatureBranchName('')).toBe(false) }) }) describe('stripConfiguredBranchPrefix', () => { it('strips a leaked username prefix the model folded into the slug', () => { expect(stripConfiguredBranchPrefix('tmchow-worktree-creation-spinner', 'tmchow')).toBe( 'worktree-creation-spinner' ) }) it('strips a multi-token custom prefix', () => { expect(stripConfiguredBranchPrefix('my-team-add-logout', 'my-team')).toBe('add-logout') }) it('normalizes the prefix the same way the slug was normalized', () => { expect(stripConfiguredBranchPrefix('jane-doe-fix-auth', 'Jane.Doe')).toBe('fix-auth') }) it('leaves a work-derived name that merely starts with a real word', () => { // The leading word matches no configured prefix, so it is content, not a prefix. expect(stripConfiguredBranchPrefix('add-logout-button', 'tmchow')).toBe('add-logout-button') }) it('does not strip when no prefix is configured', () => { expect(stripConfiguredBranchPrefix('tmchow-fix-auth', null)).toBe('tmchow-fix-auth') expect(stripConfiguredBranchPrefix('tmchow-fix-auth', undefined)).toBe('tmchow-fix-auth') expect(stripConfiguredBranchPrefix('tmchow-fix-auth', '')).toBe('tmchow-fix-auth') }) it('returns empty for prefix-only output so the caller skips the rename', () => { // The model echoed just the prefix; renaming would double it (`tmchow/tmchow`). expect(stripConfiguredBranchPrefix('tmchow', 'tmchow')).toBe('') // Confirm a real work-derived name still strips correctly. expect(stripConfiguredBranchPrefix('tmchow-fix-auth', 'tmchow')).toBe('fix-auth') }) }) describe('humanizeBranchSlug', () => { it('turns a kebab slug into a readable label', () => { expect(humanizeBranchSlug('supported-models-list')).toBe('Supported models list') expect(humanizeBranchSlug('fix-auth')).toBe('Fix auth') }) it('returns empty string for an empty slug', () => { expect(humanizeBranchSlug('')).toBe('') }) }) describe('buildBranchNamePrompt', () => { it('includes the user prompt and omits the assistant section when absent', () => { const prompt = buildBranchNamePrompt({ firstPrompt: 'Add a logout button' }) expect(prompt).toContain('Add a logout button') expect(prompt).not.toContain("Agent's initial response") }) it('keeps the default prompt general without style rules', () => { const prompt = buildBranchNamePrompt({ firstPrompt: 'Add a logout button' }) expect(prompt).toContain('Generate a short git branch name') expect(prompt).toContain('Output ONLY the branch name on a single line') expect(prompt).not.toContain('Rules:') expect(prompt).not.toMatch(/kebab-case/i) expect(prompt).not.toMatch(/between \d+ and \d+ words/i) expect(prompt).not.toMatch(/no prefixes/i) }) it('includes the assistant response when present', () => { const prompt = buildBranchNamePrompt({ firstPrompt: 'Add a logout button', assistantMessage: "I'll wire it into the header." }) expect(prompt).toContain("Agent's initial response") expect(prompt).toContain("I'll wire it into the header.") }) it('leads with a custom naming prompt so overrides can own style', () => { const prompt = buildBranchNamePrompt( { firstPrompt: 'Add a logout button' }, 'Prefer product nouns.' ) expect(prompt.startsWith('Prefer product nouns.')).toBe(true) expect(prompt).not.toContain('Additional user prompt:') expect(prompt).not.toContain('Rules:') expect(prompt).toContain('Generate a git branch name') expect(prompt).toContain('Output ONLY the branch name on a single line') expect(prompt).toContain('Add a logout button') }) })