mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
Rebase-port of #2452 (spinsirr:fix/calibration-profile-scope-and-cli) onto current master after tonight's merges made the fork branch conflict. - cli: add 'calibration' to CLI_ONLY so dispatch reaches its existing handler instead of falling through to "Unknown command" (#2035); honor --source / GBRAIN_SOURCE in the calibration CLI. - takes: route takes_list / takes_search / takes_scorecard / takes_calibration through sourceScopeOpts(ctx) (federated array > scalar > nothing) and scope engine reads via the take's page.source_id — JOIN filter for list/search, EXISTS for scorecard/curve — on both engines (#2200-class). - bigint: shared takeHitRowToHit coercion in searchTakes / searchTakesVector (both engines) + bigintToStringReplacer on the cli.ts output normalizer and the `gbrain call` exit, so int8/BIGSERIAL columns no longer crash JSON.stringify (#2450); calibration profile id BIGSERIAL → string. - calibration: default model ids route through TIER_DEFAULTS (provider-prefixed) instead of bare model strings; admin calibration chart endpoints fixed (takes has no page_slug column; month-precision since_date; Date generated_at; bigint id in drill-down). The think/gather source-scope slice of the original PR was dropped: it already landed on master via #2739. Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: spinsirr <ID+spinsirr@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
99 lines
4.3 KiB
TypeScript
99 lines
4.3 KiB
TypeScript
/**
|
|
* Regression: takes read ops honor source / federated_read scope via the
|
|
* take's page.source_id, while preserving the holder allow-list. (#2200-class;
|
|
* see garrytan/gbrain#2200 comment.)
|
|
*
|
|
* takes has no source_id column of its own — it's scoped through
|
|
* JOIN pages.source_id (list/search) or EXISTS pages (scorecard/curve).
|
|
* PGLite, in-memory, no DATABASE_URL required.
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
async function addSource(id: string): Promise<void> {
|
|
await engine.executeRaw(
|
|
`INSERT INTO sources (id, name, local_path, config, created_at)
|
|
VALUES ($1, $1, NULL, '{}'::jsonb, NOW()) ON CONFLICT (id) DO NOTHING`,
|
|
[id],
|
|
);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
await addSource('tenant-a');
|
|
await addSource('tenant-b');
|
|
|
|
const pageA = await engine.putPage('people/a-ex', { title: 'A', type: 'person', compiled_truth: '## Takes\n' }, { sourceId: 'tenant-a' });
|
|
const pageB = await engine.putPage('people/b-ex', { title: 'B', type: 'person', compiled_truth: '## Takes\n' }, { sourceId: 'tenant-b' });
|
|
|
|
await engine.addTakesBatch([
|
|
{ page_id: pageA.id, row_num: 1, claim: 'Acme founder will raise a Series A', kind: 'bet', holder: 'garry', weight: 0.7 },
|
|
{ page_id: pageA.id, row_num: 2, claim: 'Acme founder went public', kind: 'bet', holder: 'world', weight: 0.6 },
|
|
{ page_id: pageB.id, row_num: 1, claim: 'Beta founder will exit big', kind: 'bet', holder: 'garry', weight: 0.8 },
|
|
]);
|
|
// Resolve so the scorecard/curve aggregates have correct/incorrect rows.
|
|
await engine.resolveTake(pageA.id, 1, { quality: 'correct', resolvedBy: 'garry' });
|
|
await engine.resolveTake(pageA.id, 2, { quality: 'incorrect', resolvedBy: 'world' });
|
|
await engine.resolveTake(pageB.id, 1, { quality: 'correct', resolvedBy: 'garry' });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
describe('listTakes / searchTakes — JOIN pages.source_id scope', () => {
|
|
test('sourceIds (federated) filters to the listed source', async () => {
|
|
const a = await engine.listTakes({ sourceIds: ['tenant-a'] });
|
|
expect(a).toHaveLength(2);
|
|
expect(a.every(t => t.page_slug === 'people/a-ex')).toBe(true);
|
|
});
|
|
|
|
test('scalar sourceId filters to that source', async () => {
|
|
const b = await engine.listTakes({ sourceId: 'tenant-b' });
|
|
expect(b).toHaveLength(1);
|
|
expect(b[0].page_slug).toBe('people/b-ex');
|
|
});
|
|
|
|
test('no source scope → all sources (local CLI behavior unchanged)', async () => {
|
|
const all = await engine.listTakes({});
|
|
expect(all).toHaveLength(3);
|
|
});
|
|
|
|
test('source scope AND holder allow-list compose (intersection)', async () => {
|
|
const r = await engine.listTakes({ sourceIds: ['tenant-a'], takesHoldersAllowList: ['world'] });
|
|
expect(r).toHaveLength(1);
|
|
expect(r[0].holder).toBe('world');
|
|
expect(r[0].page_slug).toBe('people/a-ex');
|
|
});
|
|
|
|
test('searchTakes honors source scope', async () => {
|
|
const a = await engine.searchTakes('founder', { sourceIds: ['tenant-a'] });
|
|
expect(a.length).toBeGreaterThan(0);
|
|
expect(a.every(h => h.page_slug === 'people/a-ex')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getScorecard / getCalibrationCurve — EXISTS pages.source_id scope', () => {
|
|
test('getScorecard counts only the scoped source', async () => {
|
|
expect((await engine.getScorecard({ sourceIds: ['tenant-a'] }, undefined)).total_bets).toBe(2);
|
|
expect((await engine.getScorecard({ sourceId: 'tenant-b' }, undefined)).total_bets).toBe(1);
|
|
expect((await engine.getScorecard({}, undefined)).total_bets).toBe(3);
|
|
});
|
|
|
|
test('getScorecard source scope AND holder allow-list compose', async () => {
|
|
// tenant-a bets: garry(correct) + world(incorrect); allow-list world → 1 bet
|
|
expect((await engine.getScorecard({ sourceIds: ['tenant-a'] }, ['world'])).total_bets).toBe(1);
|
|
});
|
|
|
|
test('getCalibrationCurve buckets only the scoped source', async () => {
|
|
const a = await engine.getCalibrationCurve({ sourceIds: ['tenant-a'] }, undefined);
|
|
expect(a.reduce((s, b) => s + b.n, 0)).toBe(2);
|
|
const b = await engine.getCalibrationCurve({ sourceId: 'tenant-b' }, undefined);
|
|
expect(b.reduce((s, x) => s + x.n, 0)).toBe(1);
|
|
});
|
|
});
|