flowgit/tests/git/git.test.ts

165 lines
6.2 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { mkdtemp, rm, writeFile, mkdir } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { execGit, execGitOrThrow } from '../../electron/git/executor'
import { detectGit } from '../../electron/git/detector'
import { getRepoStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile } from '../../electron/git/changes'
import { getLog, getGraphData, getCommitDetail } from '../../electron/git/history'
import { getFileDiff } from '../../electron/git/diff'
import { listBranches, createBranch, switchBranch, deleteBranch, listTags, createTag } from '../../electron/git/branches'
import { listStash, createStash, applyStash, popStash, dropStash } from '../../electron/git/stash'
describe.sequential('Git Engine Integration Tests (Isolated Temporary Repositories)', () => {
let testDir: string
let remoteDir: string
beforeAll(async () => {
// Verify Git is detected
const detection = await detectGit()
expect(detection.found).toBe(true)
expect(detection.version).toBeDefined()
// Create temp working dir
testDir = await mkdtemp(join(tmpdir(), 'flowgit-test-repo-'))
remoteDir = await mkdtemp(join(tmpdir(), 'flowgit-remote-repo-'))
// Initialize git repo with test user config
await execGitOrThrow(['init', '-b', 'main'], { cwd: testDir })
await execGitOrThrow(['config', 'user.name', 'FlowGit Tester'], { cwd: testDir })
await execGitOrThrow(['config', 'user.email', 'tester@flowgit.local'], { cwd: testDir })
})
afterAll(async () => {
// Cleanup temporary folders safely
try {
await rm(testDir, { recursive: true, force: true })
await rm(remoteDir, { recursive: true, force: true })
} catch {
// ignore Windows directory lock cleanup failures
}
})
it('1. Detects an empty valid repository status', async () => {
const status = await getRepoStatus(testDir)
expect(status.branch).toBe('main')
expect(status.staged.length).toBe(0)
expect(status.unstaged.length).toBe(0)
expect(status.untracked.length).toBe(0)
})
it('2. Detects untracked file, stages, and unstages it', async () => {
const testFile = 'test.txt'
await writeFile(join(testDir, testFile), 'Hello FlowGit\n', 'utf8')
let status = await getRepoStatus(testDir)
expect(status.untracked.some(f => f.path === testFile)).toBe(true)
// Stage
await stageFile(testDir, testFile)
status = await getRepoStatus(testDir)
expect(status.staged.some(f => f.path === testFile)).toBe(true)
expect(status.untracked.some(f => f.path === testFile)).toBe(false)
// Unstage
await unstageFile(testDir, testFile)
status = await getRepoStatus(testDir)
expect(status.staged.some(f => f.path === testFile)).toBe(false)
expect(status.untracked.some(f => f.path === testFile)).toBe(true)
})
it('3. Commits staged files and reads commit log', async () => {
const testFile = 'test.txt'
await stageFile(testDir, testFile)
await execGitOrThrow(['commit', '-m', 'feat: initial commit'], { cwd: testDir })
const log = await getLog(testDir)
expect(log.length).toBe(1)
expect(log[0].message).toBe('feat: initial commit')
expect(log[0].author).toBe('FlowGit Tester')
expect(log[0].shortHash.length).toBeGreaterThanOrEqual(7)
const detail = await getCommitDetail(testDir, log[0].hash)
expect(detail).not.toBeNull()
expect(detail?.message).toBe('feat: initial commit')
})
it('4. Diff generation for modified file', async () => {
const testFile = 'test.txt'
await writeFile(join(testDir, testFile), 'Hello FlowGit\nLine 2 added\n', 'utf8')
const diff = await getFileDiff(testDir, testFile)
expect(diff.additions).toBe(1)
expect(diff.deletions).toBe(0)
expect(diff.hunks.length).toBeGreaterThan(0)
expect(diff.hunks[0].lines.some(l => l.type === 'added' && l.content.includes('Line 2 added'))).toBe(true)
})
it('5. Discards unstaged modifications', async () => {
const testFile = 'test.txt'
await discardFile(testDir, testFile)
const status = await getRepoStatus(testDir)
expect(status.unstaged.length).toBe(0)
})
it('6. Branch creation, checkout, commit, and deletion', async () => {
await createBranch(testDir, 'feature/blueprint-graph')
let branches = await listBranches(testDir)
expect(branches.some(b => b.name === 'feature/blueprint-graph')).toBe(true)
// Switch back to main
await switchBranch(testDir, 'main')
branches = await listBranches(testDir)
const current = branches.find(b => b.isCurrent)
expect(current?.name).toBe('main')
// Delete branch
await deleteBranch(testDir, 'feature/blueprint-graph', true)
branches = await listBranches(testDir)
expect(branches.some(b => b.name === 'feature/blueprint-graph')).toBe(false)
})
it('7. Stash operations: create, list, apply, drop', async () => {
const testFile = 'test.txt'
await writeFile(join(testDir, testFile), 'Stash test modifications\n', 'utf8')
await createStash(testDir, 'wip: test stash')
let stashes = await listStash(testDir)
expect(stashes.length).toBe(1)
expect(stashes[0].message).toContain('wip: test stash')
// Apply stash
const applyRes = await applyStash(testDir, 0)
expect(applyRes.conflicts).toBe(false)
// Drop stash
await dropStash(testDir, 0)
stashes = await listStash(testDir)
expect(stashes.length).toBe(0)
// Clean up modifications
await discardFile(testDir, testFile)
})
it('8. Tag creation, listing, and deletion', async () => {
await createTag(testDir, 'v1.0.0', 'HEAD', 'Release version 1.0.0')
let tags = await listTags(testDir)
expect(tags.some(t => t.name === 'v1.0.0')).toBe(true)
})
it('9. Blueprint graph DAG computation', async () => {
// Add second commit
const docFile = 'README.md'
await writeFile(join(testDir, docFile), '# FlowGit\n', 'utf8')
await stageFile(testDir, docFile)
await execGitOrThrow(['commit', '-m', 'docs: add readme'], { cwd: testDir })
const graph = await getGraphData(testDir, 100)
expect(graph.nodes.length).toBe(2)
expect(graph.edges.length).toBe(1)
expect(graph.nodes[0].column).toBeDefined()
expect(graph.nodes[0].row).toBe(0)
expect(graph.nodes[1].row).toBe(1)
})
})