Files
gbrain/test/emotional-weight.test.ts
b0f74017d7 fix(calibration): resolve owner holder via config (default 'self') (#3077)
* 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>
2026-07-23 12:03:26 -07:00

119 lines
4.3 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import {
computeEmotionalWeight,
HIGH_EMOTION_TAGS,
DEFAULT_USER_HOLDER,
} from '../src/core/cycle/emotional-weight.ts';
describe('computeEmotionalWeight', () => {
test('empty inputs return 0', () => {
expect(computeEmotionalWeight({ tags: [], takes: [] })).toBe(0);
});
test('high-emotion tag alone gives 0.5', () => {
const w = computeEmotionalWeight({ tags: ['wedding'], takes: [] });
expect(w).toBeCloseTo(0.5, 5);
});
test('tag matching is case-insensitive', () => {
const w = computeEmotionalWeight({ tags: ['WEDDING'], takes: [] });
expect(w).toBeCloseTo(0.5, 5);
});
test('non-emotion tag gives no boost', () => {
const w = computeEmotionalWeight({ tags: ['hardware', 'product'], takes: [] });
expect(w).toBe(0);
});
test('multiple high-emotion tags do not stack (cap at 0.5)', () => {
const w = computeEmotionalWeight({ tags: ['family', 'wedding', 'love'], takes: [] });
expect(w).toBeCloseTo(0.5, 5);
});
test('takes-only: density caps at 0.3 with 3+ takes', () => {
const takes = Array.from({ length: 5 }, () => ({
holder: 'other',
weight: 0,
kind: 'fact',
active: true,
}));
// density = min(5*0.1, 0.3) = 0.3; avg-weight = 0; holder-ratio = 0 (none match user).
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.3, 5);
});
test('takes-only: avg-weight contribution scales 0..0.1', () => {
const takes = [
{ holder: 'other', weight: 1.0, kind: 'take', active: true },
];
// density = 0.1; avg-weight = 1.0 * 0.1 = 0.1; holder-ratio = 0.
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
});
test('user-holder takes give the 0.1 ratio bump', () => {
const takes = [
{ holder: DEFAULT_USER_HOLDER, weight: 0, kind: 'take', active: true },
];
// density = 0.1; avg-weight = 0; holder-ratio = 1.0 * 0.1 = 0.1.
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
});
test('user-holder ratio is mixed-holder aware', () => {
const takes = [
{ holder: DEFAULT_USER_HOLDER, weight: 0, kind: 'take', active: true },
{ holder: 'other', weight: 0, kind: 'take', active: true },
];
// 1 of 2 active takes = 0.5 ratio * 0.1 = 0.05; density = 0.2; avg = 0.
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.25, 5);
});
test('inactive takes are excluded from density + ratio + avg', () => {
const takes = [
{ holder: DEFAULT_USER_HOLDER, weight: 1, kind: 'take', active: false },
{ holder: DEFAULT_USER_HOLDER, weight: 1, kind: 'take', active: false },
];
expect(computeEmotionalWeight({ tags: [], takes })).toBe(0);
});
test('output is bounded to [0..1]', () => {
// High-emotion tag + max takes + max weights + all-user-holder = 0.5+0.3+0.1+0.1 = 1.0
const takes = Array.from({ length: 10 }, () => ({
holder: DEFAULT_USER_HOLDER,
weight: 1.0,
kind: 'take',
active: true,
}));
const w = computeEmotionalWeight({ tags: ['wedding'], takes });
expect(w).toBeCloseTo(1.0, 5);
expect(w).toBeLessThanOrEqual(1);
});
test('over-1 take weights are clamped before averaging', () => {
// weight=2.0 should clamp to 1.0 in the avg path.
const takes = [
{ holder: 'other', weight: 2.0, kind: 'take', active: true },
];
expect(computeEmotionalWeight({ tags: [], takes })).toBeCloseTo(0.2, 5);
});
test('custom highEmotionTags override default', () => {
const customTags: ReadonlySet<string> = new Set(['hardware-failure']);
const w = computeEmotionalWeight(
{ tags: ['hardware-failure'], takes: [] },
{ highEmotionTags: customTags },
);
expect(w).toBeCloseTo(0.5, 5);
// Default seed list still excludes hardware-failure so without override:
expect(computeEmotionalWeight({ tags: ['hardware-failure'], takes: [] })).toBe(0);
});
test('HIGH_EMOTION_TAGS includes the v1 seed list', () => {
expect(HIGH_EMOTION_TAGS.has('wedding')).toBe(true);
expect(HIGH_EMOTION_TAGS.has('family')).toBe(true);
expect(HIGH_EMOTION_TAGS.has('mental-health')).toBe(true);
});
});
test('DEFAULT_USER_HOLDER is the canonical owner holder self', () => {
expect(DEFAULT_USER_HOLDER).toBe('self');
});