import { describe, expect, it } from 'vitest' import { countWorkingTree, parsePorcelainStatus } from '../git.js' describe('countWorkingTree', () => { it('counts purely-unstaged entries (the leading-space regression)', () => { // 4 modified-in-worktree files. The historical bug: execGit ran .trim(), // eating the FIRST line's leading space (" M" → "M"), so it was miscounted // as staged → 3 unstaged / 1 staged. With column parsing it must be 4 / 0. const porcelain = ' M src/commands/dev.ts\n M src/lib/dev-configure.ts\n M src/lib/x.ts\n M templates/skills/dev-start/SKILL.md\n' expect(countWorkingTree(porcelain)).toEqual({ staged: 0, unstaged: 4 }) }) it('counts a staged-only modification (X column)', () => { expect(countWorkingTree('M foo.ts')).toEqual({ staged: 1, unstaged: 0 }) }) it('counts a combined staged + unstaged entry (MM) on both axes', () => { expect(countWorkingTree('MM foo.ts')).toEqual({ staged: 1, unstaged: 1 }) }) it('counts a staged addition (A) and an unstaged deletion (D)', () => { expect(countWorkingTree('A added.ts\n D removed.ts')).toEqual({ staged: 1, unstaged: 1 }) }) it('ignores untracked entries (??)', () => { expect(countWorkingTree('?? new.ts\n M tracked.ts')).toEqual({ staged: 0, unstaged: 1 }) }) it('handles a trailing newline / empty input without over-counting', () => { expect(countWorkingTree('')).toEqual({ staged: 0, unstaged: 0 }) expect(countWorkingTree('\n')).toEqual({ staged: 0, unstaged: 0 }) }) it('counts a realistic mixed changeset by column', () => { const porcelain = [ 'M staged-only.ts', // staged ' M unstaged-only.ts', // unstaged 'MM both.ts', // staged + unstaged 'A staged-add.ts', // staged '?? untracked.ts', // neither '', ].join('\n') expect(countWorkingTree(porcelain)).toEqual({ staged: 3, unstaged: 2 }) }) }) describe('parsePorcelainStatus', () => { it('splits staged / modified / untracked by column', () => { const out = ['M staged.ts', ' M modified.ts', '?? new.ts'].join('\n') expect(parsePorcelainStatus(out)).toEqual({ staged: ['staged.ts'], modified: ['modified.ts'], untracked: ['new.ts'], }) }) it('a combined MM entry lands in BOTH staged and modified', () => { expect(parsePorcelainStatus('MM both.ts')).toEqual({ staged: ['both.ts'], modified: ['both.ts'], untracked: [], }) }) it('a STAGED RENAME yields the NEW path, not "old -> new" (the EF-guard false-negative fix)', () => { // Before the fix, slice(3) kept the whole "old -> new" string → existsSync // failed → the renamed migration was silently skipped by the destructive scan. const out = 'R src/Migrations/20260601_core_v3_54_0_001_InitialCreate.cs -> src/Migrations/20260602_core_v3_55_0_001_InitialCreate.cs' expect(parsePorcelainStatus(out)).toEqual({ staged: ['src/Migrations/20260602_core_v3_55_0_001_InitialCreate.cs'], modified: [], untracked: [], }) }) it('preserves the leading-space column on the first line (no .trim regression)', () => { expect(parsePorcelainStatus(' M src/a.ts')).toEqual({ staged: [], modified: ['src/a.ts'], untracked: [], }) }) it('ignores blank / short lines without crashing', () => { expect(parsePorcelainStatus('\n\nM x.ts\n')).toEqual({ staged: ['x.ts'], modified: [], untracked: [], }) }) })