Files
0pt/tests/document-workbench/structured-output-validation.test.ts
T
admin eec8cb2396
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / security (push) Successful in 18s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (push) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 43s
CI (SIAX Cloud) / quality (push) Successful in 1m23s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m22s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 2m4s
feat: validate source-backed decimals and dates
2026-11-02 02:20:54 +01:00

60 lines
4.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { validateStructuredOutput } from '../../src/document-workbench/structured-output-validation';
const sample = {
fields: [{ name: 'name', type: 'string' as const, required: true }, { name: 'count', type: 'integer' as const, required: true }],
value: { name: 'Acme', count: 12 },
sources: [{ id: 'page-1', text: 'Name: Acme\nCount: 12' }],
evidence: [
{ field: 'name', sourceId: 'page-1', start: 6, end: 10, transform: 'exact' as const },
{ field: 'count', sourceId: 'page-1', start: 18, end: 20, transform: 'integer' as const },
],
};
describe('structured output mechanical validation', () => {
it('accepts typed fields with matching source ranges', () => {
expect(validateStructuredOutput(sample).status).toBe('mechanically_validated');
});
it('does not accept invented, mistyped or extra values', () => {
const result = validateStructuredOutput({ ...sample, value: { name: 'Other', count: '12', extra: 1 } });
expect(result).toMatchObject({ status: 'needs_review', issues: expect.arrayContaining([
{ field: 'name', code: 'evidence_mismatch' }, { field: 'count', code: 'invalid_type' }, { field: 'extra', code: 'unexpected_field' },
]) });
});
it('rejects absent, duplicate or out-of-range evidence and ambiguous sources', () => {
expect(validateStructuredOutput({ ...sample, evidence: [] }).status).toBe('needs_review');
expect(validateStructuredOutput({ ...sample, evidence: [...sample.evidence, sample.evidence[0]!] }).status).toBe('needs_review');
expect(validateStructuredOutput({ ...sample, evidence: [{ ...sample.evidence[0]!, end: 999 }, sample.evidence[1]!] }).status).toBe('needs_review');
expect(validateStructuredOutput({ ...sample, sources: [...sample.sources, sample.sources[0]!] }).status).toBe('needs_review');
});
it('refuses unsafe integers and absent required fields', () => {
expect(validateStructuredOutput({ ...sample, value: { name: 'Acme', count: Number.MAX_SAFE_INTEGER + 1 } }).status).toBe('needs_review');
expect(validateStructuredOutput({ ...sample, value: { name: 'Acme' }, evidence: [sample.evidence[0]!] }).status).toBe('needs_review');
});
it('preserves exact decimal precision and accepts real ISO calendar dates', () => {
const text = '9007199254740993.20 2024-02-29';
const input = {
fields: [{ name: 'amount', type: 'decimal' as const, required: true }, { name: 'date', type: 'date' as const, required: true }],
value: { amount: '9007199254740993.20', date: '2024-02-29' }, sources: [{ id: 'p', text }],
evidence: [
{ field: 'amount', sourceId: 'p', start: 0, end: 19, transform: 'decimal' as const },
{ field: 'date', sourceId: 'p', start: 20, end: 30, transform: 'date' as const },
],
};
expect(validateStructuredOutput(input).status).toBe('mechanically_validated');
expect(validateStructuredOutput({ ...input, value: { ...input.value, amount: 9007199254740994 } }).status).toBe('needs_review');
expect(validateStructuredOutput({ ...input, value: { ...input.value, amount: '9007199254740993.2' } }).status).toBe('needs_review');
});
it.each(['2023-02-29', '1900-02-29', '2024-13-01', '2024-04-31', '0000-01-01'])('rejects invalid date %s even if present in the source', (text) => {
expect(validateStructuredOutput({
fields: [{ name: 'date', type: 'date', required: true }], value: { date: text }, sources: [{ id: 'p', text }],
evidence: [{ field: 'date', sourceId: 'p', start: 0, end: text.length, transform: 'date' }],
}).status).toBe('needs_review');
});
it.each(['01.0', '1e3', '1,25', '+1.0', '.5'])('rejects ambiguous decimal %s rather than guessing', (text) => {
expect(validateStructuredOutput({
fields: [{ name: 'amount', type: 'decimal', required: true }], value: { amount: text }, sources: [{ id: 'p', text }],
evidence: [{ field: 'amount', sourceId: 'p', start: 0, end: text.length, transform: 'decimal' }],
}).status).toBe('needs_review');
});
});