Bug fixes

This commit is contained in:
2026-07-05 17:52:43 +02:00
parent fb6108bd0c
commit c40531b6ae
12 changed files with 1196 additions and 946 deletions

View 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);
});
});

View File

@@ -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', () => {

View 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);
});
});