mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +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>
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
/**
|
|
* Regression tests for the local-op CLI output normalizer and CLI command
|
|
* reachability.
|
|
*
|
|
* - bigintToStringReplacer: cli.ts JSON-normalizes a local op's return value
|
|
* so a bigint column (e.g. a BIGSERIAL `id`) round-trips to a string instead
|
|
* of crashing `JSON.stringify`. (garrytan/gbrain#2450)
|
|
* - CLI_ONLY: `calibration` is reachable; it was missing from the set, so the
|
|
* dispatch fell through to "Unknown command" despite a `case 'calibration'`
|
|
* handler existing. (garrytan/gbrain#2035)
|
|
* - takeHitRowToHit: searchTakes/searchTakesVector coerce int8 driver rows
|
|
* (native BigInt) to the numeric TakeHit contract, so MCP `takes_search`
|
|
* doesn't crash the serializer the moment a query matches. (#2450 comments)
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { bigintToStringReplacer, CLI_ONLY } from '../src/cli.ts';
|
|
import { takeHitRowToHit } from '../src/core/utils.ts';
|
|
import { runCall } from '../src/commands/call.ts';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
|
|
describe('bigintToStringReplacer (#2450)', () => {
|
|
test('serializes a bigint to its string form instead of throwing', () => {
|
|
const raw = { id: 9007199254740993n, total: 5, name: 'x' };
|
|
const out = JSON.parse(JSON.stringify(raw, bigintToStringReplacer));
|
|
expect(out).toEqual({ id: '9007199254740993', total: 5, name: 'x' });
|
|
});
|
|
|
|
test('handles nested + array bigints', () => {
|
|
const raw = { row: { id: 1n }, ids: [2n, 3n], plain: true };
|
|
const out = JSON.parse(JSON.stringify(raw, bigintToStringReplacer));
|
|
expect(out).toEqual({ row: { id: '1' }, ids: ['2', '3'], plain: true });
|
|
});
|
|
|
|
test('leaves non-bigint values untouched', () => {
|
|
expect(bigintToStringReplacer('k', 5)).toBe(5);
|
|
expect(bigintToStringReplacer('k', 's')).toBe('s');
|
|
expect(bigintToStringReplacer('k', null)).toBeNull();
|
|
});
|
|
|
|
test('a bare object with a bigint throws under plain stringify but not with the replacer', () => {
|
|
expect(() => JSON.stringify({ id: 1n })).toThrow();
|
|
expect(() => JSON.stringify({ id: 1n }, bigintToStringReplacer)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('CLI_ONLY command reachability (#2035)', () => {
|
|
test('`calibration` is in CLI_ONLY so dispatch reaches its handler', () => {
|
|
expect(CLI_ONLY.has('calibration')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('takeHitRowToHit (#2450 — takes_search MCP path)', () => {
|
|
test('coerces BigInt int8 columns to numbers per the TakeHit contract', () => {
|
|
const hit = takeHitRowToHit({
|
|
take_id: 42n, page_id: 7n, page_slug: 'people/alice-example', row_num: 3n,
|
|
claim: 'Strong DX intuition', kind: 'take', holder: 'garry',
|
|
weight: 0.8, score: 0.91,
|
|
});
|
|
expect(hit).toEqual({
|
|
take_id: 42, page_id: 7, page_slug: 'people/alice-example', row_num: 3,
|
|
claim: 'Strong DX intuition', kind: 'take', holder: 'garry',
|
|
weight: 0.8, score: 0.91,
|
|
});
|
|
expect(() => JSON.stringify(hit)).not.toThrow();
|
|
});
|
|
|
|
test('a raw driver row with BigInt ids crashes plain stringify; the coerced hit does not', () => {
|
|
const raw = { take_id: 1n, page_id: 2n, row_num: 0n };
|
|
expect(() => JSON.stringify(raw)).toThrow();
|
|
expect(() => JSON.stringify(takeHitRowToHit(raw))).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('runCall output exit is bigint-safe (#2450)', () => {
|
|
test('prints an op result carrying a bigint instead of crashing', async () => {
|
|
// `gbrain call` bypasses cli.ts's normalizer, so its own stringify must
|
|
// carry the replacer. Stub just the surface runCall touches: --source
|
|
// resolution (assertSourceExists) + the get_stats handler pass-through.
|
|
const stub = {
|
|
executeRaw: async (sql: string) =>
|
|
sql.includes('FROM sources') ? [{ id: 'default' }] : [],
|
|
getStats: async () => ({ pages: 42n, chunks: 7 }),
|
|
} as unknown as BrainEngine;
|
|
|
|
const lines: string[] = [];
|
|
const orig = console.log;
|
|
console.log = (msg?: unknown) => { lines.push(String(msg)); };
|
|
try {
|
|
// --source pins tier 1 of resolveSourceId so the test is hermetic
|
|
// against GBRAIN_SOURCE / .gbrain-source on the host machine.
|
|
await runCall(stub, ['--source', 'default', 'get_stats']);
|
|
} finally {
|
|
console.log = orig;
|
|
}
|
|
expect(JSON.parse(lines.join('\n'))).toEqual({ pages: '42', chunks: 7 });
|
|
});
|
|
});
|