feat(image-import): add parseResponse with fence-stripping and schema validation

This commit is contained in:
Kenearos 2026-05-12 00:15:15 +02:00
parent 60e71d4a57
commit 93e6b32fe9
2 changed files with 129 additions and 0 deletions

View file

@ -116,6 +116,65 @@ class ImageImporter {
: ''; : '';
return typeof content === 'string' ? content : JSON.stringify(content); return typeof content === 'string' ? content : JSON.stringify(content);
} }
/**
* Strip markdown fences, brace-slice, JSON.parse, schema-validate.
* Invalid entries are dropped with console warnings.
* @param {string} rawContent
* @returns {{ month: number|null, year: number|null, entries: Array<{name:string,date:string,share:number}>, notes: string[] }}
*/
parseResponse(rawContent) {
if (typeof rawContent !== 'string') {
throw new SyntaxError('Antwort ist kein String');
}
let text = rawContent.trim();
// Strip ```json ... ``` or ``` ... ``` fences
text = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
// Brace-slice: find first { and last }
const firstBrace = text.indexOf('{');
const lastBrace = text.lastIndexOf('}');
if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
throw new SyntaxError('Kein JSON-Objekt in der Antwort gefunden');
}
text = text.slice(firstBrace, lastBrace + 1);
const parsed = JSON.parse(text); // may throw SyntaxError
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.entries)) {
throw new Error('Schema-Fehler: entries fehlt oder ist kein Array');
}
const validEntries = [];
for (const entry of parsed.entries) {
if (!entry || typeof entry.name !== 'string' || entry.name.trim().length === 0) {
console.warn('parseResponse: Eintrag mit leerem Namen verworfen', entry);
continue;
}
if (typeof entry.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.date)) {
console.warn('parseResponse: Eintrag mit ungueltigem Datum verworfen', entry);
continue;
}
const d = new Date(entry.date + 'T12:00:00');
if (isNaN(d.getTime())) {
console.warn('parseResponse: Datum nicht parsebar', entry);
continue;
}
if (entry.share !== 0.5 && entry.share !== 1.0) {
console.warn('parseResponse: Eintrag mit ungueltigem share verworfen', entry);
continue;
}
validEntries.push({ name: entry.name.trim(), date: entry.date, share: entry.share });
}
const month = (typeof parsed.month === 'number' && parsed.month >= 1 && parsed.month <= 12) ? parsed.month : null;
const year = (typeof parsed.year === 'number' && parsed.year >= 2000) ? parsed.year : null;
const notes = Array.isArray(parsed.notes) ? parsed.notes.filter(n => typeof n === 'string') : [];
return { month, year, entries: validEntries, notes };
}
} }
// Verbatim system prompt — German with Umlaute (per spec §7.3). // Verbatim system prompt — German with Umlaute (per spec §7.3).

View file

@ -646,6 +646,76 @@ runner.test('CallVisionAPI: 503 wirft mit Status', async (t) => {
}); });
}); });
// ============================================================================
// ImageImporter Tests - parseResponse (Feature A)
// ============================================================================
runner.test('Parse: cleanes JSON wird geparst', (t) => {
const importer = new ImageImporter(null);
const raw = '{"month":11,"year":2025,"entries":[{"name":"Max","date":"2025-11-22","share":1.0}],"notes":[]}';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, '1 Eintrag');
t.assertEqual(result.entries[0].name, 'Max', 'Name korrekt');
t.assertEqual(result.month, 11, 'Monat korrekt');
});
runner.test('Parse: JSON in Markdown-Fence wird gestrippt', (t) => {
const importer = new ImageImporter(null);
const raw = '```json\n{"entries":[{"name":"A","date":"2025-11-22","share":0.5}],"notes":[]}\n```';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, 'Fence wurde entfernt');
});
runner.test('Parse: JSON mit Vortext wird per Brace-Slicing extrahiert', (t) => {
const importer = new ImageImporter(null);
const raw = 'Hier das Ergebnis:\n{"entries":[{"name":"A","date":"2025-11-22","share":1.0}],"notes":[]}';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, 'Vortext ignoriert');
});
runner.test('Parse: Malformed JSON wirft SyntaxError', (t) => {
const importer = new ImageImporter(null);
try {
importer.parseResponse('das ist kein JSON');
t.assertTrue(false, 'Sollte werfen');
} catch (e) {
t.assertTrue(e instanceof SyntaxError || e.name === 'SyntaxError' || /JSON|Parse/i.test(e.message), 'SyntaxError erwartet');
}
});
runner.test('Parse: fehlendes entries-Feld wirft', (t) => {
const importer = new ImageImporter(null);
try {
importer.parseResponse('{"month":11,"year":2025,"notes":[]}');
t.assertTrue(false, 'Sollte werfen');
} catch (e) {
t.assertTrue(/entries/i.test(e.message), 'Fehlermeldung erwaehnt entries');
}
});
runner.test('Parse: share=0.75 verwirft den Eintrag', (t) => {
const importer = new ImageImporter(null);
const raw = '{"entries":[{"name":"A","date":"2025-11-22","share":0.75},{"name":"B","date":"2025-11-23","share":1.0}],"notes":[]}';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, 'Nur gueltiger Eintrag bleibt');
t.assertEqual(result.entries[0].name, 'B', 'B uebrig');
});
runner.test('Parse: invalides Datum verwirft den Eintrag', (t) => {
const importer = new ImageImporter(null);
const raw = '{"entries":[{"name":"A","date":"31.11.2025","share":1.0},{"name":"B","date":"2025-11-22","share":1.0}],"notes":[]}';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, 'Nur ISO-Datum bleibt');
t.assertEqual(result.entries[0].name, 'B', 'B uebrig');
});
runner.test('Parse: leerer Name wird verworfen', (t) => {
const importer = new ImageImporter(null);
const raw = '{"entries":[{"name":" ","date":"2025-11-22","share":1.0},{"name":"B","date":"2025-11-22","share":1.0}],"notes":[]}';
const result = importer.parseResponse(raw);
t.assertEqual(result.entries.length, 1, 'Nur gueltiger Name bleibt');
});
// ============================================================================ // ============================================================================
// Display Functions // Display Functions
// ============================================================================ // ============================================================================