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