mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
/**
|
|
* SkillOpt rejected-buffer unit tests. Uses tempdir for fs ops.
|
|
*/
|
|
|
|
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import {
|
|
REJECTED_BUFFER_CAP,
|
|
isRejected,
|
|
loadRejectedBuffer,
|
|
makeRejectedEntry,
|
|
rejectedFilePath,
|
|
rejectedKey,
|
|
saveRejectedBuffer,
|
|
} from '../../src/core/skillopt/rejected-buffer.ts';
|
|
import type { EditOp } from '../../src/core/skillopt/types.ts';
|
|
|
|
let tmpDir: string;
|
|
const SKILL = 'test-skill';
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skillopt-rejected-'));
|
|
fs.mkdirSync(path.join(tmpDir, SKILL), { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
});
|
|
|
|
describe('rejectedKey', () => {
|
|
test('same skill+edits produce same key (state-aware dedup)', () => {
|
|
const text = 'skill text v1';
|
|
const edits: EditOp[] = [{ op: 'add', anchor: '## X', content: 'hello' }];
|
|
expect(rejectedKey(text, edits)).toBe(rejectedKey(text, edits));
|
|
});
|
|
|
|
test('different skill text → different key', () => {
|
|
const edits: EditOp[] = [{ op: 'add', anchor: '## X', content: 'hello' }];
|
|
expect(rejectedKey('v1', edits)).not.toBe(rejectedKey('v2', edits));
|
|
});
|
|
|
|
test('different edits → different key', () => {
|
|
const text = 'v1';
|
|
expect(rejectedKey(text, [{ op: 'add', anchor: 'A', content: '1' }]))
|
|
.not.toBe(rejectedKey(text, [{ op: 'add', anchor: 'B', content: '1' }]));
|
|
});
|
|
});
|
|
|
|
describe('loadRejectedBuffer + saveRejectedBuffer', () => {
|
|
test('empty when file missing', () => {
|
|
expect(loadRejectedBuffer(tmpDir, SKILL)).toEqual([]);
|
|
});
|
|
|
|
test('round-trips a single entry', () => {
|
|
const entry = makeRejectedEntry('v1', [{ op: 'add', anchor: 'A', content: 'x' }], 'validation_gate_rejected');
|
|
saveRejectedBuffer(tmpDir, SKILL, [entry]);
|
|
const loaded = loadRejectedBuffer(tmpDir, SKILL);
|
|
expect(loaded).toHaveLength(1);
|
|
expect(loaded[0]!.key).toBe(entry.key);
|
|
expect(loaded[0]!.reason).toBe('validation_gate_rejected');
|
|
});
|
|
|
|
test('LRU cap evicts oldest entries beyond REJECTED_BUFFER_CAP', () => {
|
|
const entries = [];
|
|
for (let i = 0; i < REJECTED_BUFFER_CAP + 20; i++) {
|
|
const e = makeRejectedEntry(`v${i}`, [{ op: 'add', anchor: 'A', content: String(i) }], 'reason');
|
|
// Stagger timestamps so newer entries win the LRU sort.
|
|
e.ts = new Date(2026, 0, 1, 0, 0, i).toISOString();
|
|
entries.push(e);
|
|
}
|
|
saveRejectedBuffer(tmpDir, SKILL, entries);
|
|
const loaded = loadRejectedBuffer(tmpDir, SKILL);
|
|
expect(loaded).toHaveLength(REJECTED_BUFFER_CAP);
|
|
// Newest entry (highest i) must be preserved.
|
|
const newestKey = entries[entries.length - 1]!.key;
|
|
expect(loaded.some((e) => e.key === newestKey)).toBe(true);
|
|
});
|
|
|
|
test('isRejected detects an exact key match', () => {
|
|
const edits: EditOp[] = [{ op: 'add', anchor: 'A', content: 'x' }];
|
|
const entry = makeRejectedEntry('v1', edits, 'r');
|
|
saveRejectedBuffer(tmpDir, SKILL, [entry]);
|
|
const buf = loadRejectedBuffer(tmpDir, SKILL);
|
|
expect(isRejected(buf, 'v1', edits)).toBe(true);
|
|
expect(isRejected(buf, 'different skill text', edits)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('rejectedFilePath', () => {
|
|
test('returns canonical path', () => {
|
|
const p = rejectedFilePath(tmpDir, 'foo');
|
|
expect(p).toBe(path.join(tmpDir, 'foo', 'skillopt', 'rejected.json'));
|
|
});
|
|
});
|