Files
gbrain/test/cli-bigint-normalize.test.ts
26d2f8abfc fix(calibration,takes,cli): calibration CLI routing, source-scoped takes reads, BigInt-safe outputs (takeover of #2452) (#2892)
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>
2026-07-16 21:20:12 -07:00

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 });
});
});