@getopencrawl/cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +75 -298
  2. package/bin/opencrawl.cjs +168 -0
  3. package/package.json +20 -14
  4. package/bin/opencrawl.mjs +0 -553
  5. package/browser.mjs +0 -166
  6. package/src/accounts.test.mjs +0 -223
  7. package/src/agent.mjs +0 -95
  8. package/src/agent.test.mjs +0 -58
  9. package/src/answer-contract.mjs +0 -112
  10. package/src/answer-view.mjs +0 -118
  11. package/src/browser.test.mjs +0 -72
  12. package/src/capture.mjs +0 -273
  13. package/src/capture.test.mjs +0 -171
  14. package/src/client.mjs +0 -183
  15. package/src/client.test.mjs +0 -34
  16. package/src/connect.mjs +0 -230
  17. package/src/connect.test.mjs +0 -131
  18. package/src/context.mjs +0 -34
  19. package/src/context.test.mjs +0 -32
  20. package/src/display.mjs +0 -4
  21. package/src/fixtures/letter-connections.json +0 -59
  22. package/src/fixtures/product-monitor-contract.json +0 -312
  23. package/src/fixtures/research-completion-hosted.json +0 -502
  24. package/src/fixtures/research-completion-v1.json +0 -58
  25. package/src/fixtures/research-completion-v2.json +0 -33
  26. package/src/fixtures/research-presentation-hosted.json +0 -720
  27. package/src/jobs.mjs +0 -117
  28. package/src/jobs.test.mjs +0 -67
  29. package/src/mcp-conversations.test.mjs +0 -80
  30. package/src/monitors.mjs +0 -146
  31. package/src/monitors.test.mjs +0 -62
  32. package/src/product-letter-capabilities.mjs +0 -71
  33. package/src/product-letter-read.mjs +0 -178
  34. package/src/product-monitor-login.test.mjs +0 -140
  35. package/src/product-monitors.mjs +0 -321
  36. package/src/product-monitors.test.mjs +0 -69
  37. package/src/research-agent-errors.test.mjs +0 -110
  38. package/src/research-agent-feedback.test.mjs +0 -28
  39. package/src/research-agent.mjs +0 -164
  40. package/src/research-agent.test.mjs +0 -231
  41. package/src/research-answer-contract.mjs +0 -3
  42. package/src/research-answer-contract.test.mjs +0 -14
  43. package/src/research-answer-view.mjs +0 -7
  44. package/src/research-answer-view.test.mjs +0 -27
  45. package/src/research-completion.mjs +0 -94
  46. package/src/research-completion.test.mjs +0 -110
  47. package/src/research-event.mjs +0 -39
  48. package/src/research-event.test.mjs +0 -57
  49. package/src/research-presentation.test.mjs +0 -56
  50. package/src/research.mjs +0 -141
  51. package/src/research.test.mjs +0 -97
  52. package/src/sessions.mjs +0 -165
  53. package/src/sessions.test.mjs +0 -151
  54. package/src/tasks.mjs +0 -115
  55. package/src/tasks.test.mjs +0 -83
  56. package/src/upload-commands.test.mjs +0 -140
  57. package/src/uploads.mjs +0 -141
  58. package/src/uploads.test.mjs +0 -64
@@ -1,164 +0,0 @@
1
- import {readResearchCompletion,readResearchPresentation} from './research-completion.mjs';
2
- import {readResearchAnswerContract} from './research-answer-contract.mjs';
3
- import {readResearchAnswerView} from './research-answer-view.mjs';
4
- import fs from 'node:fs/promises';
5
- import {createHash} from 'node:crypto';
6
- import {allowedOptions,decimal,identifier,settle,transient,withInterrupt} from './jobs.mjs';
7
- import {displayLine,displayText} from './display.mjs';
8
-
9
- const ROOT='/api/research/agents',ACTIVE=new Set(['queued','running']);
10
- const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
11
- const text=(v,max,min=0)=>typeof v==='string'&&Array.from(v).length>=min&&Array.from(v).length<=max&&!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(v);
12
- const integer=(v,min=0,max=Number.MAX_SAFE_INTEGER)=>Number.isSafeInteger(v)&&v>=min&&v<=max;
13
- const stamp=v=>typeof v==='string'&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+00:00)$/.test(v)&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString().slice(0,19)===v.slice(0,19);
14
- const fields=(v,keys)=>Object.keys(v).every(key=>keys.includes(key));
15
- function canonicalWebsite(value){if(/[\\\s\u0000-\u001f\u007f]/.test(value)||/%(?![0-9a-f]{2})/i.test(value))throw new Error('Use a valid public website URL.');const u=new URL(value);u.hash='';return new URL(u.href.replace(/%([0-9a-f]{2})/gi,(_all,hex)=>{const ch=String.fromCharCode(parseInt(hex,16));return /^[A-Za-z0-9._~-]$/.test(ch)?ch:'%'+hex.toUpperCase()})).href;}
16
- function publicUrl(v){if(!text(v,2048,1))return false;try{const u=new URL(v);return ['http:','https:'].includes(u.protocol)&&!u.username&&!u.password&&!!u.hostname}catch{return false}}
17
- function pageEvidence(cite){
18
- if(cite.source_kind!=='web_page')return !Object.hasOwn(cite,'page');
19
- const p=cite.page;
20
- return object(p)&&fields(p,['requested_url','final_url','body_sha256','start','end','total_chars','extractor'])&&publicUrl(p.requested_url)&&publicUrl(p.final_url)&&p.final_url===cite.url&&typeof p.body_sha256==='string'&&/^[0-9a-f]{64}$/.test(p.body_sha256)&&integer(p.start,0,131071)&&integer(p.end,1,131072)&&integer(p.total_chars,1,131072)&&p.start<p.end&&p.end<=p.total_chars&&p.end-p.start<=16000&&typeof cite.end==='number'&&cite.end<=p.end-p.start&&p.extractor==='public-static-v3';
21
- }
22
- const invalid=()=>{throw new Error('Server returned invalid or out-of-scope saved research findings. No result was emitted.');};
23
- const feedbackRatings=['useful','irrelevant','incorrect','outdated'];
24
- const feedbackNote=value=>value===null||text(value,1000)&&!value.includes('\u0000')&&Buffer.byteLength(value)<=4096;
25
- function confirmedReadFailure(error,request){
26
- if((request.method??'GET')!=='GET'||!Number.isInteger(error?.status))return null;
27
- const message={
28
- 401:'Saved research could not be read. Check the selected CLI account, server and workspace and its existing credential.',
29
- 403:'This account cannot read the requested research. Check the selected account and workspace and ask its administrator to confirm access.',
30
- 404:request.command==='receipt'?'The original admission receipt is unavailable to this account. Check the selected workspace and exact operation. A missing receipt does not authorize a new admission.':'The saved research is unavailable to this account. Check the selected workspace and exact agent, run or source identifiers. Do not substitute a current webpage for retained evidence. Watches, jobs and research built by the OpenCrawl agent belong to their project: use that project\'s key (project.api_key from the build), not the organization key.',
31
- 409:'The saved research could not be read in its current state. Read the same agent, schedule or feedback and reconcile its current revision before any write.'
32
- }[error.status];
33
- return message?{code:`http_${error.status}`,message}:null;
34
- }
35
- export function validateAgentFeedback(value,agentId,runId,findingId){
36
- if(!object(value)||!fields(value,['agent_id','run_id','finding_id','feedback'])||value.agent_id!==agentId||value.run_id!==runId||value.finding_id!==findingId||!object(value.feedback))invalid();
37
- const f=value.feedback;if(!fields(f,['version','revision','state','rating','note','updated_at'])||f.version!==1||!integer(f.revision)||!feedbackNote(f.note)||Buffer.byteLength(JSON.stringify(value))>8192)invalid();
38
- if(f.state==='absent'){if(f.revision!==0||f.rating!==null||f.note!==null||f.updated_at!==null)invalid()}
39
- else if(f.state==='deleted'){if(f.revision<1||f.rating!==null||f.note!==null||!stamp(f.updated_at))invalid()}
40
- else if(f.state==='active'){if(f.revision<1||!feedbackRatings.includes(f.rating)||!stamp(f.updated_at))invalid()}
41
- else invalid();return value;
42
- }
43
- export function validateAgentSource(value,agentId,runId,sourceId){
44
- if(!object(value)||!fields(value,['agent_id','run_id','source'])||value.agent_id!==agentId||value.run_id!==runId||!object(value.source))invalid();
45
- const source=value.source;
46
- if(!fields(source,['id','url','title','text','body_sha256','observed_at','published_at','source_kind','collection_id','document_id','version_id','page'])||source.id!==sourceId||!text(source.id,80,1)||!publicUrl(source.url)||!text(source.title,2048,1)||!text(source.text,16000,1)||!source.text.trim()||source.text.includes('\u0000')||!stamp(source.observed_at)||source.published_at!==null&&!text(source.published_at,100,1)||!['indexed','web_page'].includes(source.source_kind)||!pageEvidence({...source,end:Array.from(source.text).length})||typeof source.body_sha256!=='string'||!/^[0-9a-f]{64}$/.test(source.body_sha256))invalid();
47
- for(const key of ['collection_id','document_id','version_id'])if(source.source_kind==='indexed'?!text(source[key],200,1):source[key]!==null)invalid();
48
- if(source.source_kind==='web_page'&&source.page.end-source.page.start!==Array.from(source.text).length)invalid();
49
- if(Buffer.byteLength(JSON.stringify(value))>131072||createHash('sha256').update(source.text,'utf8').digest('hex')!==source.body_sha256)invalid();
50
- return value;
51
- }
52
- export function validateAgentDescriptor(value){
53
- if(!object(value)||!fields(value,['customer','goal','collection_ids','schedule_minutes','research_policy'])||!object(value.customer)||!fields(value.customer,['name','website','context'])||!text(value.customer.name,200,1)||!value.customer.name.trim()||!publicUrl(value.customer.website)||!text(value.customer.context??'',4000)||!text(value.goal,4000,1)||!value.goal.trim()||value.schedule_minutes!==undefined&&value.schedule_minutes!==0||!Array.isArray(value.collection_ids??[])||(value.collection_ids??[]).length>8||!(value.collection_ids??[]).every(v=>text(v,200,1))||new Set(value.collection_ids??[]).size!==(value.collection_ids??[]).length)throw new Error('Agent JSON must contain a customer name and public website, a goal, optional context and up to eight unique collection_ids; schedule_minutes must be 0.');
54
- if(Object.hasOwn(value,'research_policy')&&!['completion_v1','completion_v2','completion_v3'].includes(value.research_policy))throw new Error('research_policy must be completion_v1, completion_v2 or completion_v3 when explicitly supplied.');
55
- const result={customer:{name:value.customer.name.trim(),website:canonicalWebsite(value.customer.website),context:value.customer.context??''},goal:value.goal.trim(),collection_ids:value.collection_ids??[],schedule_minutes:0,...(Object.hasOwn(value,'research_policy')?{research_policy:value.research_policy}:{})};if(Buffer.byteLength(JSON.stringify(result))>32768)throw new Error('The research brief exceeds 32 KiB.');return result;
56
- }
57
- export function validateAgentRun(value,agentId=value?.agent_id,runId=value?.id){
58
- if(!object(value)||!fields(value,['id','agent_id','agent_revision','status','phase','created_at','started_at','finished_at','outcome','findings','gaps','usage','error'])||!text(value.id,200,1)||value.id!==runId||!text(value.agent_id,200,1)||value.agent_id!==agentId||!integer(value.agent_revision,1)||!['queued','running','completed','partial','needs_input','failed','unknown','cancelled'].includes(value.status)||!['queued','researching','synthesizing','presenting','finished'].includes(value.phase)||!stamp(value.created_at)||value.started_at!==null&&!stamp(value.started_at)||value.finished_at!==null&&!stamp(value.finished_at)||!['findings','no_supported_findings','needs_input',null].includes(value.outcome)||!Array.isArray(value.findings)||value.findings.length>5||!Array.isArray(value.gaps)||value.gaps.length>5||!value.gaps.every(v=>text(v,500,1))||!object(value.usage)||Buffer.byteLength(JSON.stringify(value.usage))>16384||value.error!==null&&(!object(value.error)||!text(value.error.code,100,1)||!text(value.error.message,1000,1)))invalid();
59
- const seen=new Set();for(const finding of value.findings){if(!object(finding)||!fields(finding,['id','claim','relevance','qualifiers','citations'])||!text(finding.id,200,1)||seen.has(finding.id)||!text(finding.claim,1500,1)||!text(finding.relevance,1500,1)||!Array.isArray(finding.qualifiers)||finding.qualifiers.length>5||!finding.qualifiers.every(v=>text(v,300,1))||!Array.isArray(finding.citations)||finding.citations.length<1||finding.citations.length>3)invalid();seen.add(finding.id);
60
- for(const cite of finding.citations){if(!object(cite)||!fields(cite,['id','url','title','quote','start','end','body_sha256','observed_at','published_at','source_kind','collection_id','document_id','version_id','page'])||!text(cite.id,80,1)||!publicUrl(cite.url)||!text(cite.title,2048)||!text(cite.quote,1200,1)||!integer(cite.start,0,16000)||!integer(cite.end,1,16000)||cite.end-cite.start!==Array.from(cite.quote).length||typeof cite.body_sha256!=='string'||!/^[0-9a-f]{64}$/.test(cite.body_sha256)||!stamp(cite.observed_at)||cite.published_at!==null&&!text(cite.published_at,100,1)||!['web_search','indexed','web_page'].includes(cite.source_kind)||!pageEvidence(cite))invalid();for(const key of ['collection_id','document_id','version_id'])if(cite.source_kind==='indexed'?!text(cite[key],200,1):cite[key]!==null)invalid();}
61
- }
62
- if((value.outcome==='findings')!==Boolean(value.findings.length)||ACTIVE.has(value.status)&&(value.phase==='finished'||value.outcome!==null||value.findings.length||value.finished_at!==null)||!ACTIVE.has(value.status)&&value.phase!=='finished'||Buffer.byteLength(JSON.stringify({findings:value.findings,gaps:value.gaps}))>32768)invalid();return value;
63
- }
64
- export function validateResearchAgent(value,id=value?.id){if(!object(value)||!fields(value,['id','customer','goal','collection_ids','revision','status','schedule_minutes','created_at','updated_at','latest_run'])||!text(value.id,200,1)||value.id!==id||!integer(value.revision,1)||!['active','paused'].includes(value.status)||![0,1440].includes(value.schedule_minutes)||!stamp(value.created_at)||!stamp(value.updated_at)||!text(value.customer?.context,4000))invalid();try{validateAgentDescriptor({customer:value.customer,goal:value.goal,collection_ids:value.collection_ids,schedule_minutes:0})}catch{invalid()}if(value.latest_run!==null)validateAgentRun(value.latest_run,value.id);return value;}
65
- export function validateAgentSchedule(value,agentId){
66
- if(!object(value)||!fields(value,['agent_id','revision','enabled','schedule_minutes','next_scheduled_at','blocked_reason','blocked_run_id','deferred_reason'])||value.agent_id!==agentId||!integer(value.revision)||typeof value.enabled!=='boolean'||value.schedule_minutes!==(value.enabled?1440:0)||!['unknown','failed','needs_input','cancelled','access_changed','agent_paused',null].includes(value.blocked_reason)||!['capacity','daily_budget','storage','run_active','history_capacity',null].includes(value.deferred_reason)||value.blocked_run_id!==null&&!text(value.blocked_run_id,200,1)||value.next_scheduled_at!==null&&!stamp(value.next_scheduled_at))invalid();
67
- if((!value.enabled||value.blocked_reason!==null)&&value.deferred_reason!==null||!value.enabled&&(value.next_scheduled_at!==null||value.blocked_reason!==null||value.blocked_run_id!==null)||value.enabled&&(value.revision===0||(value.blocked_reason===null)!==(value.next_scheduled_at!==null))||value.blocked_reason===null&&value.blocked_run_id!==null)invalid();
68
- return value;
69
- }
70
- export function validateAgentRepeats(value,agentId,runId){
71
- if(!object(value)||!fields(value,['agent_id','run_id','status','items'])||value.agent_id!==agentId||value.run_id!==runId||!['available','not_assessed'].includes(value.status)||!Array.isArray(value.items)||value.items.length>5||value.status==='not_assessed'&&value.items.length)invalid();
72
- const seen=new Set();for(const item of value.items){
73
- if(!object(item)||!fields(item,['finding_id','classification','first_seen_run_id','first_seen_finding_id'])||!text(item.finding_id,200,1)||seen.has(item.finding_id)||!text(item.first_seen_run_id,200,1)||!text(item.first_seen_finding_id,200,1)||!['first_seen','exact_repeat'].includes(item.classification))invalid();
74
- const same=item.first_seen_run_id===runId&&item.first_seen_finding_id===item.finding_id;if((item.classification==='first_seen')!==same||item.classification==='exact_repeat'&&item.first_seen_run_id===runId)invalid();seen.add(item.finding_id);
75
- }
76
- return value;
77
- }
78
- export function validateAgentReceipt(value,id){if(!object(value)||!fields(value,['agent','run','replayed'])||typeof value.replayed!=='boolean')invalid();const agent=validateResearchAgent(value.agent,id??value.agent?.id),run=validateAgentRun(value.run,agent.id);if(agent.revision!==run.agent_revision||agent.latest_run?.id!==run.id)invalid();return value;}
79
- function key(value){if(typeof value!=='string'||!/^[A-Za-z0-9_-]{16,128}$/.test(value))throw new Error('Retain a random 16–128 character --request-key for this exact research request.');return value;}
80
- export function agentRequest(rest,options){
81
- if(rest[0]==='agent'&&rest[1]==='feedback'){
82
- const [, ,id,runId,findingId,action='show',...extra]=rest;
83
- identifier(id,'agent');identifier(runId,'run');identifier(findingId,'finding');
84
- if(extra.length||!['show','set','delete'].includes(action))throw new Error('Use research agent feedback AGENT_ID RUN_ID FINDING_ID [show|set|delete].');
85
- allowedOptions(options,action==='show'?[]:action==='set'?['revision','rating','note']:['revision'],'research agent feedback');
86
- const route=`${ROOT}/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/findings/${encodeURIComponent(findingId)}/feedback`,base={command:'feedback',id,runId,findingId,action,route};
87
- if(action==='show')return base;
88
- const revision=decimal(options.revision,'revision',0,Number.MAX_SAFE_INTEGER);
89
- if(action==='delete')return {...base,revision,method:'DELETE',route:route+`?expected_revision=${revision}`};
90
- if(!feedbackRatings.includes(options.rating)||!feedbackNote(options.note??null))throw new Error('Use --rating useful|irrelevant|incorrect|outdated and an optional note of at most 1000 Unicode characters.');
91
- return {...base,revision,method:'PUT',body:{expected_revision:revision,rating:options.rating,note:options.note??null}};
92
- }
93
- if(rest[0]==='agent'&&rest[1]==='source'){
94
- allowedOptions(options,[],'research agent source');if(rest.length!==5)throw new Error('Use research agent source AGENT_ID RUN_ID SOURCE_ID.');
95
- const id=identifier(rest[2],'agent'),runId=identifier(rest[3],'run'),sourceId=identifier(rest[4],'source');return {command:'source',id,runId,sourceId,route:`${ROOT}/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/sources/${encodeURIComponent(sourceId)}`};
96
- }
97
- if(rest[0]==='agent'&&rest[1]==='runs'){
98
- allowedOptions(options,['limit','cursor'],'research agent runs');if(rest.length!==3)throw new Error('Use research agent runs AGENT_ID [--limit N] [--cursor CURSOR].');
99
- const id=identifier(rest[2],'agent'),limit=decimal(options.limit,'limit',1,100,25),query=new URLSearchParams({limit});
100
- if(options.cursor!==undefined){if(typeof options.cursor!=='string'||!/^[\x21-\x7e]{1,512}$/.test(options.cursor))throw new Error('Use the returned opaque research history cursor.');query.set('cursor',options.cursor)}
101
- return {command:'runs',id,limit,cursor:options.cursor,route:`${ROOT}/${encodeURIComponent(id)}/runs?${query}`};
102
- }
103
- if(rest[0]==='agent'&&rest[1]==='schedule'){
104
- const [, ,id,action='show',...extra]=rest;identifier(id,'agent');if(extra.length||!['show','daily','pause','resume'].includes(action))throw new Error('Use research agent schedule AGENT_ID [show|daily|pause|resume].');allowedOptions(options,action==='show'?[]:['revision'],'research agent schedule');
105
- return {command:'schedule',id,route:`${ROOT}/${encodeURIComponent(id)}/schedule`,...(action==='show'?{}:{method:'PATCH',body:{expected_revision:decimal(options.revision,'revision',0,Number.MAX_SAFE_INTEGER),enabled:action!=='pause'}})};
106
- }
107
- if(rest[0]==='agent'&&rest[1]==='repeats'){
108
- allowedOptions(options,[],'research agent repeats');if(rest.length!==4)throw new Error('Use research agent repeats AGENT_ID RUN_ID.');const id=identifier(rest[2],'agent'),runId=identifier(rest[3],'run');return {command:'repeats',id,runId,route:`${ROOT}/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/repeats`};
109
- }
110
- if(rest[0]==='agent-run'){
111
- allowedOptions(options,['wait','timeout'],'research agent-run');if(rest.length!==3)throw new Error('Use research agent-run AGENT_ID RUN_ID.');
112
- const id=identifier(rest[1],'agent'),runId=identifier(rest[2],'run');if(options.timeout!==undefined&&!options.wait)throw new Error('--timeout requires --wait.');
113
- return {command:'agent-run',id,runId,route:`${ROOT}/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}`,wait:!!options.wait,timeout:decimal(options.timeout,'timeout',1,3600,300)};
114
- }
115
- const [,command,id,...extra]=rest,allowed={create:['file','request-key','wait','timeout'],list:['limit','cursor'],show:[],run:['revision','request-key','wait','timeout','research-policy'],pause:['revision'],resume:['revision'],receipt:['operation','request-key']};
116
- if(!Object.hasOwn(allowed,command))throw new Error('Choose research agent create, list, show, run, pause, resume or receipt.');allowedOptions(options,allowed[command],'research agent');
117
- if(extra.length||['create','list','receipt'].includes(command)&&id!==undefined)throw new Error('Unexpected research agent argument.');
118
- if(options.timeout!==undefined&&!options.wait)throw new Error('--timeout requires --wait.');
119
- const base={command,wait:!!options.wait,timeout:decimal(options.timeout,'timeout',1,3600,300)};
120
- if(command==='create'){if(!text(options.file,4096,1))throw new Error('Provide --file with the research brief JSON.');return {...base,route:ROOT,method:'POST',file:options.file,key:key(options['request-key'])};}
121
- if(command==='list'){const limit=decimal(options.limit,'limit',1,100,25),q=new URLSearchParams({limit});if(options.cursor!==undefined){if(!text(options.cursor,512,1))throw new Error('Use the returned bounded cursor.');q.set('cursor',options.cursor)}return {...base,limit,route:ROOT+'?'+q};}
122
- if(command==='receipt'){if(options.operation!=='create'&&!/^run:ragent_[0-9a-f]{32}$/.test(options.operation??''))throw new Error('--operation must be create or run:AGENT_ID.');return {...base,route:ROOT+'/receipts/by-key?operation='+encodeURIComponent(options.operation),key:key(options['request-key']),id:options.operation==='create'?undefined:options.operation.slice(4)};}
123
- identifier(id,'agent');const route=ROOT+'/'+encodeURIComponent(id);
124
- if(command==='show')return {...base,id,route};
125
- const revision=decimal(options.revision,'revision',1,Number.MAX_SAFE_INTEGER);
126
- if(command==='run'){if(Object.hasOwn(options,'research-policy')&&!['completion_v1','completion_v2','completion_v3'].includes(options['research-policy']))throw new Error('--research-policy must be completion_v1, completion_v2 or completion_v3 when explicitly supplied.');return {...base,id,route:route+'/runs',method:'POST',body:{expected_revision:revision,...(Object.hasOwn(options,'research-policy')?{research_policy:options['research-policy']}:{})},key:key(options['request-key'])};}
127
- return {...base,id,route,method:'PATCH',body:{expected_revision:revision,status:command==='pause'?'paused':'active'}};
128
- }
129
- async function readDescriptor(file){const handle=await fs.open(file,'r');try{const size=(await handle.stat()).size;if(!integer(size,1,65536))throw new Error('Research brief JSON must be between 1 byte and 64 KiB.');const bytes=Buffer.alloc(size+1),read=await handle.read(bytes,0,size+1,0);if(read.bytesRead!==size)throw new Error('Research brief changed while reading.');let value;try{value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes.subarray(0,size)))}catch{throw new Error('Invalid research brief JSON. Supply valid UTF-8 JSON without extra fields.');}return validateAgentDescriptor(value)}finally{await handle.close()}}
130
- export async function waitAgentRun(client,initial,{timeout,signal,now=()=>performance.now(),sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms))}={}){let run=validateAgentRun(initial),failures=0;const started=now();let deadline=started+(timeout??([2,3].includes(readResearchCompletion(run).completion?.version??0)?540:300))*1000;while(ACTIVE.has(run.status)){if(signal?.aborted)return {run,wait:{outcome:'interrupted'}};if(now()>=deadline)return {run,wait:{outcome:'timeout'}};try{run=validateAgentRun(await client.request(`${ROOT}/${encodeURIComponent(run.agent_id)}/runs/${encodeURIComponent(run.id)}`,{signal,timeout:Math.max(1,Math.min(30000,Math.ceil(deadline-now()))),maxResponseBytes:262144}),run.agent_id,run.id);failures=0}catch(error){if(!signal?.aborted&&now()<deadline&&transient(error)){await settle(++failures,{signal,now,sleep,deadline});continue;}const outcome=signal?.aborted?'interrupted':now()>=deadline?'timeout':'unavailable',failure=outcome==='unavailable'?confirmedReadFailure(error,{command:'agent-run'}):null;return {run,wait:{outcome,...(failure?{error:failure}:{})}}}if(timeout===undefined&&[2,3].includes(readResearchCompletion(run).completion?.version??0))deadline=started+540000;if(ACTIVE.has(run.status)){const next=Math.min(deadline,now()+2000);while(!signal?.aborted&&now()<next)await sleep(Math.min(100,next-now()));}}return {run,wait:{outcome:run.status}};}
131
- function renderRun(run,stdout){stdout(`Run: ${displayLine(run.id)}\nAgent: ${displayLine(run.agent_id)}\nStatus: ${displayLine(run.status)}\n`);const answer=readResearchAnswerView(run);if(answer.state==='valid'&&answer.sections.some(s=>s.assertions.length)){stdout('\nConcise answer\n');for(const section of answer.sections){stdout('\n'+displayText(section.component.text)+'\n');for(const assertion of section.assertions)stdout(displayText(assertion.text)+'\n');if(section.notice)stdout(displayText(section.notice)+'\n');const evidenceIds=section.assertions.length?[...new Set(section.assertions.flatMap(a=>a.fact_refs.flatMap(r=>run.findings.find(f=>f.id===r.finding_id)?.citations.map(c=>c.id)??[])))]:section.component.evidence_ids;if(evidenceIds.length)stdout('Evidence: '+evidenceIds.map(displayLine).join(', ')+'\n');}stdout('\nExact findings and qualifications\n');}for(const finding of run.findings){stdout(`\n${displayLine(finding.id)}\nObserved statement: ${displayText(finding.claim)}\n`);if(finding.relevance!=='Selected for this research brief.')stdout(`Relevance (inference): ${displayText(finding.relevance)}\n`);for(const qualifier of finding.qualifiers)stdout('Qualification: '+displayText(qualifier)+'\n');for(const cite of finding.citations)stdout(`[${displayLine(cite.id)}] ${displayLine(cite.title)}\n${displayLine(cite.url)}\nObserved: ${displayLine(cite.observed_at)}; source positions ${cite.start}–${cite.end}\n${displayText(cite.quote)}\n`);}for(const gap of [...run.gaps,...readResearchAnswerContract(run).notices.map(notice=>notice.text),...readResearchCompletion(run).notices.map(notice=>notice.text)])stdout('Gap: '+displayText(gap)+'\n');const presentation=readResearchPresentation(run);if(presentation&&!['reviewed','skipped_no_eligible'].includes(presentation.status))stdout('Summary: A concise presentation was not completed for every part. The saved findings and gaps remain available.\n');if(run.error)stdout('Run error: '+displayLine(run.error.message)+'\n');}
132
- export async function agentCommand(rest,options,client,{stdout=value=>process.stdout.write(value),stderr=value=>process.stderr.write(value)}={}){
133
- const request=agentRequest(rest,options);if(request.file)request.body=await readDescriptor(request.file);
134
- let value;try{value=await client.request(request.route,{method:request.method,body:request.body,idempotencyKey:request.key,maxResponseBytes:request.command==='source'?131072:request.command==='runs'?1048576:4*1024*1024});
135
- if(request.command==='runs'){
136
- if(!object(value)||!fields(value,['items','next_cursor','has_more'])||!Array.isArray(value.items)||value.items.length>request.limit||typeof value.next_cursor!=='string'||!/^[\x21-\x7e]{1,512}$/.test(value.next_cursor)||typeof value.has_more!=='boolean'||value.has_more&&!value.items.length||request.cursor!==undefined&&(value.items.length?value.next_cursor===request.cursor:value.next_cursor!==request.cursor)||Buffer.byteLength(JSON.stringify(value))>1048576)invalid();
137
- const seen=new Set();for(const item of value.items){const run=validateAgentRun(item,request.id);if(seen.has(run.id))invalid();seen.add(run.id)}
138
- }
139
- else if(request.command==='list'){if(!object(value)||!Array.isArray(value.items)||value.items.length>request.limit||value.next_cursor!==null&&!text(value.next_cursor,512,1))invalid();value.items.forEach(item=>validateResearchAgent(item));}
140
- else if(['create','run','receipt'].includes(request.command)){validateAgentReceipt(value,request.id);if(request.command==='create'&&(value.agent.revision!==1||value.agent.schedule_minutes!==0||value.agent.goal!==request.body.goal||['name','website','context'].some(k=>value.agent.customer[k]!==request.body.customer[k])||JSON.stringify(value.agent.collection_ids)!==JSON.stringify(request.body.collection_ids)))invalid();if(request.command==='run'&&value.run.agent_revision!==request.body.expected_revision)invalid();}
141
- else if(request.command==='schedule'){validateAgentSchedule(value,request.id);if(request.body&&(value.enabled!==request.body.enabled||![request.body.expected_revision,request.body.expected_revision+1].includes(value.revision)))invalid();}
142
- else if(request.command==='feedback'){
143
- validateAgentFeedback(value,request.id,request.runId,request.findingId);const f=value.feedback;
144
- if(request.action!=='show'&&![request.revision,request.revision+1].includes(f.revision))invalid();
145
- if(request.action==='set'&&(f.state!=='active'||f.rating!==request.body.rating||f.note!==request.body.note)||request.action==='delete'&&f.state==='active')invalid();
146
- }
147
- else if(request.command==='source')validateAgentSource(value,request.id,request.runId,request.sourceId);
148
- else if(request.command==='repeats')validateAgentRepeats(value,request.id,request.runId);
149
- else if(request.command==='agent-run')validateAgentRun(value,request.id,request.runId);
150
- else {validateResearchAgent(value,request.id);if(['pause','resume'].includes(request.command)&&(value.status!==request.body.status||![request.body.expected_revision,request.body.expected_revision+1].includes(value.revision)))invalid();}
151
- }catch(error){const status=Number.isInteger(error?.status)?error.status:undefined,message=confirmedReadFailure(error,request)?.message??(request.command==='feedback'?'Feedback was not confirmed. Read the same finding feedback and reconcile its revision before another write.':request.command==='source'?'Saved source context could not be read. Do not substitute a current webpage for this saved observation.':request.command==='schedule'?'Schedule request was not confirmed. Read the same schedule and reconcile its setting and revision before another write.'+(status===409&&error.detail?' Server: '+error.detail:''):'Research request was not confirmed. Read the same agent/run or recover the original admission key; never start a duplicate blindly.');const result={error:{code:status?`http_${status}`:'request_unconfirmed',message,...(request.key?{request_key:request.key}:{})}};if(options.json)stdout(JSON.stringify(result,null,2)+'\n');else stderr(message+(request.key?'\nOriginal request key: '+request.key:'')+'\n');return 1;}
152
- if(request.command==='feedback'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else stdout(`Feedback: ${displayLine(value.feedback.state)}\nRevision: ${value.feedback.revision}\nRating: ${displayLine(value.feedback.rating??'none')}\n${value.feedback.note===null?'':displayText(value.feedback.note)+'\n'}Customer feedback is not independent factual proof.\n`);return 0;}
153
- if(request.command==='source'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else stdout(`Saved source context: ${displayLine(value.source.id)}\n${displayLine(value.source.title)}\n${displayLine(value.source.url)}\nObserved: ${displayLine(value.source.observed_at)}\nThis is the admitted evidence window, which may be part of a page.\n\n${displayText(value.source.text)}\n`);return 0;}
154
- if(request.command==='runs'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else{for(const run of value.items)stdout(`${displayLine(run.id)}\t${displayLine(run.status)}\t${displayLine(run.created_at)}\n`);stderr(`Next cursor: ${displayLine(value.next_cursor)}\nMore admitted runs: ${value.has_more?'yes':'no'}\nRetain pending run IDs and read each exact run until terminal.\n`);}return 0;}
155
- if(request.command==='schedule'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else stdout(`Agent: ${displayLine(value.agent_id)}\nSchedule revision: ${value.revision}\nDaily research: ${value.enabled?'enabled':'disabled'}\nNext scheduled: ${displayLine(value.next_scheduled_at??'none')}\nBlocked: ${displayLine(value.blocked_reason??'no')}\nDeferred: ${displayLine(value.deferred_reason??'no')}\n`);return 0;}
156
- if(request.command==='repeats'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else{stdout(`Run: ${displayLine(value.run_id)}\nExact-repeat assessment: ${displayLine(value.status)}\n`);for(const item of value.items)stdout(`${displayLine(item.finding_id)}\t${displayLine(item.classification)}\tfirst seen ${displayLine(item.first_seen_run_id)} / ${displayLine(item.first_seen_finding_id)}\n`);stdout('Exact repetition is not a judgment of business novelty; all saved findings remain available.\n');}return 0;}
157
- if(request.command==='list'){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else{for(const agent of value.items)stdout(`${displayLine(agent.id)}\tr${agent.revision}\t${displayLine(agent.status)}\t${displayLine(agent.customer.name)}\n`);if(value.next_cursor)stderr('Next cursor: '+displayLine(value.next_cursor)+'\n');}return 0;}
158
- if(['show','pause','resume'].includes(request.command)){if(options.json)stdout(JSON.stringify(value,null,2)+'\n');else{stdout(`Agent: ${displayLine(value.id)}\nCustomer: ${displayLine(value.customer.name)}\nGoal: ${displayText(value.goal)}\nRevision: ${value.revision}\nStatus: ${displayLine(value.status)}\n`);if(value.latest_run)renderRun(value.latest_run,stdout);}return 0;}
159
- let run=request.command==='agent-run'?value:value.run,output=value;
160
- if(request.wait){const waited=await withInterrupt(signal=>waitAgentRun(client,run,{...(options.timeout===undefined?{}:{timeout:request.timeout}),signal}));run=waited.run;output=request.command==='agent-run'?waited:{...value,...waited};}
161
- if(options.json)stdout(JSON.stringify(output,null,2)+'\n');else{renderRun(run,stdout);if(value.replayed)stderr('Recovered the original admission receipt; read its run for current findings.\n');if(request.key)stderr('Original request key: '+request.key+'\n');}
162
- if(output.wait&&!['completed','partial'].includes(output.wait.outcome)){if(output.wait.error&&!options.json)stderr(output.wait.error.message+'\n');stderr('Follow the same agent and run to continue. Waiting does not cancel server work.\n');return 1;}
163
- return request.wait||request.command==='agent-run'?run.status==='completed'?0:run.status==='partial'?2:ACTIVE.has(run.status)&&!request.wait?0:1:0;
164
- }
@@ -1,231 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import {test} from 'node:test';
3
- import fs from 'node:fs/promises';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import http from 'node:http';
7
- import {spawn} from 'node:child_process';
8
- import {fileURLToPath} from 'node:url';
9
- import {researchCommand} from './research.mjs';
10
- import {agentRequest,validateAgentDescriptor,validateAgentRun,waitAgentRun} from './research-agent.mjs';
11
-
12
- const stamp='2026-10-24T08:00:00.000Z',id='ragent_'+'a'.repeat(32),runId='job_'+'b'.repeat(32),key='agent-request-key-0001';
13
- const input={customer:{name:'Atlas',website:'https://atlas.example/',context:''},goal:'Find workflow changes.',collection_ids:[],schedule_minutes:0};
14
- const run=(patch={})=>({id:runId,agent_id:id,agent_revision:1,status:'queued',phase:'queued',created_at:stamp,started_at:null,finished_at:null,outcome:null,findings:[],gaps:[],usage:{},error:null,...patch});
15
- const agent=(patch={})=>({id,...input,revision:1,status:'active',created_at:stamp,updated_at:stamp,latest_run:run(),...patch});
16
- const receipt={agent:agent(),run:run(),replayed:false};
17
- const citation={id:'W1',url:'https://source.example/release',title:'Release',quote:'é🌲 claim',start:3,end:11,body_sha256:'a'.repeat(64),observed_at:stamp,published_at:'October 2026',source_kind:'web_search',collection_id:null,document_id:null,version_id:null};
18
- const completed=()=>run({status:'completed',phase:'finished',started_at:stamp,finished_at:stamp,outcome:'findings',findings:[{id:'finding_one',claim:'The vendor announces a workflow.',relevance:'This may be relevant to Atlas.',qualifiers:['Enterprise only.'],citations:[citation]}]});
19
- async function execute(rest,options,value){let stdout='',stderr='';const calls=[];const code=await researchCommand(rest,options,{request:async(route,args)=>{calls.push({route,args});if(value instanceof Error)throw value;return typeof value==='function'?value(calls.length):value}},{stdout:x=>stdout+=x,stderr:x=>stderr+=x});return {code,stdout,stderr,calls};}
20
-
21
- test('execution status exit code is independent of findings and preserves legacy or new coverage diagnostics',async()=>{
22
- for(const [status,usage,code] of [['partial',{},2],['completed',{evidence_limits:{version:1,reasons:[]}},0],['partial',{evidence_limits:{version:1,reasons:['finding_omitted']}},2]]){
23
- const value={...completed(),status,outcome:'no_supported_findings',findings:[],gaps:['This bounded run is not an exhaustive survey.'],usage};
24
- const json=await execute(['agent-run',id,runId],{json:true},value),display=await execute(['agent-run',id,runId],{},value);
25
- assert.equal(json.code,code);assert.deepEqual(JSON.parse(json.stdout),value);assert.equal(json.calls.length,1);
26
- assert.equal(display.code,code);assert.ok(display.stdout.includes(value.gaps[0]));assert.equal(display.calls.length,1);
27
- }
28
- });
29
-
30
- test('five findings preserve text order, own sources and full JSON; six and oversized packets emit no findings',async()=>{
31
- const value=completed();value.findings=Array.from({length:5},(_,index)=>({...structuredClone(value.findings[0]),id:`finding_${index}`,claim:`Workflow ${index+1} has a documented export.`,qualifiers:[`Export ${index+1} requires Enterprise approval.`],citations:[{...citation,id:`W${index+1}`,url:`https://source.example/workflow/${index+1}`,quote:`Export ${index+1} is documented.`,start:0,end:23}]}));
32
- const json=await execute(['agent-run',id,runId],{json:true},value),display=await execute(['agent-run',id,runId],{},value);
33
- assert.equal(json.code,0);assert.deepEqual(JSON.parse(json.stdout),value);assert.equal(json.calls.length,1);assert.equal(display.code,0);assert.equal(display.calls.length,1);
34
- let previous=-1;for(const finding of value.findings){const position=display.stdout.indexOf(finding.claim);assert.ok(position>previous);previous=position;for(const text of [...finding.qualifiers,finding.citations[0].url,finding.citations[0].quote])assert.ok(display.stdout.includes(text))}
35
- const six={...value,findings:[...value.findings,{...value.findings[0],id:'finding_six'}]},oversized=structuredClone(value);
36
- for(const finding of oversized.findings)finding.citations=Array.from({length:3},()=>({...citation,quote:'🌲'.repeat(1200),start:0,end:1200}));
37
- assert.ok(Buffer.byteLength(JSON.stringify({findings:oversized.findings,gaps:oversized.gaps}))>32768);
38
- for(const invalid of [six,oversized]){const rejected=await execute(['agent-run',id,runId],{json:true},invalid);assert.equal(rejected.code,1);assert.equal(rejected.calls.length,1);assert.ok(!rejected.stdout.includes(value.findings[0].claim))}
39
- });
40
-
41
- test('saved run JSON and display retain all five full qualifiers without truncation',async()=>{
42
- const value=completed();value.findings[0].qualifiers=Array.from({length:5},(_,i)=>`${i+1}:`+'x'.repeat(298));
43
- const json=await execute(['agent-run',id,runId],{json:true},value);
44
- assert.equal(json.code,0);assert.deepEqual(JSON.parse(json.stdout),value);assert.equal(json.calls.length,1);
45
- const display=await execute(['agent-run',id,runId],{},value);
46
- assert.equal(display.code,0);for(const qualifier of value.findings[0].qualifiers)assert(display.stdout.includes(qualifier));
47
- for(const qualifiers of [[...value.findings[0].qualifiers,'Sixth condition.'],[...value.findings[0].qualifiers.slice(0,4),'x'.repeat(301)]]){
48
- const invalid=structuredClone(value);invalid.findings[0].qualifiers=qualifiers;
49
- assert.throws(()=>validateAgentRun(invalid,id,runId));
50
- }
51
- });
52
-
53
- test('failure diagnostic reads preserve JSON and failed exit without a retry',async()=>{
54
- for(const usage of [{},{failure:{version:1,phase:'review',check:'response_contract'}}]){
55
- const value=run({status:'failed',phase:'finished',started_at:stamp,finished_at:stamp,usage,error:{code:'invalid_output',message:'The research output could not be validated.'}});
56
- const result=await execute(['agent-run',id,runId],{json:true},value);
57
- assert.equal(result.code,1);assert.deepEqual(JSON.parse(result.stdout),value);assert.equal(result.calls.length,1);
58
- assert.throws(()=>validateAgentRun({...value,usage:{padding:'x'.repeat(16385)}},id,runId));
59
- }
60
- });
61
-
62
- test('history reads one page and prints persistent checkpoint including empty tail',async()=>{
63
- const page={items:[run()],next_cursor:'next',has_more:true};
64
- const result=await execute(['agent','runs',id],{limit:'1',cursor:'prior',json:true},page);
65
- assert.equal(result.code,0);assert.deepEqual(JSON.parse(result.stdout),page);assert.equal(result.calls.length,1);
66
- assert.equal(result.calls[0].route,`/api/research/agents/${id}/runs?limit=1&cursor=prior`);assert.equal(result.calls[0].args.method,undefined);assert.equal(result.calls[0].args.maxResponseBytes,1048576);
67
- const empty=await execute(['agent','runs',id],{cursor:'next'},{items:[],next_cursor:'next',has_more:false});
68
- assert.equal(empty.code,0);assert.match(empty.stderr,/Next cursor: next/);assert.match(empty.stderr,/More admitted runs: no/);
69
- for(const options of [{limit:'101'},{cursor:'é'},{cursor:'bad\n'},{wait:true},{'request-key':key}])assert.throws(()=>agentRequest(['agent','runs',id],options));
70
- });
71
- test('history refuses malformed page without emitting partial source text or guessed cursor',async()=>{
72
- const good={items:[completed()],next_cursor:'next',has_more:false};
73
- for(const value of [{...good,extra:true},{...good,next_cursor:null},{...good,has_more:1},{...good,next_cursor:'prior'},
74
- {...good,items:[],has_more:true},{...good,items:[]},{...good,items:[run(),run()]},{...good,items:[run({agent_id:'wrong'})]},
75
- {...good,items:[run({status:['queued']})]}]){
76
- const result=await execute(['agent','runs',id],{cursor:'prior',json:true},value);assert.equal(result.code,1);assert(!result.stdout.includes('Enterprise only.'));assert(!result.stdout.includes('next_cursor'));
77
- }
78
- const error=new Error('PRIVATE_SOURCE');error.status=404;const denied=await execute(['agent','runs',id],{cursor:'prior',json:true},error);
79
- assert.equal(denied.code,1);assert.equal(denied.calls.length,1);assert(!denied.stdout.includes('PRIVATE_SOURCE'));assert(!denied.stdout.includes('next_cursor'));
80
- });
81
- test('history enforces whole-envelope byte bound before rendering',async()=>{
82
- const items=Array.from({length:70},(_,i)=>run({id:'job_'+String(i).padStart(32,'0'),usage:{text:'🌲'.repeat(4000)}}));
83
- const result=await execute(['agent','runs',id],{limit:'100',json:true},{items,next_cursor:'next',has_more:false});assert.equal(result.code,1);assert(!result.stdout.includes('🌲'));
84
- });
85
-
86
- test('agent request parser binds exact routes, offsets and operation keys',()=>{
87
- assert.deepEqual(agentRequest(['agent-run',id,runId],{json:true}).route,`/api/research/agents/${id}/runs/${runId}`);
88
- const rerun=agentRequest(['agent','run',id],{revision:'1','request-key':key});assert.equal(rerun.method,'POST');assert.deepEqual(rerun.body,{expected_revision:1});
89
- assert.equal(agentRequest(['agent','receipt'],{operation:'run:'+id,'request-key':key}).method,undefined);
90
- for(const args of [[['agent','run',id],{'request-key':key}],[['agent-run',id,runId],{timeout:'2'}],[['agent','create'],{file:'brief.json','request-key':key,bypass:true}],[['agent','show',id,'extra'],{}]])assert.throws(()=>agentRequest(...args));
91
- });
92
- test('brief validation normalizes website and rejects oversized or broadened descriptors',()=>{
93
- assert.equal(validateAgentDescriptor({...input,customer:{name:'Atlas',website:'HTTPS://ATLAS.EXAMPLE:443/#section'}}).customer.website,input.customer.website);
94
- for(const patch of [{schedule_minutes:60},{schedule_minutes:true},{collection_ids:['x','x']},{goal:'\ud800'},{extra:'PRIVATE'},{customer:{name:'Atlas',website:'https://user:secret@x.example/'}}])assert.throws(()=>validateAgentDescriptor({...input,...patch}));
95
- });
96
- test('create sends exactly one reviewed descriptor, retains recovery key and no source API',async()=>{
97
- const dir=await fs.mkdtemp(path.join(os.tmpdir(),'opencrawl-agent-')),file=path.join(dir,'brief.json');
98
- try{await fs.writeFile(file,JSON.stringify(input));const value=await execute(['agent','create'],{file,'request-key':key,json:true},receipt);assert.equal(value.code,0);assert.equal(value.calls.length,1);assert.equal(value.calls[0].route,'/api/research/agents');assert.deepEqual(value.calls[0].args.body,input);assert.equal(value.calls[0].args.idempotencyKey,key);assert.equal(JSON.parse(value.stdout).run.status,'queued');}
99
- finally{await fs.rm(dir,{recursive:true,force:true})}
100
- });
101
- test('malformed brief never sends and never echoes private bytes',async()=>{
102
- const dir=await fs.mkdtemp(path.join(os.tmpdir(),'opencrawl-agent-')),file=path.join(dir,'brief.json');
103
- try{await fs.writeFile(file,'{"PRIVATE_SYNTHETIC_SECRET');await assert.rejects(execute(['agent','create'],{file,'request-key':key},receipt),error=>!error.message.includes('PRIVATE_SYNTHETIC_SECRET'));}
104
- finally{await fs.rm(dir,{recursive:true,force:true})}
105
- });
106
- test('unknown run admission never retries and prints only fixed error plus original key',async()=>{
107
- const value=await execute(['agent','run',id],{revision:'1','request-key':key,json:true},new Error('PRIVATE_SYNTHETIC_SECRET'));
108
- assert.equal(value.code,1);assert.equal(value.calls.length,1);assert.equal(JSON.parse(value.stdout).error.request_key,key);assert.ok(!value.stdout.includes('PRIVATE_SYNTHETIC_SECRET'));
109
- });
110
- test('plain findings preserve exact quote, qualifier, inference and source range',async()=>{
111
- const value=await execute(['agent-run',id,runId],{},completed());assert.equal(value.code,0);assert.equal(value.calls.length,1);
112
- for(const expected of ['Observed statement:','Relevance (inference):','Qualification: Enterprise only.','é🌲 claim','source positions 3–11','https://source.example/release'])assert.ok(value.stdout.includes(expected));
113
- });
114
- test('neutral relevance is hidden only in text while saved JSON and historical values remain intact',async()=>{
115
- const value=completed();value.findings[0].relevance='Selected for this research brief.';
116
- value.usage={model_stages:{version:2,synthesis_calls:1,review_calls:1,draft_findings:1,review_decisions:[{ordinal:1,selected_quotes_support_all_claims_and_qualifiers:true,source_context_preserves_restrictions_and_date:true,addresses_requested_goal:true}]}};
117
- const display=await execute(['agent-run',id,runId],{},value);assert.equal(display.code,0);assert.equal(display.calls.length,1);
118
- assert(!display.stdout.includes('Relevance (inference):'));assert(!display.stdout.includes(value.findings[0].relevance));
119
- for(const expected of [value.findings[0].claim,'Qualification: Enterprise only.',citation.quote,citation.url,'source positions 3–11'])assert(display.stdout.includes(expected));
120
- const json=await execute(['agent-run',id,runId],{json:true},value);assert.equal(json.code,0);assert.equal(json.calls.length,1);assert.deepEqual(JSON.parse(json.stdout),value);
121
- for(const relevance of ['This may be relevant to Atlas.','Selected for this research brief. ']){
122
- value.findings[0].relevance=relevance;const historical=await execute(['agent-run',id,runId],{},value);assert.equal(historical.code,0);assert(historical.stdout.includes('Relevance (inference): '+relevance));
123
- }
124
- });
125
- test('invalid scope/Unicode/citation kinds/internal fields never emit source text',async()=>{
126
- const cases=[completed(),completed(),completed(),completed(),completed()];cases[0].id='wrong';cases[1]._source_fences=[];cases[2].findings[0].citations=[{...citation,end:12}];cases[3].findings[0].citations=[{...citation,source_kind:'indexed'}];cases[4].findings[0].claim='\ud800';
127
- for(const value of cases){const result=await execute(['agent-run',id,runId],{json:true},value);assert.equal(result.code,1);assert.ok(!result.stdout.includes('é🌲 claim'));}
128
- });
129
- test('same source may support two exact quotes and hash remains supplied evidence metadata',()=>{
130
- const value=completed();value.findings[0].citations.push({...citation,quote:'claim',start:6,end:11,body_sha256:'f'.repeat(64)});assert.deepEqual(validateAgentRun(value),value);
131
- });
132
- test('read terminal failed, unknown, partial and needs-input remain distinct exit states',async()=>{
133
- for(const [status,code] of [['partial',2],['failed',1],['unknown',1],['cancelled',1],['needs_input',1]]){const value=run({status,phase:'finished',finished_at:stamp,outcome:status==='needs_input'?'needs_input':null});const result=await execute(['agent-run',id,runId],{json:true},value);assert.equal(result.code,code);assert.equal(JSON.parse(result.stdout).status,status);}
134
- });
135
- test('wait uses only exact GET and interrupted wait never cancels or creates work',async()=>{
136
- let clock=0;const calls=[];const result=await waitAgentRun({request:async(route,args)=>{calls.push({route,args});return calls.length===1?run({status:'running',phase:'researching'}):completed()}},run(),{now:()=>clock,sleep:async ms=>{clock+=ms}});
137
- assert.equal(result.wait.outcome,'completed');assert.ok(calls.every(c=>c.route===`/api/research/agents/${id}/runs/${runId}`&&!c.args.method));
138
- const controller=new AbortController();controller.abort();const stopped=await waitAgentRun({request:async()=>assert.fail()},run(),{signal:controller.signal});assert.equal(stopped.wait.outcome,'interrupted');
139
- });
140
- test('list reads one page only and historical receipt stays queued',async()=>{
141
- const page=await execute(['agent','list'],{limit:'1',json:true},{items:[agent()],next_cursor:'next'});assert.equal(page.calls.length,1);assert.equal(JSON.parse(page.stdout).next_cursor,'next');
142
- const recovered=await execute(['agent','receipt'],{operation:'create','request-key':key,json:true},{...receipt,replayed:true});assert.equal(recovered.calls[0].args.method,undefined);assert.equal(JSON.parse(recovered.stdout).run.status,'queued');
143
- });
144
- test('invalid admitted response preserves recovery key and impossible dates never render',async()=>{
145
- const result=await execute(['agent','run',id],{revision:'1','request-key':key,json:true},{agent:'PRIVATE_SYNTHETIC_SECRET'});
146
- assert.equal(result.calls.length,1);assert.equal(result.code,1);assert.equal(JSON.parse(result.stdout).error.request_key,key);assert.ok(!result.stdout.includes('PRIVATE_SYNTHETIC_SECRET'));
147
- assert.throws(()=>validateAgentRun(completedWithDate()));
148
- function completedWithDate(){return {...completed(),created_at:'2026-02-30T08:00:00Z'}}
149
- });
150
- test('status writes reject a changed requested status or unrelated revision',async()=>{
151
- for(const patch of [{revision:8,status:'paused'},{revision:2,status:'active'}]){const result=await execute(['agent','pause',id],{revision:'1',json:true},agent(patch));assert.equal(result.calls.length,1);assert.equal(result.code,1);}
152
- });
153
-
154
- test('actual CLI performs one create and exact keyed GET recovery through the real transport',async()=>{
155
- const folder=await fs.mkdtemp(path.join(os.tmpdir(),'opencrawl-agent-cli-')),calls=[];
156
- const server=http.createServer(async(req,res)=>{let body='';for await(const part of req)body+=part;calls.push({path:req.url,method:req.method,key:req.headers['idempotency-key'],authorization:req.headers.authorization,body:body?JSON.parse(body):null});res.setHeader('Content-Type','application/json');res.end(JSON.stringify(req.url.includes('/runs?')?{items:[completed()],next_cursor:'history-next',has_more:false}:req.url.endsWith('/schedule')?(req.method==='PATCH'?daily():schedule()):req.url.endsWith('/repeats')?repeats():req.url.includes('/runs/')?completed():{...receipt,replayed:req.method==='GET'}));});
157
- await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
158
- const script=fileURLToPath(new URL('../bin/opencrawl.mjs',import.meta.url)),url='http://127.0.0.1:'+server.address().port;
159
- const env={PATH:process.env.PATH,HOME:folder,OPENCRAWL_CONFIG_DIR:folder,OPENCRAWL_URL:url,OPENCRAWL_API_KEY:'synthetic-only-key'};
160
- const invoke=args=>new Promise((resolve,reject)=>{const child=spawn(process.execPath,[script,...args],{cwd:folder,env,stdio:['ignore','pipe','pipe']});let stdout='',stderr='';const timer=setTimeout(()=>child.kill('SIGKILL'),10000);child.stdout.on('data',x=>stdout+=x);child.stderr.on('data',x=>stderr+=x);child.on('error',reject);child.on('close',code=>{clearTimeout(timer);resolve({code,stdout,stderr})});});
161
- try{
162
- const file=path.join(folder,'brief.json');await fs.writeFile(file,JSON.stringify(input));
163
- const created=await invoke(['research','agent','create','--file',file,'--request-key',key,'--json']);assert.equal(created.code,0,created.stdout+created.stderr);assert.equal(JSON.parse(created.stdout).run.id,runId);
164
- const recovered=await invoke(['research','agent','receipt','--operation','create','--request-key',key,'--json']);assert.equal(recovered.code,0,recovered.stdout+recovered.stderr);assert.equal(JSON.parse(recovered.stdout).replayed,true);
165
- const saved=await invoke(['research','agent-run',id,runId,'--json']);assert.equal(saved.code,0,saved.stderr);assert.equal(JSON.parse(saved.stdout).status,'completed');
166
- const scheduleRead=await invoke(['research','agent','schedule',id,'--json']);assert.equal(scheduleRead.code,0,scheduleRead.stdout+scheduleRead.stderr);assert.deepEqual(JSON.parse(scheduleRead.stdout),schedule());
167
- const scheduled=await invoke(['research','agent','schedule',id,'daily','--revision','0','--json']);assert.equal(scheduled.code,0,scheduled.stdout+scheduled.stderr);assert.deepEqual(JSON.parse(scheduled.stdout),daily());
168
- const repeated=await invoke(['research','agent','repeats',id,runId,'--json']);assert.equal(repeated.code,0,repeated.stdout+repeated.stderr);assert.deepEqual(JSON.parse(repeated.stdout),repeats());
169
- const history=await invoke(['research','agent','runs',id,'--limit','1','--cursor','history-prior','--json']);assert.equal(history.code,0,history.stdout+history.stderr);assert.deepEqual(JSON.parse(history.stdout),{items:[completed()],next_cursor:'history-next',has_more:false});
170
- assert.deepEqual(calls.map(c=>[c.method,c.path]),[['POST','/api/research/agents'],['GET','/api/research/agents/receipts/by-key?operation=create'],['GET',`/api/research/agents/${id}/runs/${runId}`],['GET',`/api/research/agents/${id}/schedule`],['PATCH',`/api/research/agents/${id}/schedule`],['GET',`/api/research/agents/${id}/runs/${runId}/repeats`],['GET',`/api/research/agents/${id}/runs?limit=1&cursor=history-prior`]]);
171
- assert.deepEqual(calls[4].body,{expected_revision:0,enabled:true});assert.equal(calls[4].key,undefined);
172
- assert.deepEqual(calls[0].body,input);assert.ok(calls.every(c=>c.authorization==='Bearer synthetic-only-key'));assert.equal(calls[0].key,key);assert.equal(calls[1].key,key);assert.equal(calls[2].key,undefined);
173
- const invalid=await invoke(['research','agent','run',id,'--revision','1','--request-key',key,'--request-key','duplicate']);assert.equal(invalid.code,1);assert.equal(calls.length,7);
174
- }finally{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));await fs.rm(folder,{recursive:true,force:true});}
175
- });
176
- const page=patch=>({requested_url:'https://source.example/old-release',final_url:citation.url,body_sha256:'f'.repeat(64),start:100,end:116,total_chars:500,extractor:'public-static-v3',...patch});
177
- test('page evidence JSON preserves exact window/quote metadata without another fetch',async()=>{
178
- const value=completed();value.findings[0].citations=[{...citation,source_kind:'web_page',page:page()}];
179
- const result=await execute(['agent-run',id,runId],{json:true},value);assert.equal(result.code,0);assert.equal(result.calls.length,1);assert.deepEqual(JSON.parse(result.stdout),value);
180
- const c=JSON.parse(result.stdout).findings[0].citations[0];assert.equal(c.page.start+c.start,103);assert.notEqual(c.body_sha256,c.page.body_sha256);
181
- });
182
- test('invalid page metadata or wrong source kind never renders the quote',async()=>{
183
- const bad=[null,{},page({extra:true}),page({requested_url:'https://u:p@source.example/'}),page({final_url:'https://other.example/'}),page({body_sha256:'F'.repeat(64)}),page({start:-1}),page({start:true}),page({start:1.5}),page({end:100}),page({end:110}),page({end:501}),page({total_chars:131073}),page({total_chars:true}),page({start:0,end:16001,total_chars:16001}),page({extractor:'unknown'})];
184
- for(const metadata of bad){const value=completed();value.findings[0].citations=[{...citation,source_kind:'web_page',page:metadata}];const result=await execute(['agent-run',id,runId],{json:true},value);assert.equal(result.code,1);assert(!result.stdout.includes(citation.quote));}
185
- for(const extra of [{source_kind:'web_page'},{source_kind:'web_page',page:page(),version_id:'rver_foreign'},{source_kind:'web_search',page:null},{source_kind:'indexed',collection_id:'rcol_one',document_id:'doc_one',version_id:'rver_one',page:page()}]){const value=completed();value.findings[0].citations=[{...citation,...extra}];assert.throws(()=>validateAgentRun(value));}
186
- });
187
- test('legacy source kinds stay readable without page metadata',()=>{
188
- for(const c of [citation,{...citation,source_kind:'indexed',collection_id:'rcol_one',document_id:'doc_one',version_id:'rver_one'}]){const value=completed();value.findings[0].citations=[c];assert.deepEqual(validateAgentRun(value),value);}
189
- });
190
-
191
- const schedule=(patch={})=>({agent_id:id,revision:0,enabled:false,schedule_minutes:0,next_scheduled_at:null,blocked_reason:null,blocked_run_id:null,deferred_reason:null,...patch});
192
- const daily=(patch={})=>schedule({revision:1,enabled:true,schedule_minutes:1440,next_scheduled_at:stamp,...patch});
193
- const repeats=(patch={})=>({agent_id:id,run_id:runId,status:'available',items:[{finding_id:'finding_current',classification:'first_seen',first_seen_run_id:runId,first_seen_finding_id:'finding_current'}],...patch});
194
- test('schedule command is explicit, revision-bound and separate from agent pause or run',()=>{
195
- assert.deepEqual(agentRequest(['agent','schedule',id],{}),{command:'schedule',id,route:`/api/research/agents/${id}/schedule`});
196
- for(const [action,enabled] of [['daily',true],['pause',false],['resume',true]]){const request=agentRequest(['agent','schedule',id,action],{revision:'0',json:true});assert.equal(request.method,'PATCH');assert.deepEqual(request.body,{expected_revision:0,enabled});}
197
- for(const [args,options] of [[['agent','schedule',id,'daily'],{}],[['agent','schedule',id,'show'],{revision:'0'}],[['agent','schedule',id,'resume'],{revision:'-1'}],[['agent','schedule',id,'daily'],{revision:'1','request-key':key}],[['agent','schedule',id,'hourly'],{}],[['agent','repeats',id,runId,'extra'],{}]])assert.throws(()=>agentRequest(args,options));
198
- });
199
- test('daily and blocked schedules render honestly; mixed agent lists and historical reads work',async()=>{
200
- const updated=await execute(['agent','schedule',id,'daily'],{revision:'0',json:true},daily());assert.equal(updated.code,0);assert.deepEqual(updated.calls[0].args.body,{expected_revision:0,enabled:true});assert.equal(updated.calls.length,1);
201
- const blocked=await execute(['agent','schedule',id],{},daily({next_scheduled_at:null,blocked_reason:'unknown',blocked_run_id:runId}));assert.equal(blocked.code,0);assert.match(blocked.stdout,/Daily research: enabled/);assert.match(blocked.stdout,/Blocked: unknown/);
202
- const deferred=await execute(['agent','schedule',id],{},daily({deferred_reason:'daily_budget'}));assert.match(deferred.stdout,/Deferred: daily_budget/);
203
- const mixed=await execute(['agent','list'],{json:true},{items:[agent(),agent({id:'ragent_daily',schedule_minutes:1440,latest_run:null})],next_cursor:null});assert.equal(mixed.code,0);
204
- assert.equal((await execute(['agent','receipt'],{operation:'create','request-key':key,json:true},{...receipt,replayed:true})).code,0);
205
- assert.throws(()=>validateAgentDescriptor({...input,schedule_minutes:1440}));
206
- });
207
- test('schedule failures never retry, substitute a revision or expose invalid response',async()=>{
208
- for(const value of [new Error('PRIVATE_SYNTHETIC_SECRET'),daily({agent_id:'other'}),daily({revision:9}),daily({schedule_minutes:0}),daily({next_scheduled_at:null}),daily({blocked_reason:'unexpected'}),daily({extra:'PRIVATE_SYNTHETIC_SECRET'}),daily({deferred_reason:'invented'}),schedule({deferred_reason:'daily_budget'})]){const result=await execute(['agent','schedule',id,'daily'],{revision:'0',json:true},value);assert.equal(result.code,1);assert.equal(result.calls.length,1);assert(!result.stdout.includes('PRIVATE_SYNTHETIC_SECRET'));}
209
- });
210
- test('repeat command reads once, distinguishes not assessed and never removes saved findings',async()=>{
211
- const value=repeats();const result=await execute(['agent','repeats',id,runId],{json:true},value);assert.equal(result.code,0);assert.deepEqual(JSON.parse(result.stdout),value);assert.equal(result.calls.length,1);assert.equal(result.calls[0].route,`/api/research/agents/${id}/runs/${runId}/repeats`);assert.equal(result.calls[0].args.method,undefined);
212
- const unassessed=await execute(['agent','repeats',id,runId],{},repeats({status:'not_assessed',items:[]}));assert.match(unassessed.stdout,/not_assessed/);assert.match(unassessed.stdout,/not a judgment of business novelty/);
213
- const saved=await execute(['agent-run',id,runId],{json:true},completed());assert.deepEqual(JSON.parse(saved.stdout),completed());
214
- });
215
- test('repeat response rejects foreign, duplicate and contradictory first-seen references',async()=>{
216
- for(const value of [repeats({status:['available']}),repeats({items:[{...repeats().items[0],classification:['exact_repeat'],first_seen_run_id:'foreign'}]}),repeats({run_id:'foreign'}),repeats({status:'not_assessed'}),repeats({items:[...repeats().items,...repeats().items]}),repeats({items:[{...repeats().items[0],classification:'exact_repeat'}]}),repeats({items:[{...repeats().items[0],classification:'exact_repeat',first_seen_finding_id:'finding_other'}]}),repeats({items:[{...repeats().items[0],first_seen_run_id:'foreign'}]}),repeats({extra:true})]){const result=await execute(['agent','repeats',id,runId],{json:true},value);assert.equal(result.code,1);assert.equal(result.calls.length,1);}
217
- });
218
-
219
- test('source command reads an exact saved window without starting research',async()=>{
220
- const {createHash}=await import('node:crypto');const text='Before. é🌲 claim. Requires approval.\r\n';
221
- const value={agent_id:id,run_id:runId,source:{id:'W1',url:'https://source.example/release',title:'Release',text,body_sha256:createHash('sha256').update(text).digest('hex'),observed_at:stamp,published_at:null,source_kind:'indexed',collection_id:'rcoll_one',document_id:'doc_one',version_id:'rv_one'}};
222
- const result=await execute(['agent','source',id,runId,'W1'],{json:true},value);
223
- assert.equal(result.code,0);assert.deepEqual(JSON.parse(result.stdout),value);assert.equal(result.calls.length,1);assert.equal(result.calls[0].route,`/api/research/agents/${id}/runs/${runId}/sources/W1`);assert.equal(result.calls[0].args.body,undefined);
224
- const display=await execute(['agent','source',id,runId,'W1'],{},value);assert.equal(display.code,0);assert.ok(display.stdout.includes('Requires approval.'));assert.ok(display.stdout.includes('admitted evidence window'));
225
- for(const mutate of [v=>v.run_id='other',v=>v.source.id='other',v=>v.source.text+='changed',v=>v.source.body_sha256='0'.repeat(64),v=>v.source.version_id=null,v=>v.source.source_kind='web_search']){
226
- const bad=structuredClone(value);mutate(bad);const denied=await execute(['agent','source',id,runId,'W1'],{json:true},bad);assert.equal(denied.code,1);assert.ok(!denied.stdout.includes('Requires approval.'));assert.equal(denied.calls.length,1);
227
- }
228
- });
229
- test('source command rejects ambiguous or extra arguments before transport',()=>{
230
- for(const [rest,options] of [[['agent','source',id,runId],{}],[['agent','source',id,runId,'W1','extra'],{}],[['agent','source',id,runId,'W1'],{wait:true}]])assert.throws(()=>agentRequest(rest,options));
231
- });
@@ -1,3 +0,0 @@
1
- import {createHash} from 'node:crypto';
2
- import {readAnswerContract} from './answer-contract.mjs';
3
- export const readResearchAnswerContract=run=>readAnswerContract(run,text=>createHash('sha256').update(text,'utf8').digest('hex'));
@@ -1,14 +0,0 @@
1
- import {test} from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {agentCommand} from './research-agent.mjs';
4
- import {makeRun,saturated,hostile,unknown} from '../../sdk/tests/fixtures/answer-contract-cases.mjs';
5
- const execute=async(run,json=false)=>{let stdout='',stderr='',calls=0;const code=await agentCommand(['agent-run',run.agent_id,run.id],{json},{request:async()=>{calls++;return run}},{stdout:t=>stdout+=t,stderr:t=>stderr+=t});return {stdout,stderr,calls,code}};
6
- test('canonical BC03 gap appears once; five saturated notes plus one typed notice retain exact JSON/status',async()=>{
7
- for(const r of [makeRun(),saturated()]){const before=JSON.stringify(r),human=await execute(r),json=await execute(r,true);assert.equal(human.code,0);assert.equal(human.calls,1);assert.equal(json.calls,1);assert.deepEqual(JSON.parse(json.stdout),r);assert.equal(JSON.stringify(r),before);assert.equal(human.stdout.split('does not establish an answer or its contradiction').length-1,1);for(const gap of r.gaps)assert(human.stdout.includes(gap));for(const f of r.findings){assert(human.stdout.includes(f.claim));for(const q of f.qualifiers)assert(human.stdout.includes(q))}}
8
- });
9
- test('malformed assessment remains an optional warning with preserved literal output and no retry',async()=>{
10
- const r=makeRun();r.usage.answer_contract={version:99};r.status='partial';const out=await execute(r);assert.equal(out.code,2);assert.equal(out.calls,1);assert.match(out.stdout,/assessment could not be validated/);assert(out.stdout.includes(r.findings[0].claim));
11
- });
12
- test('terminal and bidi controls in assessment are escaped only in human output',async()=>{
13
- const r=hostile(),out=await execute(r),json=await execute(r,true);assert.equal(out.code,0);assert(!out.stdout.includes('\u001b'));assert(!out.stdout.includes('\u202e'));assert.match(out.stdout,/\\u001b/);assert.match(out.stdout,/\\u202e/);assert.deepEqual(JSON.parse(json.stdout),r);assert(JSON.parse(json.stdout).usage.answer_contract.components.at(-1).text.includes(unknown(r).text));
14
- });
@@ -1,7 +0,0 @@
1
- import {createHash} from 'node:crypto';
2
- import {readResearchAnswerContract} from './research-answer-contract.mjs';
3
- import {readAnswerView} from './answer-view.mjs';
4
- export function readResearchAnswerView(run){
5
- if(!Object.hasOwn(run.usage,'answer_view'))return {state:'absent',view:null,sections:[]};
6
- return readAnswerView(run,readResearchAnswerContract(run),text=>createHash('sha256').update(text,'utf8').digest('hex'));
7
- }
@@ -1,27 +0,0 @@
1
- import {test} from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {agentCommand} from './research-agent.mjs';
4
- import {makeRun,omittedRun,extraSupportedRun} from '../../sdk/tests/fixtures/answer-view-cases.mjs';
5
- const execute=async(run,json=false)=>{let stdout='',stderr='',calls=0;const code=await agentCommand(['agent-run',run.agent_id,run.id],{json},{request:async()=>{calls++;return run}},{stdout:t=>stdout+=t,stderr:t=>stderr+=t});return {stdout,stderr,calls,code}};
6
- test('concise prose precedes unchanged exact facts, conditions, citations and every saved gap',async()=>{
7
- const r=makeRun();r.status='partial';r.gaps=['Saved gap one.','Saved gap two.'];const before=JSON.stringify(r),human=await execute(r),json=await execute(r,true);
8
- assert.equal(human.code,2);assert.equal(human.calls,1);assert(human.stdout.indexOf('Concise answer')<human.stdout.indexOf('Observed statement:'));
9
- assert(human.stdout.includes(r.usage.answer_view.sections[0].assertions[0].text));assert(human.stdout.includes('Evidence: P1'));assert(human.stdout.includes('Review the exact evidence'));
10
- for(const f of r.findings){assert(human.stdout.includes(f.claim));for(const q of f.qualifiers)assert(human.stdout.includes(q));for(const c of f.citations)assert(human.stdout.includes(c.quote))}for(const g of r.gaps)assert(human.stdout.includes(g));
11
- assert.equal(json.calls,1);assert.equal(json.code,2);assert.equal(json.stdout,JSON.stringify(r,null,2)+'\n');assert.equal(JSON.stringify(r),before);
12
- });
13
- test('absent and invalid concise metadata leave legacy human output exactly equal',async()=>{
14
- const absent=makeRun();delete absent.usage.answer_view;const invalid=structuredClone(absent);invalid.usage.answer_view={version:99};
15
- assert.deepEqual(await execute(invalid),await execute(absent));assert.equal((await execute(invalid,true)).stdout,JSON.stringify(invalid,null,2)+'\n');
16
- });
17
- test('concise evidence labels include the sources of additional retained supporting fields',async()=>{
18
- const r=extraSupportedRun(),out=await execute(r);assert.equal(out.calls,1);assert(out.stdout.includes('Evidence: P1, P2'));
19
- assert.equal((await execute(r,true)).stdout,JSON.stringify(r,null,2)+'\n');
20
- });
21
- test('source-scoped unknown stays separate and optional prose does not become a complete badge',async()=>{
22
- const r=omittedRun('source_scoped_unknown_assessment','source_scoped_unknown'),out=await execute(r);
23
- assert(out.stdout.includes('Not established in the saved sources for this part.'));assert(!out.stdout.includes('All questions answered'));assert.equal(out.calls,1);
24
- });
25
- test('unsupported terminal-control prose falls back, while raw JSON remains exact',async()=>{
26
- const r=makeRun();r.usage.answer_view.sections[0].assertions[0].text+='\u001b';const out=await execute(r),raw=await execute(r,true);assert(!out.stdout.includes('Concise answer'));assert(!out.stdout.includes('\u001b'));assert.equal(raw.stdout,JSON.stringify(r,null,2)+'\n');
27
- });