132 lines
4.9 KiB
JavaScript
132 lines
4.9 KiB
JavaScript
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()));
|
|
});
|