import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; import { afterEach, describe, expect, it } from 'vitest'; function run(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) { return spawnSync(command, args, { cwd, encoding: 'utf8', env: { ...process.env, ...env, }, }); } function mustRun(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) { const result = run(command, args, cwd, env); if (result.status !== 0) { throw new Error( `${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`, ); } return result; } describe('commit-task script', () => { const tempDirs: string[] = []; afterEach(() => { while (tempDirs.length > 0) { rmSync(tempDirs.pop()!, { recursive: true, force: true }); } }); it('commits only the specified files', () => { const repoRoot = mkdtempSync(resolve(tmpdir(), 'genarrative-commit-task-')); tempDirs.push(repoRoot); mustRun('git', ['init'], repoRoot); mustRun('git', ['config', 'user.name', 'Codex Test'], repoRoot); mustRun('git', ['config', 'user.email', 'codex@example.com'], repoRoot); mkdirSync(resolve(repoRoot, 'docs'), { recursive: true }); mkdirSync(resolve(repoRoot, 'notes'), { recursive: true }); writeFileSync(resolve(repoRoot, 'docs', 'task.md'), 'v1\n', 'utf8'); writeFileSync(resolve(repoRoot, 'notes', 'other.md'), 'other v1\n', 'utf8'); mustRun('git', ['add', '.'], repoRoot); mustRun('git', ['commit', '-m', 'init'], repoRoot); writeFileSync(resolve(repoRoot, 'docs', 'task.md'), 'v2\n', 'utf8'); writeFileSync(resolve(repoRoot, 'notes', 'other.md'), 'other v2\n', 'utf8'); const scriptPath = resolve('/home/Genarrative/scripts/commit-task.mjs'); const commitResult = mustRun( 'node', [scriptPath, '-m', '提交任务文件', 'docs/task.md'], repoRoot, { GENARRATIVE_COMMIT_REPO_ROOT: repoRoot, }, ); expect(commitResult.stdout).toContain('提交任务文件'); const committedTaskContent = mustRun( 'git', ['show', 'HEAD:docs/task.md'], repoRoot, ).stdout; expect(committedTaskContent).toBe('v2\n'); const committedOtherContent = mustRun( 'git', ['show', 'HEAD:notes/other.md'], repoRoot, ).stdout; expect(committedOtherContent).toBe('other v1\n'); const statusResult = mustRun('git', ['status', '--short'], repoRoot); expect(statusResult.stdout).toContain(' M notes/other.md'); expect(statusResult.stdout).not.toContain('docs/task.md'); }); it('does not include unrelated staged files in the commit', () => { const repoRoot = mkdtempSync(resolve(tmpdir(), 'genarrative-commit-task-')); tempDirs.push(repoRoot); mustRun('git', ['init'], repoRoot); mustRun('git', ['config', 'user.name', 'Codex Test'], repoRoot); mustRun('git', ['config', 'user.email', 'codex@example.com'], repoRoot); mkdirSync(resolve(repoRoot, 'docs'), { recursive: true }); mkdirSync(resolve(repoRoot, 'notes'), { recursive: true }); writeFileSync(resolve(repoRoot, 'docs', 'task.md'), 'v1\n', 'utf8'); writeFileSync(resolve(repoRoot, 'notes', 'other.md'), 'other v1\n', 'utf8'); mustRun('git', ['add', '.'], repoRoot); mustRun('git', ['commit', '-m', 'init'], repoRoot); writeFileSync(resolve(repoRoot, 'docs', 'task.md'), 'v2\n', 'utf8'); writeFileSync(resolve(repoRoot, 'notes', 'other.md'), 'other v2\n', 'utf8'); mustRun('git', ['add', 'notes/other.md'], repoRoot); const scriptPath = resolve('/home/Genarrative/scripts/commit-task.mjs'); mustRun( 'node', [scriptPath, '-m', '只提交任务文件', 'docs/task.md'], repoRoot, { GENARRATIVE_COMMIT_REPO_ROOT: repoRoot, }, ); const committedTaskContent = mustRun( 'git', ['show', 'HEAD:docs/task.md'], repoRoot, ).stdout; expect(committedTaskContent).toBe('v2\n'); const committedOtherContent = mustRun( 'git', ['show', 'HEAD:notes/other.md'], repoRoot, ).stdout; expect(committedOtherContent).toBe('other v1\n'); const statusResult = mustRun('git', ['status', '--short'], repoRoot); expect(statusResult.stdout).toContain('M notes/other.md'); expect(statusResult.stdout).not.toContain('docs/task.md'); }); });