Now that the inspector resolves more saved files, the Recorded run dialog can hold two regions named "Positive prompt" (and two of each Copy/Send action): the frozen run field and the inline current file's prompt. Assistive technology could not tell them apart, and the real-host chronology test read two <pre> elements. Prompt blocks take an optional context label. The inline current file inside a recorded run names its regions "Current file positive prompt" and its actions "Copy/Send current file positive prompt"; the visible headings stay as in the concept. The real-host test now also scopes its frozen-field reads to the "Frozen run prompt texts" section, and a disposable-server workflow pins the distinct names with a resolved current file.
146 lines
11 KiB
JavaScript
146 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {createScope} from '../../web/js/shared/toolkit/core.js';
|
|
import * as module from '../../web/js/workspace/prompt-inspector.mjs';
|
|
|
|
function dom() {
|
|
class Element {
|
|
constructor(tag){this.tagName=tag;this.children=[];this.dataset={};this.attributes={};this.listeners=new Map();this.classList={add:()=>{}};}
|
|
append(...children){for(const child of children){child.parent=this;this.children.push(child);}}
|
|
replaceChildren(...children){this.children=[];this.append(...children);}
|
|
setAttribute(key,value){this.attributes[key]=value;}
|
|
addEventListener(type,listener,{signal}={}){this.listeners.set(type,listener);signal?.addEventListener('abort',()=>this.listeners.delete(type),{once:true});}
|
|
closest(selector){if(selector==='section'&&this.tagName==='section')return this;if(selector==='[data-send-role]'&&Object.hasOwn(this.dataset,'sendRole'))return this;if(selector==='[data-copy-role]'&&Object.hasOwn(this.dataset,'copyRole'))return this;if(selector==='[data-retry-prompts]'&&Object.hasOwn(this.dataset,'retryPrompts'))return this;return this.parent?.closest(selector);}
|
|
click(target){this.listeners.get('click')?.({target});}
|
|
}
|
|
const previous=global.document;global.document={createElement:tag=>new Element(tag),createElementNS:(_,tag)=>new Element(tag)};
|
|
const walk=(root,predicate)=>[...(predicate(root)?[root]:[]),...root.children.flatMap(child=>walk(child,predicate))];
|
|
return {Element,walk,restore:()=>{global.document=previous;}};
|
|
}
|
|
const record=text=>({status:'resolved',text,source:'library'});
|
|
test('shared prompt blocks send complete selected library values and explicit empty negatives',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
|
try{
|
|
const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:record('<literal>\nfull positive'),negative:{status:'unresolved',message:'Choose a saved negative.'}}});
|
|
assert.ok(f.walk(container,node=>node.textContent==='<literal>\nfull positive').length);
|
|
assert.equal(f.walk(container,node=>node.dataset.sendRole==='pair').length,0);
|
|
view.update({positive:record('Complete positive'),negative:record('')});
|
|
const pair=f.walk(container,node=>node.dataset.sendRole==='pair')[0];container.click(pair);
|
|
assert.deepEqual(sent,[{positive:'Complete positive',negative:''}]);
|
|
assert.equal(f.walk(container,node=>node.textContent==='Saved library prompt').length,2);
|
|
view.dispose();container.click(pair);assert.equal(sent.length,1);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
test('positive-only cards never display or send a hidden negative or pair',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,roles:['positive'],prompts:{positive:record('positive'),negative:record('hidden')},sender:{open:roles=>sent.push(roles)}});
|
|
assert.equal(f.walk(container,node=>node.attributes['aria-label']==='Negative prompt').length,0);
|
|
assert.equal(f.walk(container,node=>node.dataset.sendRole==='pair').length,0);
|
|
const button=f.walk(container,node=>node.dataset.sendRole==='positive')[0];container.click(button);assert.deepEqual(sent,[{positive:'positive'}]);
|
|
scope.dispose();container.click(button);assert.equal(sent.length,1);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
|
|
test('disposed blocks ignore updates and aborted parents never mount content',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
try{
|
|
const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record('original')}});
|
|
view.dispose();view.update({positive:record('late')});assert.equal(f.walk(container,node=>node.textContent==='late').length,0);
|
|
scope.dispose();const other=new f.Element('div');module.mountPromptBlocks(other,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record('late')}});assert.equal(other.children.length,0);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
|
|
test('frozen fields disclose their exact source and keep null, empty and whitespace actions distinct',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
|
const field=(text,source)=>({status:text===null?'unresolved':'resolved',text,source:'execution',evidence:{provenance:'recorded',source,reason:null}});
|
|
try{
|
|
const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:field('positive','runtime_return'),negative:field(null,'unknown')}});
|
|
assert.equal(f.walk(container,node=>node.dataset.sendRole==='negative'||node.dataset.sendRole==='pair'||node.dataset.copyRole==='negative').length,0);
|
|
assert.ok(f.walk(container,node=>node.textContent?.includes('runtime_return')).length);
|
|
view.update({positive:field('positive','runtime_return'),negative:field('','host_cache_output')});
|
|
assert.ok(f.walk(container,node=>node.textContent?.includes('host_cache_output')).length);
|
|
const pair=f.walk(container,node=>node.dataset.sendRole==='pair')[0];container.click(pair);
|
|
assert.deepEqual(sent,[{positive:'positive',negative:''}]);
|
|
view.update({positive:field('positive','runtime_return'),negative:field(' \n\t ','host_cache_output')});
|
|
assert.equal(f.walk(container,node=>node.tagName==='pre'&&node.textContent===' \n\t ').length,1);
|
|
container.click(f.walk(container,node=>node.dataset.sendRole==='negative')[0]);
|
|
assert.deepEqual(sent[1],{negative:' \n\t '});
|
|
view.setState({prompts:{positive:field('positive','runtime_return'),negative:field('old','host_cache_output')},actionsEnabled:false});
|
|
container.click(f.walk(container,node=>node.dataset.sendRole==='pair')[0]);assert.equal(sent.length,2);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
|
|
test('a zeroed conditioning negative shows as an explicit empty negative with its own source',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:{status:'resolved',text:'positive',source:'graph-inferred'},negative:{status:'resolved',text:'',source:'zeroed-conditioning'}}});
|
|
assert.equal(f.walk(container,node=>node.textContent==='Zeroed conditioning (no negative text)').length,1);
|
|
assert.equal(f.walk(container,node=>node.textContent==='Empty negative prompt').length,1);
|
|
container.click(f.walk(container,node=>node.dataset.sendRole==='pair')[0]);
|
|
assert.deepEqual(sent,[{positive:'positive',negative:''}]);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
|
|
const unresolved=message=>({status:'unresolved',text:null,source:null,...(message===undefined?{}:{message})});
|
|
const statusLines=(f,container)=>f.walk(container,node=>node.tagName==='p'&&node.className==='muted').map(node=>node.textContent);
|
|
test('unsupported node status names the class',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
const line='Prompt uses a node the inspector does not support (DualCFGGuider).';
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:unresolved(line),negative:unresolved(line)}});
|
|
assert.deepEqual(statusLines(f,container),[line,line]);
|
|
assert.equal(f.walk(container,node=>node.dataset.sendRole!==undefined||node.dataset.copyRole!==undefined).length,0,'no action implies a prompt exists');
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
test('runtime and composite statuses',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
const runtime='Prompt is chosen at run time (PromptSearchList).',composite='Prompt combines several texts (ConditioningCombine).';
|
|
try{
|
|
const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:unresolved(runtime),negative:unresolved(runtime)}});
|
|
assert.deepEqual(statusLines(f,container),[runtime,runtime]);
|
|
view.update({positive:unresolved(composite),negative:{status:'resolved',text:'kept',source:'execution'}});
|
|
assert.deepEqual(statusLines(f,container),[composite]);
|
|
assert.equal(f.walk(container,node=>node.tagName==='pre'&&node.textContent==='kept').length,1);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
test('limit status',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
const line='Prompt exceeds inspection limits.';
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:unresolved(line),negative:unresolved(line)}});
|
|
assert.deepEqual(statusLines(f,container),[line,line]);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
test('roles without a usable line keep the status copy, and a candidate is never shown as a prompt',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:unresolved(''),negative:{status:'ambiguous',text:'candidate text',source:null}}});
|
|
assert.deepEqual(statusLines(f,container),['Prompt could not be resolved.','Multiple possible prompts. Source could not be determined.']);
|
|
assert.equal(f.walk(container,node=>node.textContent==='candidate text').length,0);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
test('class names in status lines render as text, never markup',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
const line='Prompt uses a node the inspector does not support (<img src=x onerror=alert(1)>).';
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:unresolved(line),negative:unresolved(line)}});
|
|
assert.deepEqual(statusLines(f,container),[line,line]);
|
|
assert.equal(f.walk(container,node=>node.tagName==='img').length,0);
|
|
assert.equal(f.walk(container,node=>Object.hasOwn(node,'innerHTML')).length,0);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|
|
|
|
test('a context label names nested prompt regions and actions apart from the frozen blocks while the visible heading stays',()=>{
|
|
const f=dom(),scope=createScope(),container=new f.Element('div');
|
|
try{
|
|
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},contextLabel:'Current file',prompts:{positive:record('p'),negative:record('n')}});
|
|
const labels=f.walk(container,node=>node.attributes['aria-label']).map(node=>node.attributes['aria-label']);
|
|
for(const name of ['Current file positive prompt','Current file negative prompt','Copy current file positive prompt','Send current file positive prompt','Copy current file negative prompt','Send current file negative prompt']) assert.ok(labels.includes(name),name);
|
|
assert.equal(labels.filter(name=>/^(Positive|Negative) prompt$/.test(name)).length,0);
|
|
assert.deepEqual(f.walk(container,node=>node.tagName==='h3').map(node=>node.textContent),['Positive prompt','Negative prompt']);
|
|
const plain=new f.Element('div');module.mountPromptBlocks(plain,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record('p')}});
|
|
assert.ok(f.walk(plain,node=>node.attributes['aria-label']==='Positive prompt').length);
|
|
}finally{scope.dispose();f.restore();}
|
|
});
|