This commit is contained in:
2026-09-28 16:15:50 +02:00
parent a0ae396b33
commit 34b7dc977a
25 changed files with 1595 additions and 8 deletions
+131
View File
@@ -0,0 +1,131 @@
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()));
});