This was an update of which I cannot remember what it did but it was surely something that I needed so let's commit it. YOLO
CI / check (push) Waiting to run

This commit is contained in:
2026-09-22 16:38:00 +02:00
parent dbb7e40c6c
commit 5112d3de94
13 changed files with 172 additions and 55 deletions
+23 -3
View File
@@ -10,6 +10,7 @@ vi.mock('../src/renderer/src/terminals', () => {
import { App } from '../src/renderer/src/App'
import { ProjectProvider } from '../src/renderer/src/project'
import { normalizeCommitMsg } from '../src/renderer/src/components'
beforeAll(() => {
globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as never
@@ -78,7 +79,7 @@ describe('Pass on to Agent', () => {
})
describe('Stage + commit', () => {
it('stages a file and commits via Shift+Enter (no commit button)', async () => {
it('stages a file and commits via Shift+Enter', async () => {
const c = renderApp()
const row = await waitFor(() => {
const r = find(c, '.git-row', 'UserController.php')
@@ -86,13 +87,26 @@ describe('Stage + commit', () => {
return r
})
fireEvent.click(row.querySelector<HTMLButtonElement>('button[title="Stage changes"]')!)
// the commit button is intentionally gone — committing is keyboard-only
expect(c.querySelector('.commit-btn')).toBeNull()
const msg = c.querySelector<HTMLTextAreaElement>('.commit-input')!
fireEvent.change(msg, { target: { value: 'wire up balance' } })
fireEvent.keyDown(msg, { key: 'Enter', shiftKey: true })
await waitFor(() => expect(find(c, '.toast', 'Committed')).toBeTruthy())
})
it('commits from the button once a file is staged', async () => {
const c = renderApp()
const row = await waitFor(() => {
const r = find(c, '.git-row', 'UserController.php')
if (!r) throw new Error('git not ready')
return r
})
fireEvent.click(row.querySelector<HTMLButtonElement>('button[title="Stage changes"]')!)
await waitFor(() => expect(c.querySelector<HTMLButtonElement>('.commit-btn')!.disabled).toBe(false))
const msg = c.querySelector<HTMLTextAreaElement>('.commit-input')!
fireEvent.change(msg, { target: { value: 'wire up balance' } })
fireEvent.click(c.querySelector<HTMLButtonElement>('.commit-btn')!)
await waitFor(() => expect(find(c, '.toast', 'Committed')).toBeTruthy())
})
})
describe('Explorer background menu', () => {
@@ -126,3 +140,9 @@ describe('Explorer background menu', () => {
expect(find(c, '.ctx-item', 'Copy file name')).toBeTruthy()
})
})
describe('Commit message normalization', () => {
it('lowercases the text and folds the line breaks into one line', () => {
expect(normalizeCommitMsg('Fix Login\nAnd Logout\r\n Flow')).toBe('fix login and logout flow')
})
})
+19 -1
View File
@@ -5,7 +5,25 @@ import { afterEach, describe, expect, it } from 'vitest'
import { simpleGit } from 'simple-git'
import { readFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { classify, discard, load, stage } from '../src/main/git-service'
import { classify, discard, load, parseAhead, stage } from '../src/main/git-service'
describe('parseAhead', () => {
it('reads the ahead count from the porcelain header', () => {
expect(parseAhead('main...origin/main [ahead 3]')).toEqual({ ahead: 3, canPush: true })
expect(parseAhead('main...origin/main [ahead 1, behind 2]')).toEqual({ ahead: 1, canPush: true })
})
it('reports nothing to push on a branch in sync', () => {
expect(parseAhead('main...origin/main')).toEqual({ ahead: 0, canPush: false })
expect(parseAhead('main...origin/main [behind 2]')).toEqual({ ahead: 0, canPush: false })
})
it('treats a branch without an upstream as pushable, count unknown', () => {
expect(parseAhead('feature/x')).toEqual({ ahead: 0, canPush: true })
})
it('reports nothing to push on an empty repo or a detached HEAD', () => {
expect(parseAhead('No commits yet on main')).toEqual({ ahead: 0, canPush: false })
expect(parseAhead('HEAD (no branch)')).toEqual({ ahead: 0, canPush: false })
})
})
describe('classify', () => {
it('reads the index code as staged, working code as unstaged', () => {
+2 -2
View File
@@ -41,7 +41,7 @@ function stubBridge(): void {
write: async (t: string) => { stored = t; writes.push(t) },
},
git: {
load: async () => ({ branch: 'main', changes: [] }),
load: async () => ({ branch: 'main', ahead: 0, canPush: false, changes: [] }),
stage: async () => {}, unstage: async () => {}, commit: async () => {},
push: async () => ({ ok: true, message: '' }), discard: async () => {},
},
@@ -68,7 +68,7 @@ afterEach(() => {
async function boot(): Promise<HTMLElement> {
const c = render(<ProjectProvider><App /></ProjectProvider>).container
await waitFor(() => { if (!c.querySelector('.git-foot')) throw new Error('not ready') })
await waitFor(() => { if (!c.querySelector('.commit-box')) throw new Error('not ready') })
return c
}
+1 -1
View File
@@ -40,7 +40,7 @@ export function installBridge(rows: StubRow[], files: Record<string, string>): v
shell: { reveal: noop },
notes: { read: async () => '', write: async () => {} },
git: {
load: async () => ({ branch: 'main', changes: rows }),
load: async () => ({ branch: 'main', ahead: 0, canPush: false, changes: rows }),
stage: async () => {}, unstage: async () => {}, commit: async () => {},
push: async () => ({ ok: true, message: '' }), discard: async () => {},
},
+2 -2
View File
@@ -38,7 +38,7 @@ function stubBridge(): void {
shell: { reveal: noop },
notes: { read: async () => '', write: async () => {} },
git: {
load: async () => ({ branch: 'main', changes: [] }),
load: async () => ({ branch: 'main', ahead: 0, canPush: false, changes: [] }),
stage: async () => {}, unstage: async () => {}, commit: async () => {},
push: async () => ({ ok: true, message: '' }), discard: async () => {},
},
@@ -69,7 +69,7 @@ afterEach(() => {
async function boot(): Promise<HTMLElement> {
const c = render(<ProjectProvider><App /></ProjectProvider>).container
await waitFor(() => { if (!c.querySelector('.git-foot')) throw new Error('not ready') })
await waitFor(() => { if (!c.querySelector('.commit-box')) throw new Error('not ready') })
return c
}