Spaces:
Sleeping
Sleeping
| import { describe, it, expect, beforeEach, afterEach } from 'vitest' | |
| import fs from 'node:fs' | |
| import path from 'node:path' | |
| import os from 'node:os' | |
| import { execFileSync } from 'node:child_process' | |
| import { | |
| mcStatusToGnap, | |
| gnapStatusToMc, | |
| mcPriorityToGnap, | |
| initGnapRepo, | |
| pushTaskToGnap, | |
| removeTaskFromGnap, | |
| pullTasksFromGnap, | |
| getGnapStatus, | |
| type McTask, | |
| } from '../gnap-sync' | |
| let tmpDir: string | |
| beforeEach(() => { | |
| tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gnap-test-')) | |
| }) | |
| afterEach(() => { | |
| fs.rmSync(tmpDir, { recursive: true, force: true }) | |
| }) | |
| describe('status mapping', () => { | |
| it('maps MC statuses to GNAP states', () => { | |
| expect(mcStatusToGnap('pending')).toBe('backlog') | |
| expect(mcStatusToGnap('inbox')).toBe('backlog') | |
| expect(mcStatusToGnap('in_progress')).toBe('in_progress') | |
| expect(mcStatusToGnap('done')).toBe('done') | |
| expect(mcStatusToGnap('review')).toBe('review') | |
| expect(mcStatusToGnap('blocked')).toBe('blocked') | |
| expect(mcStatusToGnap('cancelled')).toBe('cancelled') | |
| }) | |
| it('maps GNAP states back to MC statuses', () => { | |
| expect(gnapStatusToMc('backlog')).toBe('inbox') | |
| expect(gnapStatusToMc('in_progress')).toBe('in_progress') | |
| expect(gnapStatusToMc('done')).toBe('done') | |
| expect(gnapStatusToMc('review')).toBe('review') | |
| }) | |
| it('falls back for unknown values', () => { | |
| expect(mcStatusToGnap('unknown_status')).toBe('backlog') | |
| expect(gnapStatusToMc('unknown_state')).toBe('inbox') | |
| }) | |
| }) | |
| describe('priority mapping', () => { | |
| it('maps MC priorities to GNAP priorities', () => { | |
| expect(mcPriorityToGnap('low')).toBe('low') | |
| expect(mcPriorityToGnap('medium')).toBe('medium') | |
| expect(mcPriorityToGnap('high')).toBe('high') | |
| expect(mcPriorityToGnap('critical')).toBe('critical') | |
| expect(mcPriorityToGnap('urgent')).toBe('critical') | |
| }) | |
| it('falls back to medium for unknown priorities', () => { | |
| expect(mcPriorityToGnap('unknown')).toBe('medium') | |
| }) | |
| }) | |
| describe('initGnapRepo', () => { | |
| it('creates directory structure and initializes git', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| expect(fs.existsSync(path.join(repoPath, 'version'))).toBe(true) | |
| expect(fs.existsSync(path.join(repoPath, 'agents.json'))).toBe(true) | |
| expect(fs.existsSync(path.join(repoPath, 'tasks'))).toBe(true) | |
| expect(fs.existsSync(path.join(repoPath, '.git'))).toBe(true) | |
| expect(fs.readFileSync(path.join(repoPath, 'version'), 'utf-8').trim()).toBe('1') | |
| }) | |
| it('is idempotent — re-running does not error', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| initGnapRepo(repoPath) | |
| expect(fs.existsSync(path.join(repoPath, '.git'))).toBe(true) | |
| }) | |
| }) | |
| describe('pushTaskToGnap', () => { | |
| it('writes task JSON and commits', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| const task: McTask = { | |
| id: 42, | |
| title: 'Test task', | |
| description: 'A test', | |
| status: 'in_progress', | |
| priority: 'high', | |
| assigned_to: 'agent-claude', | |
| tags: ['auth', 'sprint-1'], | |
| created_at: 1710500000, | |
| updated_at: 1710510000, | |
| project_id: 1, | |
| } | |
| pushTaskToGnap(task, repoPath) | |
| const filePath = path.join(repoPath, 'tasks', 'mc-42.json') | |
| expect(fs.existsSync(filePath)).toBe(true) | |
| const content = JSON.parse(fs.readFileSync(filePath, 'utf-8')) | |
| expect(content.id).toBe('mc-42') | |
| expect(content.title).toBe('Test task') | |
| expect(content.state).toBe('in_progress') | |
| expect(content.priority).toBe('high') | |
| expect(content.assignee).toBe('agent-claude') | |
| expect(content.tags).toEqual(['auth', 'sprint-1']) | |
| expect(content.mc_id).toBe(42) | |
| expect(content.mc_project_id).toBe(1) | |
| // Verify it was committed | |
| const log = execFileSync('git', ['log', '--oneline'], { | |
| cwd: repoPath, | |
| encoding: 'utf-8', | |
| }) | |
| expect(log).toContain('Update task mc-42') | |
| }) | |
| it('handles string tags (JSON serialized)', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| const task: McTask = { | |
| id: 1, | |
| title: 'String tags task', | |
| status: 'pending', | |
| priority: 'low', | |
| tags: '["bug","fix"]', | |
| } | |
| pushTaskToGnap(task, repoPath) | |
| const content = JSON.parse( | |
| fs.readFileSync(path.join(repoPath, 'tasks', 'mc-1.json'), 'utf-8') | |
| ) | |
| expect(content.tags).toEqual(['bug', 'fix']) | |
| }) | |
| }) | |
| describe('removeTaskFromGnap', () => { | |
| it('removes the task file and commits', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| const task: McTask = { | |
| id: 7, | |
| title: 'To be removed', | |
| status: 'done', | |
| priority: 'low', | |
| } | |
| pushTaskToGnap(task, repoPath) | |
| expect(fs.existsSync(path.join(repoPath, 'tasks', 'mc-7.json'))).toBe(true) | |
| removeTaskFromGnap(7, repoPath) | |
| expect(fs.existsSync(path.join(repoPath, 'tasks', 'mc-7.json'))).toBe(false) | |
| const log = execFileSync('git', ['log', '--oneline'], { | |
| cwd: repoPath, | |
| encoding: 'utf-8', | |
| }) | |
| expect(log).toContain('Remove task mc-7') | |
| }) | |
| it('does nothing when task does not exist', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| // Should not throw | |
| removeTaskFromGnap(999, repoPath) | |
| }) | |
| }) | |
| describe('pullTasksFromGnap', () => { | |
| it('reads all task files from the repo', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| pushTaskToGnap({ id: 1, title: 'Task A', status: 'pending', priority: 'low' }, repoPath) | |
| pushTaskToGnap({ id: 2, title: 'Task B', status: 'done', priority: 'high' }, repoPath) | |
| const tasks = pullTasksFromGnap(repoPath) | |
| expect(tasks).toHaveLength(2) | |
| const ids = tasks.map(t => t.id).sort() | |
| expect(ids).toEqual(['mc-1', 'mc-2']) | |
| }) | |
| it('returns empty array for non-existent directory', () => { | |
| const tasks = pullTasksFromGnap(path.join(tmpDir, 'nonexistent')) | |
| expect(tasks).toEqual([]) | |
| }) | |
| }) | |
| describe('getGnapStatus', () => { | |
| it('reports uninitialized for empty directory', () => { | |
| const status = getGnapStatus(path.join(tmpDir, 'empty')) | |
| expect(status.initialized).toBe(false) | |
| expect(status.taskCount).toBe(0) | |
| expect(status.hasRemote).toBe(false) | |
| }) | |
| it('reports correct status after init and push', () => { | |
| const repoPath = path.join(tmpDir, 'gnap-repo') | |
| initGnapRepo(repoPath) | |
| pushTaskToGnap({ id: 1, title: 'Task', status: 'pending', priority: 'medium' }, repoPath) | |
| const status = getGnapStatus(repoPath) | |
| expect(status.initialized).toBe(true) | |
| expect(status.taskCount).toBe(1) | |
| expect(status.hasRemote).toBe(false) | |
| expect(status.remoteUrl).toBe('') | |
| }) | |
| }) | |