import test from 'node:test'; import assert from 'node:assert/strict'; import {spawnSync} from 'node:child_process'; import {readFileSync} from 'node:fs'; import {buildPayload, previewPayload, pythonExample, parseAnswer, requestDecision, ENDPOINT} from '../api.mjs'; import {words, examples} from '../locales.mjs'; const make = (extra={}) => buildPayload({model:'valen-2b',mode:'predicate',question:'Is it positive?',context:'Great service!',options:'Low\nHigh',...extra}); test('native predicate, choice and zero-based score request contracts',()=>{ const p=make();assert.equal(p.questions[0].type,'predicate');assert.equal(p.temperature,1); assert.equal(p.questions[0].choices,undefined); assert.deepEqual(make({mode:'choice'}).questions[0].choices[0],{value:'option_1',description:'Low'}); assert.deepEqual(make({mode:'score'}).questions[0].levels[1],{label:'1',description:'High'}); assert.equal(make({context:''}).input,'Is it positive?'); }); test('rejects invalid input before network calls',()=>{ for(const value of [{model:'invalid'},{question:' '},{context:'x'.repeat(4001)},{mode:'choice',options:'one'},{mode:'score',options:Array.from({length:9},(_,i)=>String(i)).join('\n')},{image:'https://example.com/image.jpg'}])assert.throws(()=>make(value)); }); test('images use inline data; preview omits bytes without changing request',()=>{ const p=make({image:'data:image/jpeg;base64,Ymx1ZQ=='}); assert.equal(p.input[0].content[1].type,'input_image'); assert(!JSON.stringify(previewPayload(p)).includes('Ymx1ZQ==')); assert(p.input[0].content[1].image_url.includes('Ymx1ZQ==')); assert(pythonExample(p).includes('Image.open("image.jpg")')); }); test('generated Python preserves literal user text and is syntactically valid',()=>{ const p=make({context:'Quotes " and \'\'\', backslash \\, newlines\n中文 \u2028 and '}); const script=`import ast,json,sys\ndata=json.load(sys.stdin)\ntree=ast.parse(data['code'])\nnode=next(n for n in tree.body if isinstance(n,ast.Assign) and isinstance(n.targets[0],ast.Name) and n.targets[0].id=='payload')\nassert json.loads(ast.literal_eval(node.value.args[0]))==data['payload']\n`; const result=spawnSync('python3',['-c',script],{input:JSON.stringify({code:pythonExample(p),payload:p}),encoding:'utf8'}); assert.equal(result.status,0,result.stderr); }); test('native results preserve optional confidence separately',()=>{ const a=parseAnswer({answers:[{name:'q0',type:'predicate',probability:.8}]},make()); assert.equal(a.title,'yes');assert(Math.abs(a.rows[1].probability-.2)<1e-10);assert.equal(a.confidence,undefined); const c=parseAnswer({answers:[{name:'q0',type:'choice',choice:'option_2',confidence:.4,probabilities:[{value:'option_1',probability:.1},{value:'option_2',probability:.9}]}]},make({mode:'choice'})); assert.equal(c.title,'High');assert.equal(c.confidence,.4); const s=parseAnswer({answers:[{name:'q0',type:'score',score:.9,probabilities:[{value:0,probability:.1},{value:1,probability:.9}]}]},make({mode:'score'})); assert.equal(s.title,'0.90 / 1'); }); test('malformed results cannot render as successful answers',()=>{ for(const body of [null,{answers:[]},{answers:[null]},{answers:[{name:'q0',type:'predicate',probability:NaN}]},{answers:[{name:'wrong',type:'predicate',probability:.8}]}])assert.throws(()=>parseAnswer(body,make()),/response/); assert.throws(()=>parseAnswer({answers:[{name:'q0',type:'choice',choice:'option_1',probabilities:[{value:'option_1',probability:.5},{value:'option_1',probability:.5}]}]},make({mode:'choice'})),/response/); }); test('browser request uses real origin, no credentials or automatic retries',async()=>{ let calls=0; await assert.rejects(requestDecision(make(),undefined,async(url,options)=>{ calls++;assert.equal(url,ENDPOINT);assert.equal(options.credentials,'omit');assert.equal(options.headers.Origin,undefined);assert.equal(options.redirect,'error'); return {ok:false,status:429}; }),/http_429/);assert.equal(calls,1); await assert.rejects(requestDecision(make(),undefined,async()=>({ok:true,json:async()=>{throw new Error('invalid json');}})),/response/); }); test('both locales cover UI labels and all examples create valid payloads',()=>{ const html=readFileSync(new URL('../index.html',import.meta.url),'utf8'); for(const locale of ['en','zh']){ for(const [,key] of html.matchAll(/data-(?:i18n|placeholder)="([^"]+)"/g))assert.equal(typeof words[locale][key],'string',`${locale}:${key}`); for(const [mode,question,context,options] of examples[locale])make({mode,question,context,options}); } });