mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
* fix(calibration): resolve owner holder via config (default 'self'), fixes #2464 Takeover of #2467 (rebased onto master). consolidate writes owner takes with holder='self' while calibration-profile, calibration CLI/op, think's calibration block, emotional-weight, and doctor's calibration_freshness all defaulted to a hardcoded 'garry' — so getScorecard returned 0 resolved and the calibration profile never built on non-upstream brains. New src/core/owner-holder.ts is the single source of truth: resolveOwnerHolder({override, configValue}) = override > emotional_weight.user_holder config > 'self'. All six call sites route through it; doctor's freshness SQL is parameterized (). Upgrade note: upstream-owner brains with historical holder='garry' profiles should `gbrain config set emotional_weight.user_holder garry` to keep reading them. Co-authored-by: devty <devty@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(calibration): replace real-name holder fixture with charlie-example placeholder Privacy iron rule: no real people's names in checked-in code. The sanctioned placeholder mapping uses people/charlie-example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: devty <devty@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Garry Tan <garrytan@gmail.com>
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
/**
|
|
* v0.36.1.0 (T7) — gbrain calibration CLI + get_calibration_profile MCP op tests.
|
|
*
|
|
* Hermetic. Mock engine + injected args.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
getLatestProfile,
|
|
getCalibrationProfileOp,
|
|
formatProfileText,
|
|
__testing,
|
|
type CalibrationProfileRow,
|
|
} from '../src/commands/calibration.ts';
|
|
import type { OperationContext } from '../src/core/operations.ts';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
import { GBrainError } from '../src/core/types.ts';
|
|
|
|
const { parseArgs } = __testing;
|
|
|
|
function buildMockEngine(opts: { rows: CalibrationProfileRow[] }): {
|
|
engine: BrainEngine;
|
|
capturedSql: string[];
|
|
capturedParams: unknown[][];
|
|
} {
|
|
const capturedSql: string[] = [];
|
|
const capturedParams: unknown[][] = [];
|
|
const engine = {
|
|
kind: 'pglite',
|
|
// #2464: getCalibrationProfileOp resolves the owner holder via
|
|
// resolveOwnerHolder(config emotional_weight.user_holder, else 'self'), so the
|
|
// mock must implement getConfig. null = key unset → resolver falls back to 'self'.
|
|
async getConfig(): Promise<string | null> {
|
|
return null;
|
|
},
|
|
async executeRaw<T>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
capturedSql.push(sql);
|
|
capturedParams.push(params ?? []);
|
|
// SELECT first row matching holder + optional source filter
|
|
const holder = (params ?? [])[0];
|
|
const matching = opts.rows.filter(r => r.holder === holder);
|
|
if ((params ?? []).length > 1) {
|
|
const p2 = (params ?? [])[1];
|
|
if (Array.isArray(p2)) {
|
|
return matching.filter(r => (p2 as string[]).includes(r.source_id)) as unknown as T[];
|
|
}
|
|
return matching.filter(r => r.source_id === p2) as unknown as T[];
|
|
}
|
|
return matching as unknown as T[];
|
|
},
|
|
} as unknown as BrainEngine;
|
|
return { engine, capturedSql, capturedParams };
|
|
}
|
|
|
|
function buildCtx(engine: BrainEngine, opts: { sourceId?: string; allowedSources?: string[] } = {}): OperationContext {
|
|
const ctx: OperationContext = {
|
|
engine,
|
|
config: {} as never,
|
|
logger: { info() {}, warn() {}, error() {} } as never,
|
|
dryRun: false,
|
|
remote: false,
|
|
sourceId: opts.sourceId ?? 'default',
|
|
};
|
|
if (opts.allowedSources) ctx.auth = { allowedSources: opts.allowedSources } as never;
|
|
return ctx;
|
|
}
|
|
|
|
function buildProfile(opts: Partial<CalibrationProfileRow> & { holder: string }): CalibrationProfileRow {
|
|
return {
|
|
id: '1',
|
|
source_id: opts.source_id ?? 'default',
|
|
holder: opts.holder,
|
|
wave_version: 'v0.36.1.0',
|
|
generated_at: '2026-05-17T15:00:00Z',
|
|
published: opts.published ?? false,
|
|
total_resolved: opts.total_resolved ?? 12,
|
|
brier: opts.brier ?? 0.21,
|
|
accuracy: opts.accuracy ?? 0.6,
|
|
partial_rate: opts.partial_rate ?? 0.1,
|
|
grade_completion: opts.grade_completion ?? 1.0,
|
|
pattern_statements: opts.pattern_statements ?? ['You called early-stage tactics well — 8 of 10 held up.'],
|
|
active_bias_tags: opts.active_bias_tags ?? ['over-confident-geography'],
|
|
voice_gate_passed: opts.voice_gate_passed ?? true,
|
|
voice_gate_attempts: opts.voice_gate_attempts ?? 1,
|
|
model_id: 'claude-sonnet-4-6',
|
|
};
|
|
}
|
|
|
|
// ─── parseArgs ──────────────────────────────────────────────────────
|
|
|
|
describe('parseArgs', () => {
|
|
test('empty args: defaults applied (no holder, no flags)', () => {
|
|
expect(parseArgs([])).toEqual({ sub: undefined, opts: {} });
|
|
});
|
|
|
|
test('--holder <id>', () => {
|
|
expect(parseArgs(['--holder', 'people/charlie-example']).opts.holder).toBe('people/charlie-example');
|
|
});
|
|
|
|
test('--json flag', () => {
|
|
expect(parseArgs(['--json']).opts.json).toBe(true);
|
|
});
|
|
|
|
test('--regenerate flag', () => {
|
|
expect(parseArgs(['--regenerate']).opts.regenerate).toBe(true);
|
|
});
|
|
|
|
test('--source <id> (so the reachable command can target a non-default source)', () => {
|
|
expect(parseArgs(['--source', 'canon']).opts.source).toBe('canon');
|
|
});
|
|
|
|
test('--undo-wave <version>', () => {
|
|
expect(parseArgs(['--undo-wave', 'v0.36.1.0']).opts.undoWave).toBe('v0.36.1.0');
|
|
});
|
|
|
|
test('ab-report subcommand', () => {
|
|
expect(parseArgs(['ab-report']).opts.abReport).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── getLatestProfile ───────────────────────────────────────────────
|
|
|
|
describe('getLatestProfile', () => {
|
|
test('returns the row when holder matches', async () => {
|
|
const { engine } = buildMockEngine({ rows: [buildProfile({ holder: 'garry' })] });
|
|
const profile = await getLatestProfile(engine, { holder: 'garry', sourceId: 'default' });
|
|
expect(profile).not.toBeNull();
|
|
expect(profile!.holder).toBe('garry');
|
|
});
|
|
|
|
test('returns null when no profile exists', async () => {
|
|
const { engine } = buildMockEngine({ rows: [] });
|
|
const profile = await getLatestProfile(engine, { holder: 'unknown', sourceId: 'default' });
|
|
expect(profile).toBeNull();
|
|
});
|
|
|
|
test('source-scoped query: scalar sourceId filters to that source', async () => {
|
|
const rows = [
|
|
buildProfile({ holder: 'garry', source_id: 'default' }),
|
|
buildProfile({ holder: 'garry', source_id: 'tenant-b' }),
|
|
];
|
|
const { engine } = buildMockEngine({ rows });
|
|
const profile = await getLatestProfile(engine, { holder: 'garry', sourceId: 'tenant-b' });
|
|
expect(profile!.source_id).toBe('tenant-b');
|
|
});
|
|
|
|
test('federated array filters to any of the listed sources', async () => {
|
|
const rows = [
|
|
buildProfile({ holder: 'garry', source_id: 'tenant-a' }),
|
|
buildProfile({ holder: 'garry', source_id: 'tenant-c' }),
|
|
];
|
|
const { engine, capturedSql, capturedParams } = buildMockEngine({ rows });
|
|
await getLatestProfile(engine, { holder: 'garry', sourceIds: ['tenant-a', 'tenant-b'] });
|
|
expect(capturedSql[0]).toContain('= ANY($2::text[])');
|
|
expect(capturedParams[0]![1]).toEqual(['tenant-a', 'tenant-b']);
|
|
});
|
|
|
|
test('no source filter when neither sourceId nor sourceIds is passed', async () => {
|
|
const { engine, capturedSql } = buildMockEngine({ rows: [] });
|
|
await getLatestProfile(engine, { holder: 'garry' });
|
|
// SELECT clause names the column but WHERE clause omits source_id filter.
|
|
expect(capturedSql[0]).not.toContain('AND source_id');
|
|
});
|
|
|
|
test('coerces BIGSERIAL bigint id to number so JSON.stringify is safe (#2450)', async () => {
|
|
const engine = {
|
|
kind: 'pglite',
|
|
async executeRaw<T>(): Promise<T[]> {
|
|
return [{ ...buildProfile({ holder: 'brain' }), id: 10n }] as unknown as T[];
|
|
},
|
|
} as unknown as BrainEngine;
|
|
const p = await getLatestProfile(engine, { holder: 'brain' });
|
|
expect(typeof p!.id).toBe('string');
|
|
expect(p!.id).toBe('10');
|
|
expect(() => JSON.stringify(p)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── formatProfileText ──────────────────────────────────────────────
|
|
|
|
describe('formatProfileText', () => {
|
|
test('null profile prints helpful cold-brain message', () => {
|
|
const out = formatProfileText(null, 'garry');
|
|
expect(out).toContain('No calibration profile yet');
|
|
expect(out).toContain('gbrain dream --phase calibration_profile');
|
|
});
|
|
|
|
test('happy profile prints Brier + accuracy + patterns + bias tags', () => {
|
|
const p = buildProfile({ holder: 'garry' });
|
|
const out = formatProfileText(p, 'garry');
|
|
expect(out).toContain('holder: garry');
|
|
expect(out).toContain('Brier:');
|
|
expect(out).toContain('Pattern statements:');
|
|
expect(out).toContain('• You called early-stage tactics');
|
|
expect(out).toContain('Active bias tags: over-confident-geography');
|
|
});
|
|
|
|
test('partial-grade row prints "60% graded" note', () => {
|
|
const p = buildProfile({ holder: 'garry', grade_completion: 0.6 });
|
|
const out = formatProfileText(p, 'garry');
|
|
expect(out).toContain('60% graded');
|
|
});
|
|
|
|
test('voice-gate-failed row prints template-fallback note', () => {
|
|
const p = buildProfile({ holder: 'garry', voice_gate_passed: false, voice_gate_attempts: 2 });
|
|
const out = formatProfileText(p, 'garry');
|
|
expect(out).toContain('voice gate fell back to template');
|
|
});
|
|
|
|
test('published=true is annotated', () => {
|
|
const p = buildProfile({ holder: 'garry', published: true });
|
|
const out = formatProfileText(p, 'garry');
|
|
expect(out).toContain('published to mounts');
|
|
});
|
|
});
|
|
|
|
// ─── getCalibrationProfileOp ────────────────────────────────────────
|
|
|
|
describe('getCalibrationProfileOp (MCP)', () => {
|
|
test('defaults holder to "self" when omitted (config emotional_weight.user_holder unset)', async () => {
|
|
const { engine } = buildMockEngine({ rows: [buildProfile({ holder: 'self' })] });
|
|
const ctx = buildCtx(engine);
|
|
const result = await getCalibrationProfileOp(ctx, {});
|
|
expect(result?.holder).toBe('self');
|
|
});
|
|
|
|
test('routes through sourceScopeOpts: scalar source-bound client gets source-scoped result', async () => {
|
|
const rows = [
|
|
buildProfile({ holder: 'self', source_id: 'default' }),
|
|
buildProfile({ holder: 'self', source_id: 'tenant-b' }),
|
|
];
|
|
const { engine } = buildMockEngine({ rows });
|
|
const ctx = buildCtx(engine, { sourceId: 'tenant-b' });
|
|
const result = await getCalibrationProfileOp(ctx, {});
|
|
expect(result?.source_id).toBe('tenant-b');
|
|
});
|
|
|
|
test('federated read scope sees the union of allowed sources', async () => {
|
|
const rows = [
|
|
buildProfile({ holder: 'self', source_id: 'tenant-a' }),
|
|
buildProfile({ holder: 'self', source_id: 'tenant-z' }),
|
|
];
|
|
const { engine } = buildMockEngine({ rows });
|
|
const ctx = buildCtx(engine, { allowedSources: ['tenant-a', 'tenant-b'] });
|
|
const result = await getCalibrationProfileOp(ctx, {});
|
|
// tenant-a is in the federated set → returns it; tenant-z is not → filtered out
|
|
expect(result?.source_id).toBe('tenant-a');
|
|
});
|
|
|
|
test('returns null for unknown holder without throwing', async () => {
|
|
const { engine } = buildMockEngine({ rows: [] });
|
|
const ctx = buildCtx(engine);
|
|
expect(await getCalibrationProfileOp(ctx, { holder: 'people/nobody' })).toBeNull();
|
|
});
|
|
|
|
test('throws on empty/non-string holder', async () => {
|
|
const { engine } = buildMockEngine({ rows: [] });
|
|
const ctx = buildCtx(engine);
|
|
try {
|
|
await getCalibrationProfileOp(ctx, { holder: '' });
|
|
throw new Error('should have thrown');
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(GBrainError);
|
|
expect((err as GBrainError).problem).toBe('INVALID_HOLDER');
|
|
}
|
|
});
|
|
});
|