|
2 | 2 | * Tests for git.ts module |
3 | 3 | */ |
4 | 4 |
|
5 | | -import { afterEach, beforeEach, describe, expect, test } from "bun:test" |
| 5 | +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" |
6 | 6 | import { execSync } from "node:child_process" |
7 | 7 | import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" |
8 | 8 | import { join } from "node:path" |
9 | | -import { generateCommitMessage, hasChanges } from "../src/git.ts" |
| 9 | +import { commitChanges, generateCommitMessage, hasChanges, pushChanges } from "../src/git.ts" |
| 10 | +import type { Logger } from "../src/logger.ts" |
10 | 11 |
|
11 | 12 | const TEST_DIR = "/tmp/opencoder-test-git" |
12 | 13 |
|
@@ -161,4 +162,168 @@ describe("git", () => { |
161 | 162 | expect(generateCommitMessage("REFACTOR CODE")).toBe("refactor: REFACTOR CODE") |
162 | 163 | }) |
163 | 164 | }) |
| 165 | + |
| 166 | + describe("commitChanges", () => { |
| 167 | + let mockLogger: Logger |
| 168 | + let testGitDir: string |
| 169 | + |
| 170 | + beforeEach(() => { |
| 171 | + // Create mock logger |
| 172 | + mockLogger = { |
| 173 | + log: mock(() => {}), |
| 174 | + logError: mock(() => {}), |
| 175 | + logVerbose: mock(() => {}), |
| 176 | + say: mock(() => {}), |
| 177 | + info: mock(() => {}), |
| 178 | + success: mock(() => {}), |
| 179 | + warn: mock(() => {}), |
| 180 | + alert: mock(() => {}), |
| 181 | + flush: mock(() => {}), |
| 182 | + setCycleLog: mock(() => {}), |
| 183 | + cleanup: mock(() => 0), |
| 184 | + } as unknown as Logger |
| 185 | + |
| 186 | + // Create a test git repository |
| 187 | + testGitDir = "/tmp/opencoder-test-git-commit" |
| 188 | + if (existsSync(testGitDir)) { |
| 189 | + rmSync(testGitDir, { recursive: true }) |
| 190 | + } |
| 191 | + mkdirSync(testGitDir, { recursive: true }) |
| 192 | + execSync("git init", { cwd: testGitDir }) |
| 193 | + execSync('git config user.email "test@test.com"', { cwd: testGitDir }) |
| 194 | + execSync('git config user.name "Test User"', { cwd: testGitDir }) |
| 195 | + }) |
| 196 | + |
| 197 | + afterEach(() => { |
| 198 | + if (existsSync(testGitDir)) { |
| 199 | + rmSync(testGitDir, { recursive: true }) |
| 200 | + } |
| 201 | + }) |
| 202 | + |
| 203 | + test("commits changes without signoff", () => { |
| 204 | + // Create a file to commit |
| 205 | + writeFileSync(join(testGitDir, "test.txt"), "test content") |
| 206 | + |
| 207 | + commitChanges(testGitDir, mockLogger, "feat: add test file", false) |
| 208 | + |
| 209 | + // Verify commit was created |
| 210 | + const log = execSync("git log --oneline", { cwd: testGitDir, encoding: "utf-8" }) |
| 211 | + expect(log).toContain("feat: add test file") |
| 212 | + |
| 213 | + // Verify logger was called |
| 214 | + expect(mockLogger.log).toHaveBeenCalledWith("Committed: feat: add test file") |
| 215 | + |
| 216 | + // Verify no signoff in commit message |
| 217 | + const fullLog = execSync("git log --format=%B -n 1", { |
| 218 | + cwd: testGitDir, |
| 219 | + encoding: "utf-8", |
| 220 | + }) |
| 221 | + expect(fullLog).not.toContain("Signed-off-by") |
| 222 | + }) |
| 223 | + |
| 224 | + test("commits changes with signoff", () => { |
| 225 | + // Create a file to commit |
| 226 | + writeFileSync(join(testGitDir, "test2.txt"), "test content 2") |
| 227 | + |
| 228 | + commitChanges(testGitDir, mockLogger, "fix: bug fix", true) |
| 229 | + |
| 230 | + // Verify commit was created |
| 231 | + const log = execSync("git log --oneline", { cwd: testGitDir, encoding: "utf-8" }) |
| 232 | + expect(log).toContain("fix: bug fix") |
| 233 | + |
| 234 | + // Verify logger was called |
| 235 | + expect(mockLogger.log).toHaveBeenCalledWith("Committed: fix: bug fix") |
| 236 | + |
| 237 | + // Verify signoff in commit message |
| 238 | + const fullLog = execSync("git log --format=%B -n 1", { |
| 239 | + cwd: testGitDir, |
| 240 | + encoding: "utf-8", |
| 241 | + }) |
| 242 | + expect(fullLog).toContain("Signed-off-by: Test User <test@test.com>") |
| 243 | + }) |
| 244 | + |
| 245 | + test("logs error when commit fails (no changes)", () => { |
| 246 | + // Try to commit with no changes (should fail) |
| 247 | + commitChanges(testGitDir, mockLogger, "feat: nothing to commit", false) |
| 248 | + |
| 249 | + // Verify error was logged |
| 250 | + expect(mockLogger.logError).toHaveBeenCalledWith(expect.stringContaining("Failed to commit")) |
| 251 | + }) |
| 252 | + |
| 253 | + test("logs error for non-git directory", () => { |
| 254 | + const nonGitDir = "/tmp/opencoder-test-non-git-commit" |
| 255 | + if (existsSync(nonGitDir)) { |
| 256 | + rmSync(nonGitDir, { recursive: true }) |
| 257 | + } |
| 258 | + mkdirSync(nonGitDir, { recursive: true }) |
| 259 | + |
| 260 | + commitChanges(nonGitDir, mockLogger, "feat: test", false) |
| 261 | + |
| 262 | + // Verify error was logged |
| 263 | + expect(mockLogger.logError).toHaveBeenCalledWith(expect.stringContaining("Failed to commit")) |
| 264 | + |
| 265 | + rmSync(nonGitDir, { recursive: true }) |
| 266 | + }) |
| 267 | + }) |
| 268 | + |
| 269 | + describe("pushChanges", () => { |
| 270 | + let mockLogger: Logger |
| 271 | + let testGitDir: string |
| 272 | + |
| 273 | + beforeEach(() => { |
| 274 | + // Create mock logger |
| 275 | + mockLogger = { |
| 276 | + log: mock(() => {}), |
| 277 | + logError: mock(() => {}), |
| 278 | + logVerbose: mock(() => {}), |
| 279 | + say: mock(() => {}), |
| 280 | + info: mock(() => {}), |
| 281 | + success: mock(() => {}), |
| 282 | + warn: mock(() => {}), |
| 283 | + alert: mock(() => {}), |
| 284 | + flush: mock(() => {}), |
| 285 | + setCycleLog: mock(() => {}), |
| 286 | + cleanup: mock(() => 0), |
| 287 | + } as unknown as Logger |
| 288 | + |
| 289 | + // Create a test git repository |
| 290 | + testGitDir = "/tmp/opencoder-test-git-push" |
| 291 | + if (existsSync(testGitDir)) { |
| 292 | + rmSync(testGitDir, { recursive: true }) |
| 293 | + } |
| 294 | + mkdirSync(testGitDir, { recursive: true }) |
| 295 | + execSync("git init", { cwd: testGitDir }) |
| 296 | + execSync('git config user.email "test@test.com"', { cwd: testGitDir }) |
| 297 | + execSync('git config user.name "Test User"', { cwd: testGitDir }) |
| 298 | + }) |
| 299 | + |
| 300 | + afterEach(() => { |
| 301 | + if (existsSync(testGitDir)) { |
| 302 | + rmSync(testGitDir, { recursive: true }) |
| 303 | + } |
| 304 | + }) |
| 305 | + |
| 306 | + test("logs error when push fails (no remote)", () => { |
| 307 | + // Try to push without remote configured (should fail) |
| 308 | + pushChanges(testGitDir, mockLogger) |
| 309 | + |
| 310 | + // Verify error was logged |
| 311 | + expect(mockLogger.logError).toHaveBeenCalledWith(expect.stringContaining("Failed to push")) |
| 312 | + }) |
| 313 | + |
| 314 | + test("logs error for non-git directory", () => { |
| 315 | + const nonGitDir = "/tmp/opencoder-test-non-git-push" |
| 316 | + if (existsSync(nonGitDir)) { |
| 317 | + rmSync(nonGitDir, { recursive: true }) |
| 318 | + } |
| 319 | + mkdirSync(nonGitDir, { recursive: true }) |
| 320 | + |
| 321 | + pushChanges(nonGitDir, mockLogger) |
| 322 | + |
| 323 | + // Verify error was logged |
| 324 | + expect(mockLogger.logError).toHaveBeenCalledWith(expect.stringContaining("Failed to push")) |
| 325 | + |
| 326 | + rmSync(nonGitDir, { recursive: true }) |
| 327 | + }) |
| 328 | + }) |
164 | 329 | }) |
0 commit comments