import { test } from 'node:test'; import assert from 'node:assert/strict'; import { JobRunner } from '../src/job.js'; import { createLogger } from '../src/logger.js'; process.env.LOG_LEVEL = 'error'; // keep test output clean const logger = createLogger({}); function baseTrigger(overrides = {}) { return { repo: 'o/r', issueNumber: 3, issueTitle: 'Do a thing', issueBody: 'body', isPullRequest: false, defaultBranch: 'main', requestText: '@claude do the thing', author: 'selimaj-dev', ...overrides, }; } function fakeGitea({ existingPRs = [] } = {}) { const calls = { comments: [], createdPRs: [] }; return { baseUrl: 'https://git.example.com', token: 'tok', getPullRequest: async () => null, listOpenPullRequests: async () => existingPRs, createPullRequest: async (repo, body) => { calls.createdPRs.push({ repo, body }); return { html_url: 'https://git.example.com/o/r/pulls/99' }; }, createIssueComment: async (repo, n, body) => { calls.comments.push({ repo, n, body }); }, calls, }; } function fakeGitFactory({ hasChanges = false } = {}) { const calls = { cloned: false, checkedOutBranch: null, committed: null, pushed: null }; const factory = () => ({ clone: async () => { calls.cloned = true; }, checkoutWorkBranch: async (b) => { calls.checkedOutBranch = b; }, hasChanges: async () => hasChanges, commitAll: async (msg) => { calls.committed = msg; }, push: async (b) => { calls.pushed = b; }, }); factory.calls = calls; return factory; } test('posts a comment with claude output when nothing changed', async () => { const gitea = fakeGitea(); const gitFactory = fakeGitFactory({ hasChanges: false }); const claude = { runOnce: async () => 'Just an answer, no code change needed.' }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await runner.run(baseTrigger()); assert.equal(gitea.calls.comments.length, 1); assert.match(gitea.calls.comments[0].body, /Just an answer/); assert.equal(gitFactory.calls.committed, null); assert.equal(gitea.calls.createdPRs.length, 0); }); test('commits, pushes and opens a PR when changes are made on an issue', async () => { const gitea = fakeGitea(); const gitFactory = fakeGitFactory({ hasChanges: true }); const claude = { runOnce: async () => 'Fixed it.' }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await runner.run(baseTrigger()); assert.equal(gitFactory.calls.checkedOutBranch, 'claude/issue-3'); assert.match(gitFactory.calls.committed, /#3/); assert.equal(gitFactory.calls.pushed, 'claude/issue-3'); assert.equal(gitea.calls.createdPRs.length, 1); assert.match(gitea.calls.comments[0].body, /pulls\/99/); }); test('reuses an existing open PR instead of creating a duplicate', async () => { const gitea = fakeGitea({ existingPRs: [{ head: { ref: 'claude/issue-3' }, html_url: 'https://git.example.com/o/r/pulls/7' }], }); const gitFactory = fakeGitFactory({ hasChanges: true }); const claude = { runOnce: async () => 'Fixed it.' }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await runner.run(baseTrigger()); assert.equal(gitea.calls.createdPRs.length, 0); assert.match(gitea.calls.comments[0].body, /pulls\/7/); }); test('does not open a PR when the trigger is already a PR conversation', async () => { const gitea = fakeGitea(); gitea.getPullRequest = async () => ({ head: { ref: 'existing-branch' } }); const gitFactory = fakeGitFactory({ hasChanges: true }); const claude = { runOnce: async () => 'Updated per review comment.' }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await runner.run(baseTrigger({ isPullRequest: true })); assert.equal(gitFactory.calls.pushed, 'existing-branch'); assert.equal(gitFactory.calls.checkedOutBranch, null); // issue-only path skipped assert.equal(gitea.calls.createdPRs.length, 0); }); test('posts a failure comment when claude throws, and never throws itself', async () => { const gitea = fakeGitea(); const gitFactory = fakeGitFactory(); const claude = { runOnce: async () => { throw new Error('claude exploded'); } }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await assert.doesNotReject(runner.run(baseTrigger())); assert.equal(gitea.calls.comments.length, 1); assert.match(gitea.calls.comments[0].body, /claude exploded/); }); test('swallows an error thrown while posting the failure comment itself', async () => { const gitea = fakeGitea(); gitea.createIssueComment = async () => { throw new Error('gitea is down'); }; const gitFactory = fakeGitFactory(); const claude = { runOnce: async () => { throw new Error('claude exploded'); } }; const runner = new JobRunner({ gitea, claude, config: {}, logger, gitFactory }); await assert.doesNotReject(runner.run(baseTrigger())); });