import { describe, expect, it } from 'vitest' import { generatedJobDefaults, generatedJobFormSchema, normalizeGeneratedJobInput, } from '../generated/schema' import type { GeneratedJobDefinition } from '../types/generated-ui' import { validateFieldValue } from '../composables/useFormValidation' import { buildRules } from '../composables/formBuilderSchema' const definition: GeneratedJobDefinition = { id: 'summarize', title: 'Summarize', description: 'Summarize a dataset', cpu: 2, input_schema: { type: 'object', properties: { threshold: { type: 'number', title: 'Threshold', default: 2.5, minimum: 0, maximum: 10 }, method: { type: 'string', title: 'Method', enum: ['fast', 'robust'], default: 'fast' }, normalize: { type: 'boolean', title: 'Normalize', default: true }, source: { anyOf: [{ type: 'string', format: 'path' }, { type: 'null' }], default: null, 'x-mint-component': 'path', }, }, required: ['threshold'], }, output_schema: {}, } describe('generated UI schema adapter', () => { it('allows present empty strings, arrays, nullable values and false', () => { const requiredDefinition: GeneratedJobDefinition = { ...definition, input_schema: { properties: { nullable: { anyOf: [{ type: 'string' }, { type: 'null' }] }, text: { type: 'string' }, labels: { type: 'array', items: { type: 'string' } }, enabled: { type: 'boolean' }, nonempty: { type: 'string', minLength: 1 }, }, required: ['nullable', 'text', 'labels', 'enabled', 'nonempty'], }, } const fields = generatedJobFormSchema(requiredDefinition).sections[0]!.fields const rules = buildRules(fields) const values = { nullable: null, text: '', labels: [], enabled: false, nonempty: 'valid' } expect(fields.map(field => validateFieldValue(values[field.name as keyof typeof values], rules[field.name], values))) .toEqual([null, null, null, null, null]) expect(generatedJobDefaults(requiredDefinition)).toEqual({ nullable: null }) expect(validateFieldValue('', rules.nonempty, {})).not.toBeNull() expect(normalizeGeneratedJobInput(requiredDefinition, values)).toEqual(values) }) it('supplies fixed literal values and resolves array enums and numeric items', () => { const arrayDefinition: GeneratedJobDefinition = { ...definition, input_schema: { $defs: { Choice: { type: 'integer', enum: [1, 2] } }, properties: { kind: { type: 'string', const: 'fixed' }, choices: { type: 'array', items: { $ref: '#/$defs/Choice' } }, counts: { type: 'array', items: { type: 'integer' } }, scales: { type: 'array', items: { type: 'number' } }, labels: { type: 'array', items: { type: 'string' } }, flags: { type: 'array', items: { type: 'boolean' } }, }, }, } expect(generatedJobDefaults(arrayDefinition)).toEqual({ kind: 'fixed' }) expect(generatedJobFormSchema(arrayDefinition).sections[0]!.fields).toMatchObject([ { name: 'kind', defaultValue: 'fixed', disabled: true }, { name: 'choices', type: 'multiselect', props: { options: [1, 2] } }, { name: 'counts', type: 'tags' }, { name: 'scales', type: 'tags' }, { name: 'labels', type: 'tags', props: { allowDuplicates: true } }, { name: 'flags', type: 'tags' }, ]) expect(normalizeGeneratedJobInput(arrayDefinition, { counts: ['1', '2'], scales: ['0.5', '1.5'], choices: [1, 2], labels: ['1', '1'], })).toEqual({ counts: [1, 2], scales: [0.5, 1.5], choices: [1, 2], labels: ['1', '1'] }) expect(normalizeGeneratedJobInput(arrayDefinition, { counts: ['bad', '1.5'] })) .toEqual({ counts: ['bad', 1.5] }) expect(normalizeGeneratedJobInput(arrayDefinition, { flags: ['true', 'false', true, 'bad'] })) .toEqual({ flags: [true, false, true, 'bad'] }) }) it('applies integer bounds and numeric steps to the existing controls', () => { const boundedDefinition: GeneratedJobDefinition = { ...definition, input_schema: { properties: { count: { type: 'integer', exclusiveMinimum: 0, exclusiveMaximum: 10, multipleOf: 2 }, fraction: { type: 'number', minimum: 0, maximum: 1, multipleOf: 0.1 }, } }, } const fields = generatedJobFormSchema(boundedDefinition).sections[0]!.fields const rules = buildRules(fields) expect(fields).toMatchObject([ { validation: { min: 2, max: 8 }, props: { min: 2, max: 8, step: 2 } }, { validation: { min: 0, max: 1 }, props: { min: 0, max: 1, step: 0.1 } }, ]) expect(validateFieldValue(0, rules.count, {})).not.toBeNull() expect(validateFieldValue(10, rules.count, {})).not.toBeNull() }) it('preserves field labels and hints outside nullable schemas and enum references', () => { const annotatedDefinition: GeneratedJobDefinition = { ...definition, input_schema: { type: 'object', $defs: { Method: { type: 'string', enum: ['fast', 'robust'], title: 'Method' } }, properties: { count: { anyOf: [{ type: 'integer', minimum: 1 }, { type: 'null' }], title: 'Optional sample count', description: 'Leave empty to use all samples', default: null, }, method: { $ref: '#/$defs/Method', title: 'Analysis method', description: 'Choose the analysis method', }, }, }, } expect(generatedJobFormSchema(annotatedDefinition).sections[0]?.fields).toMatchObject([ { name: 'count', type: 'number', label: 'Optional sample count', hint: 'Leave empty to use all samples', defaultValue: null, validation: { min: 1 }, }, { name: 'method', type: 'select', label: 'Analysis method', hint: 'Choose the analysis method', props: { options: ['fast', 'robust'] }, }, ]) }) it('maps the supported JSON Schema subset to standard FormBuilder fields', () => { expect(generatedJobFormSchema(definition).sections[0]?.fields).toMatchObject([ { name: 'threshold', type: 'number', validation: { required: true, min: 0, max: 10 } }, { name: 'method', type: 'select', props: { options: ['fast', 'robust'] } }, { name: 'normalize', type: 'toggle' }, { name: 'source', type: 'path' }, ]) }) it('extracts defaults without sharing mutable input state', () => { const defaults = generatedJobDefaults(definition) const second = generatedJobDefaults(definition) defaults.threshold = 9 expect(second).toEqual({ threshold: 2.5, method: 'fast', normalize: true, source: null }) }) it('preserves explicit null defaults without inventing values for missing defaults', () => { const nullableDefinition: GeneratedJobDefinition = { ...definition, input_schema: { type: 'object', properties: { optional_count: { anyOf: [{ type: 'integer' }, { type: 'null' }], default: null, }, required_count: { type: 'integer' }, }, required: ['required_count'], }, } expect({ defaults: generatedJobDefaults(nullableDefinition), fields: generatedJobFormSchema(nullableDefinition).sections[0]?.fields.map(field => ({ name: field.name, defaultValue: field.defaultValue, })), }).toEqual({ defaults: { optional_count: null }, fields: [ { name: 'optional_count', defaultValue: null }, { name: 'required_count', defaultValue: undefined }, ], }) }) it('keeps a selected path for SDK staging during submission', () => { const selection = { kind: 'path' as const, files: [new File(['data'], 'data.csv')], relativePaths: ['data.csv'], } expect(normalizeGeneratedJobInput(definition, { threshold: 2.5, source: selection, })).toEqual({ threshold: 2.5, source: selection, }) }) it('marks path arrays as multiple path inputs', () => { const listDefinition: GeneratedJobDefinition = { ...definition, input_schema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'string', format: 'path' }, 'x-mint-component': 'path', }, }, required: ['sources'], }, } expect(generatedJobFormSchema(listDefinition).sections[0]?.fields[0]).toMatchObject({ name: 'sources', type: 'path', props: { multiple: true }, }) }) })