134 lines
5.2 KiB
JavaScript
134 lines
5.2 KiB
JavaScript
const { decideReviewOutcome, reviewAgreesWithPrimary } = require('../../server/lib/scanReview');
|
|
|
|
const ai = (name, botanicalName, confidence) => ({ name, botanicalName, confidence });
|
|
|
|
describe('reviewAgreesWithPrimary', () => {
|
|
it('agrees on matching botanical names regardless of casing/accents', () => {
|
|
expect(reviewAgreesWithPrimary(
|
|
ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7),
|
|
ai('Fensterblatt', 'MONSTERA DELICIOSA', 0.6),
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('agrees on matching common names when botanicals differ', () => {
|
|
expect(reviewAgreesWithPrimary(
|
|
ai('Rose', 'Rosa chinensis', 0.5),
|
|
ai('Rose', 'Rosa hybrida', 0.5),
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('does not agree for different species in the same genus', () => {
|
|
// Regression: post-grounding comparison used to collapse these onto the
|
|
// same catalog entry and fake an agreement.
|
|
expect(reviewAgreesWithPrimary(
|
|
ai('Fiddle Leaf Fig', 'Ficus lyrata', 0.7),
|
|
ai('Rubber Plant', 'Ficus elastica', 0.7),
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('never agrees when either side is missing', () => {
|
|
expect(reviewAgreesWithPrimary(null, ai('Rose', 'Rosa chinensis', 0.5))).toBe(false);
|
|
expect(reviewAgreesWithPrimary(ai('Rose', 'Rosa chinensis', 0.5), null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('decideReviewOutcome', () => {
|
|
it('rejects a disagreeing review at lower confidence', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.85),
|
|
reviewResult: ai('Rose', 'Rosa chinensis', 0.55),
|
|
agrees: false,
|
|
});
|
|
expect(decision).toEqual({ accept: false, replace: false, reason: 'review-rejected-low-confidence' });
|
|
});
|
|
|
|
it('accepts a disagreeing review at higher confidence', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Rose', 'Rosa chinensis', 0.55),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.8),
|
|
agrees: false,
|
|
});
|
|
expect(decision).toEqual({ accept: true, replace: true, reason: 'review-overrode-primary' });
|
|
});
|
|
|
|
it('accepts a disagreeing stronger-model review within the 0.05 calibration margin', () => {
|
|
// Models are not calibrated against each other: an honest gpt-5 answer at
|
|
// 0.75 must not lose to an overconfident gpt-5-mini answer at 0.79.
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Rose', 'Rosa chinensis', 0.79),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.75),
|
|
agrees: false,
|
|
});
|
|
expect(decision).toEqual({ accept: true, replace: true, reason: 'review-overrode-primary' });
|
|
});
|
|
|
|
it('still rejects a disagreeing review clearly below the margin', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Rose', 'Rosa chinensis', 0.79),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.73),
|
|
agrees: false,
|
|
});
|
|
expect(decision.accept).toBe(false);
|
|
});
|
|
|
|
it('keeps the higher-confidence primary when the review agrees at lower confidence', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.85),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.6),
|
|
agrees: true,
|
|
});
|
|
expect(decision.accept).toBe(true);
|
|
expect(decision.replace).toBe(false);
|
|
expect(decision.reason).toBe('review-confirmed-primary');
|
|
});
|
|
|
|
it('boosts confidence when two different models agree on the species', () => {
|
|
// The 55%-forever bug: primary 0.55 + agreeing review 0.54 must NOT stay
|
|
// at 0.55 — independent cross-model agreement is real evidence.
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.55),
|
|
reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.54),
|
|
agrees: true,
|
|
});
|
|
expect(decision.confidence).toBeCloseTo(0.75, 5);
|
|
});
|
|
|
|
it('caps the agreement-boosted confidence at 0.97', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.95),
|
|
reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.9),
|
|
agrees: true,
|
|
});
|
|
expect(decision.confidence).toBe(0.97);
|
|
});
|
|
|
|
it('gives no confidence bonus on disagreement', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Rose', 'Rosa chinensis', 0.5),
|
|
reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.6),
|
|
agrees: false,
|
|
});
|
|
expect(decision.confidence).toBeUndefined();
|
|
});
|
|
|
|
it('replaces with the agreeing review on a confidence tie (stronger model wins ties)', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7),
|
|
agrees: true,
|
|
});
|
|
expect(decision.accept).toBe(true);
|
|
expect(decision.replace).toBe(true);
|
|
});
|
|
|
|
it('treats missing confidences as 0 without crashing', () => {
|
|
const decision = decideReviewOutcome({
|
|
primaryResult: ai('Rose', 'Rosa chinensis', undefined),
|
|
reviewResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.5),
|
|
agrees: false,
|
|
});
|
|
expect(decision.accept).toBe(true);
|
|
expect(decision.replace).toBe(true);
|
|
});
|
|
});
|