// Read-only API contract checks; these are synthetic observations, not learner // operations. No progress, score or teaching run is created. import fs from 'node:fs'; import assert from 'node:assert/strict'; import {login,call} from './role-permissions-live.mjs'; const session=await login('root'),r=await call('tran/v1/teaching/assignments/86',session.accessToken);assert.equal(r.status,200); const task=r.data,step=task.definitionSnapshot.steps[0],ops=step.action.mechanism.steps,code=step.stepCode||step.id; const observation={schema:'teaching-virtual-observation/1',stepCode:code,kind:'target-click',targetId:ops.at(-1).targetId,completed:true, selectedToolId:'',selectedToolIds:step.toolRequirements?.map(r=>r.anyOf[0])||(step.toolId?[step.toolId]:[]), safetyKeys:(step.safetyRules||[]).map((s,i)=>s.key||s.code||s.id||'SAFETY-'+(i+1)),measurementNote:'', operations:ops.map(op=>({targetId:op.targetId,kind:op.kind,completed:true}))}; const body={version:task.version,steps:[{completedStepCode:code,actionCode:step.actionCode||'COMPLETE_'+code.toUpperCase(),observation}]}; const valid=await call('tran/v1/teaching/assignments/86/preview/validate',session.accessToken,body,'POST'); assert.equal(valid.status,200,'historical task accepts complete observations'); body.steps[0].observation.operations=[]; const invalid=await call('tran/v1/teaching/assignments/86/preview/validate',session.accessToken,body,'POST'); const dir='reports/bridge-tools-authoring-20260924';fs.mkdirSync(dir,{recursive:true}); fs.writeFileSync(dir+'/api-contract.json',JSON.stringify({pass:valid.status===200&&invalid.status===400,validStatus:valid.status,missingOperationsStatus:invalid.status,syntheticReadOnlyContractTest:true},null,2)); assert.equal(invalid.status,400,'incomplete observations rejected by running service');console.log('Live operation contract: complete accepted, missing operations rejected');