mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-29 20:29:33 +01:00
84 lines
3.0 KiB
TypeScript
84 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();
|
||
|
|
});
|
||
|
|
});
|