Bug fixes
This commit is contained in:
49
__tests__/server/scanConfidenceHonesty.test.js
Normal file
49
__tests__/server/scanConfidenceHonesty.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { applyCatalogGrounding } = require('../../server/lib/scanGrounding');
|
||||
|
||||
describe('scan confidence honesty', () => {
|
||||
const catalogEntries = [
|
||||
{
|
||||
name: 'Rose',
|
||||
botanicalName: 'Rosa chinensis',
|
||||
description: 'Catalog rose entry.',
|
||||
careInfo: { waterIntervalDays: 4, light: 'Full sun', temp: '15-25C' },
|
||||
},
|
||||
];
|
||||
|
||||
const lowConfidenceAiResult = {
|
||||
name: 'Rose',
|
||||
botanicalName: 'Rosa chinensis',
|
||||
confidence: 0.55,
|
||||
description: 'Possibly a rose, image is ambiguous.',
|
||||
careInfo: { waterIntervalDays: 5, light: 'Full sun', temp: '15-25C' },
|
||||
};
|
||||
|
||||
it('does not inflate a low AI confidence when a catalog match is found', () => {
|
||||
const { grounded, result } = applyCatalogGrounding(lowConfidenceAiResult, catalogEntries, 'en');
|
||||
|
||||
expect(grounded).toBe(true);
|
||||
// Regression: this used to be forced up to at least 0.78, presenting an
|
||||
// uncertain model guess as a confident identification.
|
||||
expect(result.confidence).toBe(0.55);
|
||||
});
|
||||
|
||||
it('keeps a high AI confidence unchanged', () => {
|
||||
const { result } = applyCatalogGrounding(
|
||||
{ ...lowConfidenceAiResult, confidence: 0.9 },
|
||||
catalogEntries,
|
||||
'en',
|
||||
);
|
||||
|
||||
expect(result.confidence).toBe(0.9);
|
||||
});
|
||||
|
||||
it('clamps missing confidence to the neutral default without boosting it', () => {
|
||||
const { result } = applyCatalogGrounding(
|
||||
{ ...lowConfidenceAiResult, confidence: undefined },
|
||||
catalogEntries,
|
||||
'en',
|
||||
);
|
||||
|
||||
expect(result.confidence).toBe(0.6);
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ describe('scan language guards', () => {
|
||||
expect(grounded.result.botanicalName).toBe('Euphorbia pulcherrima');
|
||||
expect(grounded.result.description).toContain('identified with AI');
|
||||
expect(grounded.result.careInfo.light).toBe('Bright indirect light');
|
||||
expect(grounded.result.confidence).toBeGreaterThanOrEqual(0.78);
|
||||
expect(grounded.result.confidence).toBe(0.66);
|
||||
});
|
||||
|
||||
it('keeps a botanical fallback name for English scans when the catalog name is German', () => {
|
||||
|
||||
104
__tests__/server/scanReview.test.js
Normal file
104
__tests__/server/scanReview.test.js
Normal file
@@ -0,0 +1,104 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user