mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Maintainer tooling in scripts/pr-bot/: a daemon (systemd user unit
codeman-pr-bot) that lists open PRs with gh, reviews each head commit once in
a Codeman claude session (`prbot-<n>`) running in a private `git clone
--shared`, and sends the verdict, ranked findings, checks and a recommendation
to Telegram with action buttons. Merge, close, post-comment and approve-CI
happen only from a Telegram command or button plus a confirmation tap; the
bot never writes to GitHub on its own. The Telegram token and chat id come
from the existing notifier bot's env file.
Verified live: three PRs reviewed end to end (383, 363, 368), reports
delivered with buttons, reviewer sessions on the pinned model. Findings
along the way, each fixed and documented: a linked worktree inherits the
main checkout's model pin (hence the shared clone), undici's 5-minute header
timeout cut off the first review, gh was missing from the service PATH, and
the periodic scan orphaned an in-flight review's record.
typecheck/lint/format now cover scripts/pr-bot; tests in
test/pr-bot-{report,state,commands}.test.ts; guide in docs/pr-bot.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
/**
|
|
* @fileoverview StateStore semantics for the PR bot: upsert keeps review results
|
|
* across scans, a closed PR that reopens comes back as reviewed, saves are atomic
|
|
* and 0600, and the message map is bounded.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { mkdtempSync, readdirSync, statSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import { StateStore } from '../scripts/pr-bot/state.js';
|
|
import type { PrSummary } from '../scripts/pr-bot/github.js';
|
|
|
|
function summary(over: Partial<PrSummary> = {}): PrSummary {
|
|
return {
|
|
number: 7,
|
|
title: 't',
|
|
author: 'a',
|
|
headSha: 'aaaa',
|
|
baseRef: 'master',
|
|
headRef: 'x',
|
|
isDraft: false,
|
|
mergeable: 'MERGEABLE',
|
|
mergeState: 'CLEAN',
|
|
additions: 1,
|
|
deletions: 1,
|
|
changedFiles: 1,
|
|
updatedAt: '',
|
|
url: 'https://example/7',
|
|
isCrossRepository: true,
|
|
labels: [],
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe('StateStore', () => {
|
|
it('starts empty, persists, and reloads', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'prbot-state-'));
|
|
const path = join(dir, 'state.json');
|
|
const store = new StateStore(path);
|
|
const rec = store.upsertPr(summary());
|
|
expect(rec.status).toBe('new');
|
|
rec.status = 'reviewed';
|
|
rec.reviewedSha = 'aaaa';
|
|
rec.verdict = 'merge';
|
|
store.state.telegramOffset = 42;
|
|
store.save();
|
|
expect(statSync(path).mode & 0o777).toBe(0o600);
|
|
expect(readdirSync(dir)).toEqual(['state.json']); // no tmp file left behind
|
|
const again = new StateStore(path);
|
|
expect(again.pr(7)?.verdict).toBe('merge');
|
|
expect(again.state.telegramOffset).toBe(42);
|
|
});
|
|
|
|
it('upsert refreshes metadata but keeps the review; reopening a closed PR restores reviewed', () => {
|
|
const store = new StateStore(join(mkdtempSync(join(tmpdir(), 'prbot-state-')), 'state.json'));
|
|
const rec = store.upsertPr(summary());
|
|
rec.status = 'reviewed';
|
|
rec.reviewedSha = 'aaaa';
|
|
const moved = store.upsertPr(summary({ headSha: 'bbbb', title: 'renamed' }));
|
|
expect(moved).toBe(rec); // same object: a review in flight keeps writing into the stored record
|
|
expect(moved.status).toBe('reviewed');
|
|
expect(moved.reviewedSha).toBe('aaaa');
|
|
expect(moved.headSha).toBe('bbbb');
|
|
expect(moved.title).toBe('renamed');
|
|
moved.status = 'closed';
|
|
moved.closedAs = 'closed';
|
|
expect(store.openPrs()).toHaveLength(0);
|
|
const reopened = store.upsertPr(summary({ headSha: 'bbbb' }));
|
|
expect(reopened.status).toBe('reviewed');
|
|
expect(reopened.closedAs).toBeUndefined();
|
|
expect(store.openPrs()).toHaveLength(1);
|
|
});
|
|
|
|
it('bounds the message map on save', () => {
|
|
const store = new StateStore(join(mkdtempSync(join(tmpdir(), 'prbot-state-')), 'state.json'));
|
|
for (let i = 0; i < 2500; i++) store.rememberMessage(i, 1);
|
|
store.save();
|
|
const keys = Object.keys(store.state.messages).map(Number);
|
|
expect(keys).toHaveLength(2000);
|
|
expect(Math.min(...keys)).toBe(500);
|
|
expect(store.prForMessage(2499)).toBe(1);
|
|
expect(store.prForMessage(10)).toBeUndefined();
|
|
});
|
|
});
|