diff --git a/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js new file mode 100644 index 00000000..e2221dc2 --- /dev/null +++ b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.js @@ -0,0 +1,21 @@ +var createDoomModule = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename; + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){}var moduleOverrides={...Module};var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("doom.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{receiveInstance(mod,inst);resolve(mod.exports)})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.unshift(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.unshift(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=Module["noExitRuntime"]||true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __emscripten_system=command=>{if(ENVIRONMENT_IS_NODE){if(!command)return 1;var cmdstr=UTF8ToString(command);if(!cmdstr.length)return 0;var cp=require("child_process");var ret=cp.spawnSync(cmdstr,[],{shell:true,stdio:"inherit"});var _W_EXITCODE=(ret,sig)=>ret<<8|sig;if(ret.status===null){var signalToNumber=sig=>{switch(sig){case"SIGHUP":return 1;case"SIGQUIT":return 3;case"SIGFPE":return 8;case"SIGKILL":return 9;case"SIGALRM":return 14;case"SIGTERM":return 15;default:return 2}};return _W_EXITCODE(0,signalToNumber(ret.signal))}return _W_EXITCODE(ret.status,0)}if(!command)return 0;return-52};var _emscripten_get_now=()=>performance.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var FS_createPath=FS.createPath;var FS_unlink=path=>FS.unlink(path);var FS_createLazyFile=FS.createLazyFile;var FS_createDevice=FS.createDevice;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";var wasmImports={__syscall_fcntl64:___syscall_fcntl64,__syscall_ioctl:___syscall_ioctl,__syscall_mkdirat:___syscall_mkdirat,__syscall_openat:___syscall_openat,__syscall_renameat:___syscall_renameat,__syscall_rmdir:___syscall_rmdir,__syscall_unlinkat:___syscall_unlinkat,_emscripten_system:__emscripten_system,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["__wasm_call_ctors"];var _free=Module["_free"]=wasmExports["free"];var _malloc=Module["_malloc"]=wasmExports["malloc"];var _doomgeneric_Tick=Module["_doomgeneric_Tick"]=wasmExports["doomgeneric_Tick"];var _doomgeneric_Create=Module["_doomgeneric_Create"]=wasmExports["doomgeneric_Create"];var _DG_GetFrameBuffer=Module["_DG_GetFrameBuffer"]=wasmExports["DG_GetFrameBuffer"];var _DG_GetScreenWidth=Module["_DG_GetScreenWidth"]=wasmExports["DG_GetScreenWidth"];var _DG_GetScreenHeight=Module["_DG_GetScreenHeight"]=wasmExports["DG_GetScreenHeight"];var _DG_PushKeyEvent=Module["_DG_PushKeyEvent"]=wasmExports["DG_PushKeyEvent"];var __emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];var __emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];var _emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["setValue"]=setValue;Module["getValue"]=getValue;Module["FS_createPreloadedFile"]=FS_createPreloadedFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = createDoomModule; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = createDoomModule; +} else if (typeof define === 'function' && define['amd']) + define([], () => createDoomModule); diff --git a/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm new file mode 100755 index 00000000..fb99ae7c Binary files /dev/null and b/pi-mono-reference/packages/coding-agent/examples/extensions/doom-overlay/doom/build/doom.wasm differ diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e92..3334c0e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1 @@ -packages: - - "packages/*" +packages: [] diff --git a/src/main/agent-role-helpers.ts b/src/main/agent-role-helpers.ts index 13f567e0..0104dbe6 100644 --- a/src/main/agent-role-helpers.ts +++ b/src/main/agent-role-helpers.ts @@ -1,11 +1,14 @@ /** * Role-classification helpers used across prompt construction and skill - * auto-assignment. Keeping a single keyword list here prevents drift - * between system-prompt.ts and automation-skills.ts. + * auto-assignment. Historical keyword match remains here for backfill and + * UI-free fallback; typed `agent.roleLevel` is the source of truth once the + * column is populated. * * Matching uses word-boundary tokenization instead of substring search, * so "lead" does not match "cleaner" or "leaderboard". */ +import type { AgentRoleLevel } from "@shared/types"; +import { isLeadershipLevel } from "@shared/types"; export const LEADERSHIP_ROLE_KEYWORDS = [ "chief", @@ -31,3 +34,26 @@ export function roleMatchesLeadershipKeywords(role: string, title?: string | nul const tokens = new Set(tokenize(`${role} ${title ?? ""}`)); return LEADERSHIP_ROLE_KEYWORDS.some((keyword) => tokens.has(keyword)); } + +/** + * One-shot inference from free-text role/title strings. Used when creating or + * backfilling an agent so `roleLevel` can be stored once and re-read cheaply. + * The most specific token wins; order here is most-senior → least. + */ +export function inferRoleLevel(role: string, title?: string | null): AgentRoleLevel { + const tokens = new Set(tokenize(`${role} ${title ?? ""}`)); + if (tokens.has("ceo") || tokens.has("cto") || tokens.has("cmo") || tokens.has("coo") || tokens.has("cfo") || tokens.has("chief")) { + return "c_suite"; + } + if (tokens.has("vp") || tokens.has("vice") || tokens.has("president")) return "vp"; + if (tokens.has("director") || tokens.has("head")) return "director"; + if (tokens.has("manager")) return "manager"; + if (tokens.has("lead")) return "lead"; + return "ic"; +} + +/** Canonical leadership check: typed enum wins, fallback to keyword match. */ +export function agentIsLeader(agent: { roleLevel?: AgentRoleLevel | null; role: string; title: string | null }): boolean { + if (agent.roleLevel) return isLeadershipLevel(agent.roleLevel); + return roleMatchesLeadershipKeywords(agent.role, agent.title); +} diff --git a/src/main/agent-runtime/approval-hook.ts b/src/main/agent-runtime/approval-hook.ts new file mode 100644 index 00000000..8d54fdf0 --- /dev/null +++ b/src/main/agent-runtime/approval-hook.ts @@ -0,0 +1,134 @@ +import type { AppDatabase } from "@main/database"; +import type { DesktopEvent } from "@shared/types"; + +interface BeforeRequestPayload { + agentId: string; + companyId: string; + taskId: string | null; + toolCallId: string; + toolName: string; + args: unknown; +} + +interface AfterRequestPayload { + toolCallId: string; + toolName: string; + isError: boolean; +} + +export interface HumanApprovalHookDeps { + db: AppDatabase; + emitEvent: (event: DesktopEvent) => void; + logger: { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + }; +} + +interface PendingApproval { + approvalId: string; + resolve: (verdict: { block?: boolean; reason?: string }) => void; +} + +const TOOL_LABEL: Record = { + bash: "shell", + edit: "edit", + write: "write", + read: "read", + ls: "ls", + grep: "grep", + find: "find", +}; + +function summariseArgs(toolName: string, args: unknown): string { + if (args === null || typeof args !== "object") return String(args); + const argsObj = args as Record; + switch (toolName) { + case "bash": { + const command = typeof argsObj.command === "string" ? argsObj.command : ""; + return command.length > 200 ? `${command.slice(0, 197)}...` : command; + } + case "write": + case "edit": + case "read": { + const path = typeof argsObj.path === "string" ? argsObj.path : (typeof argsObj.file_path === "string" ? argsObj.file_path : ""); + return path; + } + default: + try { + const json = JSON.stringify(args); + return json.length > 200 ? `${json.slice(0, 197)}...` : json; + } catch { + return ""; + } + } +} + +/** + * Human-in-the-loop hook bridging pi `beforeToolCall` to + * AgentCompany's `ApprovalService` via the existing approvals table. + */ +export class HumanApprovalHook { + private readonly pending = new Map(); + + constructor(private readonly deps: HumanApprovalHookDeps) {} + + async before(runId: string, payload: BeforeRequestPayload): Promise<{ block?: boolean; reason?: string }> { + const { db, emitEvent } = this.deps; + const summary = summariseArgs(payload.toolName, payload.args); + const label = TOOL_LABEL[payload.toolName] ?? payload.toolName; + const approvalId = db.requestApproval({ + companyId: payload.companyId, + relatedTaskId: payload.taskId, + requestedByAgentId: payload.agentId, + type: "dangerous_command", + payloadSummary: `${label}: ${summary}`.slice(0, 280), + impactSummary: `Agent ${payload.agentId} requested ${label} during run ${runId}.`, + payloadJson: JSON.stringify({ + runId, + toolCallId: payload.toolCallId, + toolName: payload.toolName, + args: payload.args, + }), + }); + + emitEvent({ type: "approval-updated", approvalId, action: "created" }); + emitEvent({ type: "domain-changed" }); + + return new Promise<{ block?: boolean; reason?: string }>((resolve) => { + this.pending.set(approvalId, { approvalId, resolve }); + }); + } + + /** + * Called by the IPC approval-decided pipeline when a user resolves a + * pending approval. If we have a pending pi `beforeToolCall` waiting on + * this approval, resolve it. + */ + resolveApproval(approvalId: string, decision: "approve" | "reject" | "revision_requested", note: string): boolean { + const pending = this.pending.get(approvalId); + if (!pending) return false; + this.pending.delete(approvalId); + if (decision === "approve") { + pending.resolve({}); + } else { + pending.resolve({ block: true, reason: note || `Tool execution ${decision}.` }); + } + return true; + } + + /** Called when a run is cancelled — resolves any pending approvals as blocked. */ + cancelAll(reason: string): void { + for (const [id, pending] of this.pending) { + pending.resolve({ block: true, reason }); + this.pending.delete(id); + } + } + + + async after(_runId: string, _payload: AfterRequestPayload): Promise { + // Audit hook intentionally minimal — actual tool result is captured by + // the agent-event stream and persisted via SqliteSessionStore. + } +} diff --git a/src/main/agent-runtime/index.ts b/src/main/agent-runtime/index.ts new file mode 100644 index 00000000..bce9d9d3 --- /dev/null +++ b/src/main/agent-runtime/index.ts @@ -0,0 +1,4 @@ +export { SqliteSessionStore } from "./sqlite-session-store"; +export { HumanApprovalHook } from "./approval-hook"; +export { CostWriter } from "./cost-writer"; +export { RuntimeBridge } from "./runtime-bridge"; diff --git a/src/main/agent-runtime/sqlite-session-store.ts b/src/main/agent-runtime/sqlite-session-store.ts new file mode 100644 index 00000000..d6acbde2 --- /dev/null +++ b/src/main/agent-runtime/sqlite-session-store.ts @@ -0,0 +1,65 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { AppDatabase } from "@main/database"; + +/** + * SQLite-backed pi `AgentMessage[]` store. + * + * One store instance per task_session. Owners must call `appendMessage` + * exactly once per pi `message_end` event, and `replaceMessages` after pi + * `compact` returns a compacted message list. + */ +export class SqliteSessionStore { + constructor( + private readonly db: AppDatabase, + public readonly taskSessionId: string, + ) {} + + loadMessages(): AgentMessage[] { + const rows = this.db.sessionEntries.listActive(this.taskSessionId); + const result: AgentMessage[] = []; + for (const row of rows) { + if (row.kind === "message") { + try { + result.push(JSON.parse(row.payloadJson) as AgentMessage); + } catch { + // Skip malformed entries (defensive) — preserves the rest of the + // transcript instead of failing the entire load. + } + } + } + return result; + } + + appendMessage(message: AgentMessage): void { + this.db.sessionEntries.append({ + taskSessionId: this.taskSessionId, + kind: "message", + role: (message as { role?: string }).role ?? null, + payloadJson: JSON.stringify(message), + }); + } + + appendCompaction(summary: string): void { + this.db.sessionEntries.append({ + taskSessionId: this.taskSessionId, + kind: "compaction", + role: null, + payloadJson: JSON.stringify({ summary }), + }); + } + + replaceMessages(messages: AgentMessage[], compactionSummary?: string): void { + const entries: Array<{ kind: "message" | "compaction"; role: string | null; payloadJson: string }> = []; + if (compactionSummary) { + entries.push({ kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: compactionSummary }) }); + } + for (const message of messages) { + entries.push({ + kind: "message", + role: (message as { role?: string }).role ?? null, + payloadJson: JSON.stringify(message), + }); + } + this.db.sessionEntries.replace({ taskSessionId: this.taskSessionId, entries }); + } +} diff --git a/src/main/credentials/credential-store.ts b/src/main/credentials/credential-store.ts new file mode 100644 index 00000000..32c12da3 --- /dev/null +++ b/src/main/credentials/credential-store.ts @@ -0,0 +1,122 @@ +import type { AppDatabase } from "@main/database"; +import type { CredentialBackend, CredentialKind } from "@main/database/credentials"; +import type { CredentialStatus } from "@main/database/credentials"; +import type { CredentialSummary } from "@shared/contracts/schemas-credentials"; +import type { StoredCredentialPayload } from "./types"; + +const HINT_SUFFIX_LEN = 4; +const HINT_PREFIX_LEN = 6; + +function maskApiKey(value: string): string { + if (value.length <= HINT_PREFIX_LEN + HINT_SUFFIX_LEN) { + return `${value.slice(0, 2)}...`; + } + const prefix = value.slice(0, HINT_PREFIX_LEN); + const suffix = value.slice(-HINT_SUFFIX_LEN); + return `${prefix}...${suffix}`; +} + +export function deriveDisplayHint(payload: StoredCredentialPayload): string { + switch (payload.kind) { + case "api_key": + return payload.value.source === "literal" + ? maskApiKey(payload.value.value) + : `env:${payload.value.value}`; + case "azure": + return payload.value.resourceName + ? `azure:${payload.value.resourceName}` + : `azure:${maskApiKey(payload.value.apiKey)}`; + case "aws": + if (payload.value.authMode === "profile") return `aws:profile=${payload.value.awsProfile ?? "?"}`; + if (payload.value.authMode === "iam") return `aws:iam=${maskApiKey(payload.value.accessKeyId ?? "?")}`; + return `aws:bearer`; + case "gcp_adc": + return `gcp:${payload.value.project}@${payload.value.location}`; + case "oauth": { + const expiresMs = payload.value.expires; + const expiresLabel = typeof expiresMs === "number" && Number.isFinite(expiresMs) + ? new Date(expiresMs).toISOString() + : "no-expiry"; + return `oauth:expires=${expiresLabel}`; + } + } +} + +export interface CredentialStoreDeps { + db: AppDatabase; +} + +export class CredentialStore { + constructor(private readonly deps: CredentialStoreDeps) {} + + list(): CredentialSummary[] { + return this.deps.db.credentialsTable.list().map((row) => ({ + connectorId: row.connectorId, + kind: row.kind as CredentialKind, + displayHint: row.displayHint, + status: row.status, + expiresAt: row.expiresAt, + lastRefreshedAt: row.lastRefreshedAt, + lastError: row.lastError, + updatedAt: row.updatedAt, + })); + } + + /** Encrypts and stores a credential. Caller is responsible for kind/connectorId match. */ + save(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null = null): CredentialSummary { + const json = JSON.stringify(payload.value); + const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(json); + const stored = this.deps.db.credentialsTable.upsert({ + connectorId, + kind: payload.kind, + backend: backend as CredentialBackend, + ciphertext, + displayHint: deriveDisplayHint(payload), + status: "active", + expiresAt, + lastRefreshedAt: null, + lastError: null, + }); + return { + connectorId: stored.connectorId, + kind: stored.kind, + displayHint: stored.displayHint, + status: stored.status, + expiresAt: stored.expiresAt, + lastRefreshedAt: stored.lastRefreshedAt, + lastError: stored.lastError, + updatedAt: stored.updatedAt, + }; + } + + /** Decrypts and returns the typed credential payload. Returns null when missing. */ + read(connectorId: string): StoredCredentialPayload | null { + const row = this.deps.db.credentialsTable.get(connectorId); + if (!row) return null; + const json = this.deps.db.vault.decrypt( + row.backend, + Buffer.isBuffer(row.ciphertext) ? row.ciphertext : Buffer.from(row.ciphertext), + ); + const value = JSON.parse(json); + return { kind: row.kind, value } as StoredCredentialPayload; + } + + remove(connectorId: string): void { + this.deps.db.credentialsTable.remove(connectorId); + } + + setStatus(connectorId: string, status: CredentialStatus, lastError: string | null = null): void { + this.deps.db.credentialsTable.setStatus(connectorId, status, lastError); + } + + /** Replaces the OAuth ciphertext after a successful refresh. */ + saveRefreshedOAuth(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null): void { + const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(JSON.stringify(payload.value)); + this.deps.db.credentialsTable.markRefreshed( + connectorId, + backend as CredentialBackend, + ciphertext, + expiresAt, + ); + } +} diff --git a/src/main/credentials/index.ts b/src/main/credentials/index.ts new file mode 100644 index 00000000..a886f453 --- /dev/null +++ b/src/main/credentials/index.ts @@ -0,0 +1,4 @@ +export { CredentialStore, deriveDisplayHint } from "./credential-store"; +export { CredentialResolver, CredentialError, type ResolvedCredential } from "./credential-resolver"; +export { OAuthLoginService } from "./oauth-login-service"; +export type { StoredCredentialPayload, OAuthCredentialPayload, CredentialKind } from "./types"; diff --git a/src/main/credentials/types.ts b/src/main/credentials/types.ts new file mode 100644 index 00000000..aaa6e7b9 --- /dev/null +++ b/src/main/credentials/types.ts @@ -0,0 +1,30 @@ +import type { + ApiKeyValue, + AwsCredential, + AzureCredential, + GcpAdcCredential, +} from "@shared/contracts/schemas-credentials"; + +/** + * pi-ai's `OAuthCredentials` shape. + * + * Mirrored locally as a type-only declaration to avoid pulling pi runtime + * symbols into modules that only need the shape. The actual contract is + * enforced by pi at runtime via the OAuth provider interface. + */ +export interface OAuthCredentialPayload { + refresh: string; + access: string; + expires: number; + /** Provider-specific extra fields (e.g. github copilot domain) */ + [key: string]: unknown; +} + +export type StoredCredentialPayload = + | { kind: "oauth"; value: OAuthCredentialPayload } + | { kind: "api_key"; value: ApiKeyValue } + | { kind: "azure"; value: AzureCredential } + | { kind: "aws"; value: AwsCredential } + | { kind: "gcp_adc"; value: GcpAdcCredential }; + +export type CredentialKind = StoredCredentialPayload["kind"]; diff --git a/src/main/database-stale-recovery.ts b/src/main/database-stale-recovery.ts new file mode 100644 index 00000000..6202d481 --- /dev/null +++ b/src/main/database-stale-recovery.ts @@ -0,0 +1,70 @@ +import type { DatabaseSync } from "node:sqlite"; +import { STALE_LOCK_TIMEOUT_MS } from "./database-base"; + +function nowIso(): string { + return new Date().toISOString(); +} + +/** + * Stale-state recovery helpers extracted from `database-base.ts` to keep that + * file under the 400-line cap. Both helpers are called by `AppDatabase.init` + * (interruptStaleRuns) and the orchestration recovery loop (recoverStaleLocks). + */ + +interface InterruptDeps { + db: DatabaseSync; + transaction: (op: () => T) => T; + releaseAllWorkspaceLocks: () => void; +} + +export function interruptStaleRunsImpl(deps: InterruptDeps): number { + const ts = nowIso(); + let interruptedCount = 0; + deps.transaction(() => { + const result = deps.db + .prepare( + "update runs set status = 'interrupted', error_message = 'AgentCompany restarted before the run could report completion.', finished_at = ?, updated_at = ? where status = 'running'", + ) + .run(ts, ts); + interruptedCount = Number(result.changes); + if (interruptedCount > 0) { + deps.db + .prepare( + "update tasks set status = 'blocked', updated_at = ? where id in (select task_id from runs where status = 'interrupted' and finished_at = ?) and status in ('in_progress', 'todo')", + ) + .run(ts, ts); + } + deps.db + .prepare( + "update tasks set active_run_id = null where active_run_id in (select id from runs where status = 'interrupted')", + ) + .run(); + deps.db.prepare("update agents set status = 'idle', updated_at = ? where status = 'running'").run(ts); + deps.releaseAllWorkspaceLocks(); + }); + return interruptedCount; +} + +interface RecoverDeps { + db: DatabaseSync; + readAll: (sql: string, ...params: import("node:sqlite").SQLInputValue[]) => T[]; +} + +export function recoverStaleLocksImpl( + deps: RecoverDeps, +): Array<{ taskId: string; companyId: string; title: string }> { + const cutoff = new Date(Date.now() - STALE_LOCK_TIMEOUT_MS).toISOString(); + const stale = deps.readAll<{ id: string; companyId: string; title: string }>( + "SELECT id, company_id AS companyId, title FROM tasks WHERE execution_locked_at IS NOT NULL AND execution_locked_at < ? AND status = 'in_progress' AND checkout_run_id NOT IN (SELECT id FROM runs WHERE status = 'running')", + cutoff, + ); + const ts = nowIso(); + for (const task of stale) { + deps.db + .prepare( + "UPDATE tasks SET checkout_run_id = NULL, execution_locked_at = NULL, status = 'todo', updated_at = ? WHERE id = ?", + ) + .run(ts, task.id); + } + return stale.map((t) => ({ taskId: t.id, companyId: t.companyId, title: t.title })); +} diff --git a/src/main/database/entity-crud.ts b/src/main/database/entity-crud.ts index 5e124130..46ec06e4 100644 --- a/src/main/database/entity-crud.ts +++ b/src/main/database/entity-crud.ts @@ -63,7 +63,7 @@ export function buildEntityCrudMethods( getAgent(id: string): AgentRecord { const row = getDb() .prepare( - "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", + "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", ) .get(id) as AgentRecord | undefined; if (!row) throw new Error(`Agent not found: ${id}`); @@ -74,7 +74,7 @@ export function buildEntityCrudMethods( findAgent(id: string): AgentRecord | null { const row = getDb() .prepare( - "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", + "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?", ) .get(id) as AgentRecord | undefined; if (!row) return null; diff --git a/src/main/database/snapshot.ts b/src/main/database/snapshot.ts index 6d06e662..72907140 100644 --- a/src/main/database/snapshot.ts +++ b/src/main/database/snapshot.ts @@ -63,7 +63,7 @@ export function buildSnapshotMethods( ...companyArgs, ).map((record) => ({ ...record, isPrimary: toBoolean(record.isPrimary) })) as WorkspaceRecord[], agents: readAll( - `select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`, + `select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`, ...companyArgs, ).map((record: SqliteRow) => ({ ...record, heartbeatEnabled: toBoolean(record.heartbeatEnabled) })) as AgentRecord[], goals: readAll( diff --git a/src/main/model-resolver.ts b/src/main/model-resolver.ts new file mode 100644 index 00000000..1c90fa5c --- /dev/null +++ b/src/main/model-resolver.ts @@ -0,0 +1,91 @@ +/** + * Resolves `Model` and the dynamic stream headers needed for providers + * that pi-ai's catalog does not ship out of the box. + * + * Today this only covers the two Fireworks connectors (`fireworks` and + * `fireworks-firepass`), which both speak the Anthropic-compatible endpoint + * at `https://api.fireworks.ai/inference` and require Bearer auth. For every + * other provider we delegate to pi-ai's `getModel` / `getModels`. + */ + +import { getModel, getModels, type Api, type Model } from "@mariozechner/pi-ai"; + +const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference"; +const FIREWORKS_DEFAULT_CONTEXT = 128_000; +const FIREWORKS_DEFAULT_MAX_TOKENS = 8192; + +export const FIREWORKS_FIREPASS_MODEL_ID = "accounts/fireworks/routers/kimi-k2p5-turbo"; +export const FIREWORKS_FIREPASS_MODEL_NAME = "Kimi K2.5 Turbo (Fire Pass)"; + +const FIREWORKS_CURATED_MODELS: Array<{ id: string; name: string; contextWindow?: number }> = [ + { id: "accounts/fireworks/models/kimi-k2-instruct", name: "Kimi K2 Instruct", contextWindow: 131_072 }, + { id: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B A35B Instruct", contextWindow: 262_144 }, + { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1", contextWindow: 163_840 }, + { id: "accounts/fireworks/models/glm-4p5", name: "GLM 4.5", contextWindow: 131_072 }, + { id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B Instruct", contextWindow: 131_072 }, +]; + +function buildFireworksModel(modelId: string, name?: string, contextWindow?: number): Model<"anthropic-messages"> { + return { + id: modelId, + name: name ?? modelId, + api: "anthropic-messages", + provider: "fireworks", + baseUrl: FIREWORKS_BASE_URL, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: contextWindow ?? FIREWORKS_DEFAULT_CONTEXT, + maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, + }; +} + +function isFireworksProvider(provider: string): boolean { + return provider === "fireworks" || provider === "fireworks-firepass"; +} + +export function resolveModel(provider: string, modelId: string): Model | null { + if (provider === "fireworks-firepass") { + return buildFireworksModel(FIREWORKS_FIREPASS_MODEL_ID, FIREWORKS_FIREPASS_MODEL_NAME, 262_144); + } + if (provider === "fireworks") { + const curated = FIREWORKS_CURATED_MODELS.find((entry) => entry.id === modelId); + return buildFireworksModel(modelId, curated?.name, curated?.contextWindow); + } + try { + return getModel(provider as never, modelId as never) ?? null; + } catch { + return null; + } +} + +export function resolveModels(provider: string): Array<{ id: string; name: string }> { + if (provider === "fireworks-firepass") { + return [{ id: FIREWORKS_FIREPASS_MODEL_ID, name: FIREWORKS_FIREPASS_MODEL_NAME }]; + } + if (provider === "fireworks") { + return FIREWORKS_CURATED_MODELS.map(({ id, name }) => ({ id, name })); + } + try { + const models = getModels(provider as never) ?? []; + return models.map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? model.id })); + } catch { + return []; + } +} + +/** + * Extra HTTP headers that must be merged into the streamSimple call for the + * given provider. Returns undefined when no extra headers are required. + * + * Fireworks' Anthropic-compat endpoint authenticates via `Authorization: Bearer`, + * not `x-api-key`. pi-ai's anthropic provider only sends `x-api-key`, so we + * inject the bearer header at the call site. + */ +export function resolveExtraStreamHeaders(provider: string, apiKey: string): Record | undefined { + if (!apiKey) return undefined; + if (isFireworksProvider(provider)) { + return { Authorization: `Bearer ${apiKey}` }; + } + return undefined; +} diff --git a/src/main/resume-interrupted-tasks.ts b/src/main/resume-interrupted-tasks.ts new file mode 100644 index 00000000..2e4b07da --- /dev/null +++ b/src/main/resume-interrupted-tasks.ts @@ -0,0 +1,65 @@ +import type log from "electron-log/main.js"; +import type { AppDatabase } from "./database"; + +interface ResumeInterruptedTasksDeps { + db: AppDatabase; + logger: typeof log; + wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => string | null; + publishDomainChanged: () => void; +} + +/** + * Resume tasks that were left behind by a clean shutdown. + * + * The worker-service marks running/queued runs as "interrupted" with a fixed + * summary string on shutdown, but does not reset the owning task. This helper + * rolls those tasks back to "todo", releases their run locks, and re-wakes the + * assigned agents so heartbeat can pick them up again. + */ +export function resumeInterruptedTasks({ + db, + logger, + wakeAgentIfPossible, + publishDomainChanged, +}: ResumeInterruptedTasksDeps): void { + try { + const snapshot = db.listSnapshot(); + const shutdownRuns = snapshot.runs.filter( + (r) => + r.status === "interrupted" && + (r.summary === "Application shut down." || r.summary === "Worker process crashed."), + ); + if (shutdownRuns.length === 0) return; + + const resumedAgents = new Set(); + for (const run of shutdownRuns) { + try { + const task = db.getTask(run.taskId); + if (task.status === "in_progress" || task.status === "blocked") { + db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); + db.releaseTaskLock(run.taskId, run.id); + } + if (!resumedAgents.has(run.agentId)) { + resumedAgents.add(run.agentId); + wakeAgentIfPossible(run.agentId, run.companyId, "app_restart_resume"); + } + } catch (error) { + logger.warn( + `[bootstrap] Skipped shutdown recovery for run ${run.id} (task/agent may have been deleted):`, + error, + ); + } + } + + if (resumedAgents.size > 0) { + logger.info( + `[bootstrap] Resumed ${shutdownRuns.length} interrupted task(s) across ${resumedAgents.size} agent(s) after clean shutdown recovery.`, + ); + publishDomainChanged(); + } + } catch (err) { + logger.warn( + `[bootstrap] Shutdown recovery failed: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} diff --git a/src/main/service-graph-orchestration-deps.ts b/src/main/service-graph-orchestration-deps.ts new file mode 100644 index 00000000..70b83b6a --- /dev/null +++ b/src/main/service-graph-orchestration-deps.ts @@ -0,0 +1,65 @@ +/** + * Helpers for assembling orchestration-service dependencies in service-graph.ts. + * Extracted to keep service-graph.ts under the 400-line cap. + */ + +import type { AppDatabase } from "./database"; +import { createSkillService } from "./skill-service"; +import type { createSessionManagementService } from "./session-management-service"; +import type { RuntimeSkillEntry } from "./skill-types"; + +interface Logger { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} + +export function makeListRuntimeSkillEntries(deps: { db: AppDatabase; logger: Logger }) { + let cache: ReturnType | null = null; + return (companyId: string, agentId: string, connectorId: string): RuntimeSkillEntry[] => { + try { + if (!cache) cache = createSkillService({ db: deps.db, profileDir: deps.db.profileDir, logger: deps.logger }); + return cache.listRuntimeSkillEntries(companyId, agentId, connectorId); + } catch (error) { + deps.logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error); + return []; + } + }; +} + +export function makeSessionHandoffLookup( + deps: { logger: Logger }, + getSessionService: () => ReturnType, +) { + return (taskId: string): string | null => { + try { + return getSessionService().getHandoffContext(taskId); + } catch (error) { + deps.logger.warn(`[service-graph] getSessionHandoff failed for task ${taskId}:`, error); + return null; + } + }; +} + +export function makePreRunRotationEvaluator( + deps: { db: AppDatabase; logger: Logger }, + getSessionService: () => ReturnType, +) { + return (taskId: string, connectorId: string): { rotated: boolean; handoff: string | null } => { + try { + const sessionSvc = getSessionService(); + const activeSession = deps.db.getActiveTaskSession(taskId); + if (!activeSession) return { rotated: false, handoff: null }; + const result = sessionSvc.evaluateRotation(activeSession, connectorId); + if (result.shouldRotate) { + sessionSvc.rotateSession(activeSession); + const handoff = sessionSvc.getHandoffContext(taskId); + return { rotated: true, handoff }; + } + return { rotated: false, handoff: null }; + } catch (error) { + deps.logger.warn(`[service-graph] evaluatePreRunSessionRotation failed for task ${taskId}:`, error); + return { rotated: false, handoff: null }; + } + }; +} diff --git a/src/main/service-graph-pi-runtime.ts b/src/main/service-graph-pi-runtime.ts new file mode 100644 index 00000000..407e0ab0 --- /dev/null +++ b/src/main/service-graph-pi-runtime.ts @@ -0,0 +1,58 @@ +/** + * Pi-mono runtime + credential service factory. + * + * Composes `CredentialStore`, `CredentialResolver`, `OAuthLoginService`, and + * `RuntimeBridge` for `service-graph.ts`. Extracted to keep `service-graph.ts` + * under the 400-line cap. + */ + +import type { AppDatabase } from "./database"; +import type { DesktopEvent } from "@shared/types"; +import { CredentialResolver, CredentialStore, OAuthLoginService } from "./credentials"; +import { RuntimeBridge } from "./agent-runtime"; + +export interface PiRuntimeServicesDeps { + db: AppDatabase; + emitEvent: (event: DesktopEvent) => void; + refreshConnectorHealth: (connectorId: string) => Promise; + logger: { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + }; +} + +export interface PiRuntimeServices { + credentialStore: CredentialStore; + credentialResolver: CredentialResolver; + oauthLoginService: OAuthLoginService; + runtimeBridge: RuntimeBridge; + release: () => void; +} + +export function createPiRuntimeServices(deps: PiRuntimeServicesDeps): PiRuntimeServices { + const credentialStore = new CredentialStore({ db: deps.db }); + const credentialResolver = new CredentialResolver(credentialStore); + const oauthLoginService = new OAuthLoginService({ + store: credentialStore, + emitEvent: deps.emitEvent, + refreshConnectorHealth: deps.refreshConnectorHealth, + logger: deps.logger, + }); + const runtimeBridge = new RuntimeBridge({ + db: deps.db, + emitEvent: deps.emitEvent, + logger: deps.logger, + credentialResolver, + }); + + return { + credentialStore, + credentialResolver, + oauthLoginService, + runtimeBridge, + release: () => { + runtimeBridge.approvalHook.cancelAll("AgentCompany shutting down."); + }, + }; +} diff --git a/src/main/suppress-warnings.ts b/src/main/suppress-warnings.ts new file mode 100644 index 00000000..0fb1700e --- /dev/null +++ b/src/main/suppress-warnings.ts @@ -0,0 +1,25 @@ +/** + * Silences the `node:sqlite` ExperimentalWarning emitted on first import. + * Must be imported before any module that touches `node:sqlite`, otherwise + * the warning has already fired. + * + * Targeted: only the SQLite experimental warning. All other warnings pass + * through unchanged. + */ + +const originalEmitWarning = process.emitWarning.bind(process); + +function isSqliteExperimentalWarning(warning: unknown): boolean { + if (typeof warning === "string") { + return /SQLite is an experimental feature/.test(warning); + } + if (warning instanceof Error) { + return warning.name === "ExperimentalWarning" && /SQLite/.test(warning.message); + } + return false; +} + +process.emitWarning = ((warning: unknown, ...rest: unknown[]) => { + if (isSqliteExperimentalWarning(warning)) return; + return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest); +}) as typeof process.emitWarning; diff --git a/src/main/worker/log-redactor.ts b/src/main/worker/log-redactor.ts new file mode 100644 index 00000000..979d1b0c --- /dev/null +++ b/src/main/worker/log-redactor.ts @@ -0,0 +1,81 @@ +/** + * Defensive token redactor for worker process logs. + * + * Pi-mono's HTTP clients are not known to log credentials, but stack traces + * from upstream libraries (or future pi versions) could include header + * fragments containing `Authorization: Bearer ...` or `?access_token=...`. + * To prevent any such fragment from reaching the parent's stderr stream, + * `installLogRedactor()` wraps `console.error`, `console.warn`, `console.log`, + * `process.stdout.write`, and `process.stderr.write` so that every string + * argument is filtered through `redactTokens()` before transmission. + */ + +const PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ + { pattern: /Bearer\s+[A-Za-z0-9._\-+/=]{8,}/g, replacement: "Bearer [REDACTED]" }, + { pattern: /(authorization\s*[:=]\s*)([^\s,;]{8,})/gi, replacement: "$1[REDACTED]" }, + { pattern: /(access_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" }, + { pattern: /(refresh_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" }, + { pattern: /(api[_-]?key\s*[:=]\s*['"]?)([A-Za-z0-9._-]{12,})/gi, replacement: "$1[REDACTED]" }, + { pattern: /sk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replacement: "sk-[REDACTED]" }, + { pattern: /ghp_[A-Za-z0-9]{16,}/g, replacement: "ghp_[REDACTED]" }, + { pattern: /gho_[A-Za-z0-9]{16,}/g, replacement: "gho_[REDACTED]" }, + { pattern: /AKIA[A-Z0-9]{16}/g, replacement: "AKIA[REDACTED]" }, +]; + +export function redactTokens(text: string): string { + let result = text; + for (const { pattern, replacement } of PATTERNS) { + result = result.replace(pattern, replacement); + } + return result; +} + +function redactArg(arg: unknown): unknown { + if (typeof arg === "string") return redactTokens(arg); + if (arg instanceof Error) { + const redacted = new Error(redactTokens(arg.message)); + redacted.name = arg.name; + if (arg.stack) redacted.stack = redactTokens(arg.stack); + return redacted; + } + return arg; +} + +interface InstallTarget { + console: { log: typeof console.log; warn: typeof console.warn; error: typeof console.error; info: typeof console.info }; + stdoutWrite: typeof process.stdout.write; + stderrWrite: typeof process.stderr.write; +} + +export function installLogRedactor(target?: Partial): () => void { + const cons = target?.console ?? console; + const origLog = cons.log.bind(cons); + const origWarn = cons.warn.bind(cons); + const origError = cons.error.bind(cons); + const origInfo = cons.info.bind(cons); + + cons.log = (...args: unknown[]) => origLog(...args.map(redactArg)); + cons.warn = (...args: unknown[]) => origWarn(...args.map(redactArg)); + cons.error = (...args: unknown[]) => origError(...args.map(redactArg)); + cons.info = (...args: unknown[]) => origInfo(...args.map(redactArg)); + + const stdoutWriteOrig = (target?.stdoutWrite ?? process.stdout.write).bind(process.stdout); + const stderrWriteOrig = (target?.stderrWrite ?? process.stderr.write).bind(process.stderr); + process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => { + if (typeof chunk === "string") return stdoutWriteOrig(redactTokens(chunk), ...rest as never[]); + return stdoutWriteOrig(chunk as never, ...rest as never[]); + }) as typeof process.stdout.write; + process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => { + if (typeof chunk === "string") return stderrWriteOrig(redactTokens(chunk), ...rest as never[]); + return stderrWriteOrig(chunk as never, ...rest as never[]); + }) as typeof process.stderr.write; + + return () => { + cons.log = origLog; + cons.warn = origWarn; + cons.error = origError; + cons.info = origInfo; + process.stdout.write = stdoutWriteOrig; + process.stderr.write = stderrWriteOrig; + }; +} diff --git a/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx b/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx index 3c9beb73..2f731779 100644 --- a/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx +++ b/src/renderer/components/execution/approval-inspector/SchedulePicker.tsx @@ -25,7 +25,7 @@ export function SchedulePicker({ onScheduledAtChange: (value: string) => void onClearError: () => void }) { - const minDatetime = useMemo(computeMinDatetime, []) + const minDatetime = useMemo(() => computeMinDatetime(), []) return (
diff --git a/src/renderer/components/ui/AttentionRow.tsx b/src/renderer/components/ui/AttentionRow.tsx index 06a0c2b1..8f203568 100644 --- a/src/renderer/components/ui/AttentionRow.tsx +++ b/src/renderer/components/ui/AttentionRow.tsx @@ -1,6 +1,6 @@ import type { ElementType, ReactNode } from "react"; import { motion } from "framer-motion"; -import { ArrowRight } from "@phosphor-icons/react"; +import { ArrowRight } from "./icons"; const ATTENTION_STAGGER_MAX = 8; const ATTENTION_STAGGER_STEP = 0.04; diff --git a/src/renderer/components/ui/feedback.tsx b/src/renderer/components/ui/feedback.tsx index b31a70b5..ddeaf522 100644 --- a/src/renderer/components/ui/feedback.tsx +++ b/src/renderer/components/ui/feedback.tsx @@ -1,6 +1,6 @@ import { useEffect, type ReactNode } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { Trash, XCircle } from "@phosphor-icons/react"; +import { Trash, XCircle } from "./icons"; import { ActionButton } from "./primitives"; import { useFocusTrap } from "./hooks"; diff --git a/src/shared/contracts/schemas-agent-event.ts b/src/shared/contracts/schemas-agent-event.ts new file mode 100644 index 00000000..4dcf991f --- /dev/null +++ b/src/shared/contracts/schemas-agent-event.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +/** + * The renderer-bound `agent-event` push payload. + * + * The `event` payload is pi's AgentEvent (discriminated by `type`). We do not + * mirror pi's type system in Zod (it would couple the renderer to pi's + * internals); we validate the envelope and pass the event through opaquely. + */ +export const agentEventPushSchema = z.object({ + runId: z.string(), + agentId: z.string(), + companyId: z.string(), + taskId: z.string().nullable(), + emittedAt: z.string(), + event: z.object({ + type: z.string(), + }).passthrough(), +}); + +export type AgentEventPush = z.infer; diff --git a/src/shared/contracts/schemas-credentials.ts b/src/shared/contracts/schemas-credentials.ts new file mode 100644 index 00000000..daa90338 --- /dev/null +++ b/src/shared/contracts/schemas-credentials.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; + +export const credentialKindSchema = z.enum([ + "oauth", + "api_key", + "aws", + "azure", + "gcp_adc", +]); + +export const credentialStatusSchema = z.enum([ + "active", + "expired", + "revoked", + "error", + "not_configured", +]); + +export const apiKeyValueSchema = z.object({ + source: z.enum(["literal", "env"]), + value: z.string().min(1), +}); + +export const azureCredentialSchema = z.object({ + apiKey: z.string().min(1), + baseUrl: z.string().url().optional().or(z.literal("")), + resourceName: z.string().optional().or(z.literal("")), + apiVersion: z.string().optional().or(z.literal("")), + deploymentMap: z.record(z.string(), z.string()).optional(), +}); + +export const awsCredentialSchema = z.object({ + authMode: z.enum(["profile", "iam", "bearer"]), + region: z.string().min(1).default("us-east-1"), + awsProfile: z.string().optional(), + accessKeyId: z.string().optional(), + secretAccessKey: z.string().optional(), + sessionToken: z.string().optional(), + bearerToken: z.string().optional(), + forceCache: z.boolean().optional(), + endpointUrl: z.string().optional(), + skipAuth: z.boolean().optional(), + forceHttp1: z.boolean().optional(), +}); + +export const gcpAdcCredentialSchema = z.object({ + project: z.string().min(1), + location: z.string().min(1), + credentialsPath: z.string().optional(), +}); + +export const credentialPayloadSchema = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("api_key"), value: apiKeyValueSchema }), + z.object({ kind: z.literal("azure"), value: azureCredentialSchema }), + z.object({ kind: z.literal("aws"), value: awsCredentialSchema }), + z.object({ kind: z.literal("gcp_adc"), value: gcpAdcCredentialSchema }), +]); + +export const saveCredentialInputSchema = z.object({ + connectorId: z.string().min(1), + payload: credentialPayloadSchema, +}); + +export const removeCredentialInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const testCredentialInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const credentialSummarySchema = z.object({ + connectorId: z.string(), + kind: credentialKindSchema, + displayHint: z.string(), + status: credentialStatusSchema, + expiresAt: z.string().nullable(), + lastRefreshedAt: z.string().nullable(), + lastError: z.string().nullable(), + updatedAt: z.string().nullable(), +}); + +export type CredentialSummary = z.infer; +export type CredentialPayload = z.infer; +export type SaveCredentialInput = z.infer; +export type AwsCredential = z.infer; +export type AzureCredential = z.infer; +export type GcpAdcCredential = z.infer; +export type ApiKeyValue = z.infer; diff --git a/src/shared/contracts/schemas-oauth.ts b/src/shared/contracts/schemas-oauth.ts new file mode 100644 index 00000000..60e51d2f --- /dev/null +++ b/src/shared/contracts/schemas-oauth.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +export const oauthStartInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const oauthCancelInputSchema = z.object({ + connectorId: z.string().min(1), +}); + +export const oauthPromptResponseSchema = z.object({ + connectorId: z.string().min(1), + requestId: z.string().min(1), + value: z.string(), +}); + +export const oauthManualCodeResponseSchema = z.object({ + connectorId: z.string().min(1), + requestId: z.string().min(1), + code: z.string().min(1), +}); + +export const oauthAuthInfoSchema = z.object({ + connectorId: z.string(), + url: z.string().url(), + instructions: z.string().optional(), +}); + +export const oauthProgressSchema = z.object({ + connectorId: z.string(), + message: z.string(), +}); + +export const oauthStatusSchema = z.object({ + connectorId: z.string(), + status: z.enum(["active", "cancelled", "error", "in_progress"]), + errorMessage: z.string().optional(), +}); + +export const oauthPromptRequestSchema = z.object({ + connectorId: z.string(), + requestId: z.string(), + message: z.string(), + placeholder: z.string().optional(), + allowEmpty: z.boolean().optional(), +}); + +export const oauthManualCodeRequestSchema = z.object({ + connectorId: z.string(), + requestId: z.string(), +}); + +export type OAuthStartInput = z.infer; +export type OAuthAuthInfoEvent = z.infer; +export type OAuthProgressEvent = z.infer; +export type OAuthStatusEvent = z.infer; +export type OAuthPromptRequestEvent = z.infer; +export type OAuthManualCodeRequestEvent = z.infer; +export type OAuthPromptResponseInput = z.infer; +export type OAuthManualCodeResponseInput = z.infer; diff --git a/src/shared/types/records-core.ts b/src/shared/types/records-core.ts index 172ee743..2ae3ca37 100644 --- a/src/shared/types/records-core.ts +++ b/src/shared/types/records-core.ts @@ -6,6 +6,7 @@ import type { WorkspaceState, DepartmentId, AgentStatus, + AgentRoleLevel, GoalStatus, ProjectStatus, TaskStatus, @@ -73,6 +74,8 @@ export interface AgentRecord { name: string; role: string; title: string; + /** Seniority enum used for leadership detection; free-text `role` remains UI-editable. */ + roleLevel: AgentRoleLevel; department: DepartmentId | null; status: AgentStatus; reportsTo: string | null; diff --git a/tests/integration/agent-model-switching.spec.ts b/tests/integration/agent-model-switching.spec.ts index df0c4b0e..7f8bc5ee 100644 --- a/tests/integration/agent-model-switching.spec.ts +++ b/tests/integration/agent-model-switching.spec.ts @@ -71,7 +71,7 @@ describe("agent model switching", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -98,7 +98,7 @@ describe("agent model switching", () => { title: "Frontend Engineer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Build frontend features", @@ -147,7 +147,7 @@ describe("agent model switching", () => { title: "Frontend Engineer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.3-codex", capabilities: "Build frontend features", diff --git a/tests/integration/api-recursive-org-execution.spec.ts b/tests/integration/api-recursive-org-execution.spec.ts index 10a48c23..c90ed7ba 100644 --- a/tests/integration/api-recursive-org-execution.spec.ts +++ b/tests/integration/api-recursive-org-execution.spec.ts @@ -25,7 +25,7 @@ async function createHarness() { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -52,7 +52,7 @@ async function createHarness() { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -65,7 +65,7 @@ async function createHarness() { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -81,7 +81,7 @@ async function createHarness() { priority: "high", status: "in_progress", }); - const run = db.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId: ctoId, workspaceId, connectorId: "openai-codex" }); const token = createAgentToken(ctoId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); const port = await server.start(); @@ -114,7 +114,7 @@ describe("recursive org execution", () => { role: "Engineer", title: "Frontend Engineer", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "React, TypeScript, Tailwind, frontend delivery", @@ -176,7 +176,7 @@ describe("recursive org execution", () => { title: "Director of People", status: "active", reportsTo: harness.ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Coordinate staffing", @@ -197,7 +197,7 @@ describe("recursive org execution", () => { taskId: hrTaskId, agentId: hrId, workspaceId: harness.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const hrToken = createAgentToken(hrId, harness.companyId, hrRun.id); @@ -213,7 +213,7 @@ describe("recursive org execution", () => { role: "Backend Engineer", title: "Backend Engineer Golang", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Go, PostgreSQL, distributed systems", @@ -243,7 +243,7 @@ describe("recursive org execution", () => { role: "Backend Engineer", title: "Backend Engineer Golang", department: "engineering", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Go, APIs", @@ -270,7 +270,7 @@ describe("recursive org execution", () => { title: "Chief Marketing Officer", status: "active", reportsTo: harness.ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Lead marketing", @@ -287,7 +287,7 @@ describe("recursive org execution", () => { title: "Misrouted Engineer", department: "engineering", reportsTo: cmoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: harness.workspaceId, model: null, capabilities: "Should not report outside the CTO branch", diff --git a/tests/integration/api-run-events.spec.ts b/tests/integration/api-run-events.spec.ts index a1bf8498..fc335d41 100644 --- a/tests/integration/api-run-events.spec.ts +++ b/tests/integration/api-run-events.spec.ts @@ -40,7 +40,7 @@ async function createHarness() { title: "Lead agent", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Operate the company", @@ -55,7 +55,7 @@ async function createHarness() { priority: "high", status: "todo", }); - const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "openai-codex" }); const token = createAgentToken(agentId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); const port = await server.start(); diff --git a/tests/integration/autonomous-chain/approval-decisions.spec.ts b/tests/integration/autonomous-chain/approval-decisions.spec.ts index a631269b..e42f2daf 100644 --- a/tests/integration/autonomous-chain/approval-decisions.spec.ts +++ b/tests/integration/autonomous-chain/approval-decisions.spec.ts @@ -65,7 +65,7 @@ describe("Step 4: Hire approval + decideApproval", () => { type: "hire_agent", payloadSummary: "Hire CTO", impactSummary: "Test", - payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "codex_local" } }), + payloadJson: JSON.stringify({ agentId: hireId, config: { connector: "openai-codex" } }), }); const approval = shared.db.getApprovalRecord(approvalId); diff --git a/tests/integration/autonomous-chain/automation-messages.spec.ts b/tests/integration/autonomous-chain/automation-messages.spec.ts index 3aba169b..074d292c 100644 --- a/tests/integration/autonomous-chain/automation-messages.spec.ts +++ b/tests/integration/autonomous-chain/automation-messages.spec.ts @@ -91,13 +91,13 @@ describe("Agent Messages", () => { beforeEach(() => { agentAId = shared.db.saveAgent({ companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer", - department: "engineering", connectorId: "claude_local", status: "active", + department: "engineering", connectorId: "claude-pro", status: "active", capabilities: "Backend development, API design, code review", budgetMonthlyUsd: 100, }); agentBId = shared.db.saveAgent({ companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer", - department: "design", connectorId: "claude_local", status: "active", + department: "design", connectorId: "claude-pro", status: "active", capabilities: "UI design, wireframing, user research", budgetMonthlyUsd: 100, }); @@ -242,13 +242,13 @@ describe("Auto Task Assignment", () => { beforeEach(() => { backendId = shared.db.saveAgent({ companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend", - department: "engineering", connectorId: "codex_local", status: "active", + department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "API development, database design, Node.js, PostgreSQL, backend testing", budgetMonthlyUsd: 200, }); frontendId = shared.db.saveAgent({ companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend", - department: "engineering", connectorId: "codex_local", status: "active", + department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components", budgetMonthlyUsd: 200, }); @@ -290,8 +290,8 @@ describe("Auto Task Assignment", () => { it("returns null if all agents have exhausted their budget", () => { // Set positive budgets that are fully spent - shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "codex_local", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); - shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "codex_local", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); + shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); + shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); // Exhaust budgets by recording spending via direct SQL (since finishRun would need a real run) (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(backendId); (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(frontendId); diff --git a/tests/integration/autonomous-chain/crash-recovery.spec.ts b/tests/integration/autonomous-chain/crash-recovery.spec.ts index e9b9a9f5..2b702306 100644 --- a/tests/integration/autonomous-chain/crash-recovery.spec.ts +++ b/tests/integration/autonomous-chain/crash-recovery.spec.ts @@ -28,7 +28,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); @@ -66,7 +66,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); diff --git a/tests/integration/autonomous-chain/run-failure.spec.ts b/tests/integration/autonomous-chain/run-failure.spec.ts index f3ce65d3..7479943b 100644 --- a/tests/integration/autonomous-chain/run-failure.spec.ts +++ b/tests/integration/autonomous-chain/run-failure.spec.ts @@ -27,7 +27,7 @@ describe("Step 6: Run failure escalation (database layer)", () => { taskId, agentId, workspaceId: shared.workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); shared.db.markRunStatus(run.id, "running", "Started"); diff --git a/tests/integration/autonomous-chain/shared-setup.ts b/tests/integration/autonomous-chain/shared-setup.ts index 967427e0..72d1abc3 100644 --- a/tests/integration/autonomous-chain/shared-setup.ts +++ b/tests/integration/autonomous-chain/shared-setup.ts @@ -88,7 +88,7 @@ export function installSharedSetup(): SharedHandle { title: "Test", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: state.workspaceId, model: null, capabilities: "Test capabilities", diff --git a/tests/integration/connector-model-enforcement.spec.ts b/tests/integration/connector-model-enforcement.spec.ts index 7ab84312..2f914b0d 100644 --- a/tests/integration/connector-model-enforcement.spec.ts +++ b/tests/integration/connector-model-enforcement.spec.ts @@ -70,7 +70,7 @@ describe("connector model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -97,7 +97,7 @@ describe("connector model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Lead the company", @@ -113,7 +113,7 @@ describe("connector model enforcement", () => { status: "todo", }); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); const result = taskRunner.queueTaskRun(taskId, "manual", "board"); @@ -134,7 +134,7 @@ describe("connector model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -161,7 +161,7 @@ describe("connector model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", capabilities: "Lead the company", @@ -182,12 +182,12 @@ describe("connector model enforcement", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const checkoutSuccess = db.checkoutTask(taskId, run.id); expect(checkoutSuccess).toBe(true); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); taskRunner.resumeQueuedRuns(); diff --git a/tests/integration/cross-subsystem.spec.ts b/tests/integration/cross-subsystem.spec.ts index f5d22eae..79f4c97a 100644 --- a/tests/integration/cross-subsystem.spec.ts +++ b/tests/integration/cross-subsystem.spec.ts @@ -52,10 +52,10 @@ const mockLogger = { /** * Creates a full entity scaffold with a budget policy: - * company, workspace, connector (gemini_local), agent, goal, project, budget policy. + * company, workspace, connector (gemini-cli), agent, goal, project, budget policy. */ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { - const connectorId: ConnectorId = "gemini_local"; + const connectorId: ConnectorId = "gemini-cli"; db.saveConnector({ id: connectorId, @@ -228,7 +228,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -387,7 +387,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); diff --git a/tests/integration/database.spec.ts b/tests/integration/database.spec.ts index 511f5376..34ac0b92 100644 --- a/tests/integration/database.spec.ts +++ b/tests/integration/database.spec.ts @@ -1,7 +1,21 @@ import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +vi.mock("electron", () => ({ + safeStorage: { + isEncryptionAvailable: () => false, + getSelectedStorageBackend: () => "basic_text", + encryptString: (value: string) => Buffer.from(value, "utf8"), + decryptString: (value: Buffer) => value.toString("utf8"), + }, +})); + import { AppDatabase } from "../../src/main/database"; const tempDirs: string[] = []; @@ -45,7 +59,7 @@ describe("AppDatabase", () => { title: "Lead agent", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Operate the company", @@ -89,7 +103,7 @@ describe("AppDatabase", () => { }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: null, envBindingText: "OPENAI_API_KEY=vault-openai", @@ -105,7 +119,7 @@ describe("AppDatabase", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); await db.appendRunLog(run.id, "stdout", "hello from run\n"); @@ -190,7 +204,7 @@ describe("AppDatabase", () => { title: "", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: alphaWorkspaceId, model: null, capabilities: "Lead alpha", @@ -204,7 +218,7 @@ describe("AppDatabase", () => { title: "", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: betaWorkspaceId, model: null, capabilities: "Lead beta", diff --git a/tests/integration/document-run-provenance.spec.ts b/tests/integration/document-run-provenance.spec.ts index dcd83266..2f4208a5 100644 --- a/tests/integration/document-run-provenance.spec.ts +++ b/tests/integration/document-run-provenance.spec.ts @@ -26,7 +26,7 @@ async function createHarness() { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -53,7 +53,7 @@ async function createHarness() { title: "Research Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Research and synthesis", @@ -86,7 +86,7 @@ async function createHarness() { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const token = createAgentToken(agentId, companyId, run.id); const server = createApiServer(db, () => {}, () => null); diff --git a/tests/integration/onboarding-model-pool.spec.ts b/tests/integration/onboarding-model-pool.spec.ts index 4b37c628..9ea4edcb 100644 --- a/tests/integration/onboarding-model-pool.spec.ts +++ b/tests/integration/onboarding-model-pool.spec.ts @@ -6,6 +6,16 @@ import { AppDatabase } from "../../src/main/database"; import { createOnboardingService } from "../../src/main/onboarding-service"; import { buildRunPayload } from "../../src/main/orchestration/turn-context"; import { getConnectorDefinition } from "../../src/main/connectors"; +import { getModels } from "@mariozechner/pi-ai"; + +function listProviderModels(provider: string): Array<{ id: string; label: string }> { + try { + const models = getModels(provider as never); + return models.map((model) => ({ id: model.id, label: model.name ?? model.id })); + } catch { + return []; + } +} const tempDirs: string[] = []; @@ -80,22 +90,22 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); - const enabledModel = codexDefinition.module.models?.[0]?.id; - const disabledModel = codexDefinition.module.models?.[1]?.id; + const codexDefinition = getConnectorDefinition("openai-codex"); + const enabledModel = listProviderModels(codexDefinition.provider)[0]?.id; + const disabledModel = listProviderModels(codexDefinition.provider)[1]?.id; if (!enabledModel || !disabledModel) { - throw new Error("codex_local requires at least two models for this integration test."); + throw new Error("openai-codex requires at least two models for this integration test."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]); + db.setDisabledModels("openai-codex", [{ id: disabledModel, label: disabledModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Model Governance Co", @@ -106,7 +116,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch runtime-safe company", goalDescription: "Ensure onboarding connector models flow into runtime decisions.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: enabledModel, }); @@ -115,9 +125,9 @@ describe("onboarding model pool integration", () => { } const agent = db.getAgent(summary.leadAgentId); - expect(agent.connectorId).toBe("codex_local"); + expect(agent.connectorId).toBe("openai-codex"); expect(agent.model).toBe(enabledModel); - expect(db.getDisabledModels("codex_local")).toEqual([{ id: disabledModel, label: disabledModel }]); + expect(db.getDisabledModels("openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); expect(db.getCurrentCompanyId()).toBe(summary.companyId); const payload = buildKickoffPayload(db, summary); @@ -128,7 +138,7 @@ describe("onboarding model pool integration", () => { expect(systemPrompt).toContain(enabledModel); const codexConnectorLine = systemPrompt .split("\n") - .find((line) => line.includes("**Codex**")); + .find((line) => line.includes("ChatGPT Plus/Pro (Codex)")); expect(codexConnectorLine).toBeDefined(); expect(codexConnectorLine).not.toContain(`, ${disabledModel},`); expect(codexConnectorLine).not.toContain(` ${disabledModel}.`); @@ -136,46 +146,53 @@ describe("onboarding model pool integration", () => { db.close(); }); - it("uses compat connector csv models minus disabled entries in runtime prompt context", async () => { + it("excludes disabled pi-ai models from anthropic connector runtime prompt context", async () => { const profileDir = await createTempDir("agentcompany-onboarding-compat-"); const workspaceDir = await createTempDir("agentcompany-onboarding-compat-workspace-"); const db = new AppDatabase(profileDir); db.init(); - db.saveConnector({ - id: "claude_compatible", - command: "claude", - model: "model-alpha,model-beta,model-gamma", - envBindingText: "", - notes: "Custom provider", - }); + const anthropicDefinition = getConnectorDefinition("anthropic"); + const anthropicModels = listProviderModels(anthropicDefinition.provider); + const enabledModel = anthropicModels[0]?.id; + const disabledModel = anthropicModels[1]?.id; + if (!enabledModel || !disabledModel) { + throw new Error("anthropic provider must expose at least two models from pi-ai."); + } + db.updateConnectorHealth({ - id: "claude_compatible", + id: "anthropic", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("claude_compatible", [{ id: "model-beta", label: "model-beta" }]); + db.setDisabledModels("anthropic", [{ id: disabledModel, label: disabledModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ - companyName: "Compat Governance Co", - companyDescription: "Tests compat model governance", + companyName: "Anthropic Governance Co", + companyDescription: "Tests anthropic model governance", workspacePath: workspaceDir, autoApproveHires: true, reviewDeliverables: false, executionMode: "ai_ceo", - goalTitle: "Launch compat company", - goalDescription: "Ensure compat model pools survive onboarding and runtime prompt assembly.", - connectorId: "claude_compatible", - connectorModel: "model-alpha", + goalTitle: "Launch anthropic company", + goalDescription: "Ensure anthropic model pools survive onboarding and runtime prompt assembly.", + connectorId: "anthropic", + connectorModel: enabledModel, }); const payload = buildKickoffPayload(db, summary); expect(payload.systemPrompt).not.toBeNull(); const systemPrompt = payload.systemPrompt ?? ""; - expect(systemPrompt).toContain("Enabled models: model-alpha, model-gamma."); - expect(systemPrompt).not.toContain("model-beta"); + expect(systemPrompt).toContain("Enabled models:"); + expect(systemPrompt).toContain(enabledModel); + const anthropicLine = systemPrompt + .split("\n") + .find((line) => line.includes(anthropicDefinition.label)); + expect(anthropicLine).toBeDefined(); + expect(anthropicLine).not.toContain(`, ${disabledModel},`); + expect(anthropicLine).not.toContain(` ${disabledModel}.`); db.close(); }); @@ -187,7 +204,7 @@ describe("onboarding model pool integration", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -203,7 +220,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch default-mode company", goalDescription: "Leave the CEO model unset so runtime falls back to connector defaults.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: "", }); @@ -215,7 +232,7 @@ describe("onboarding model pool integration", () => { expect(agent.model).toBeNull(); const payload = buildKickoffPayload(db, summary); - const defaultModel = getConnectorDefinition("codex_local").defaultModel; + const defaultModel = getConnectorDefinition("openai-codex").defaultModel; expect(payload.model).toBe(defaultModel); db.close(); @@ -227,22 +244,22 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); + const codexDefinition = getConnectorDefinition("openai-codex"); const defaultModel = codexDefinition.defaultModel; - const fallbackModel = codexDefinition.module.models?.find((model) => model.id !== defaultModel)?.id; + const fallbackModel = listProviderModels(codexDefinition.provider).find((model) => model.id !== defaultModel)?.id; if (!defaultModel || !fallbackModel) { - throw new Error("codex_local requires a default and fallback model for reroute testing."); + throw new Error("openai-codex requires a default and fallback model for reroute testing."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: defaultModel, label: defaultModel }]); + db.setDisabledModels("openai-codex", [{ id: defaultModel, label: defaultModel }]); const summary = createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Default Reroute Co", @@ -253,7 +270,7 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Launch rerouted company", goalDescription: "Ensure runtime picks the first enabled model when connector default is disabled.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: "", }); @@ -269,21 +286,21 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - const codexDefinition = getConnectorDefinition("codex_local"); - const disabledModel = codexDefinition.module.models?.[0]?.id; + const codexDefinition = getConnectorDefinition("openai-codex"); + const disabledModel = listProviderModels(codexDefinition.provider)[0]?.id; if (!disabledModel) { - throw new Error("codex_local requires a model to validate onboarding rejection."); + throw new Error("openai-codex requires a model to validate onboarding rejection."); } db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: disabledModel, label: disabledModel }]); + db.setDisabledModels("openai-codex", [{ id: disabledModel, label: disabledModel }]); expect(() => createBootstrapHarness(db).bootstrapOnboarding({ companyName: "Rejected Model Co", @@ -294,9 +311,9 @@ describe("onboarding model pool integration", () => { executionMode: "ai_ceo", goalTitle: "Reject disabled model bootstrap", goalDescription: "Invalid explicit models should fail before the CEO is created.", - connectorId: "codex_local", + connectorId: "openai-codex", connectorModel: disabledModel, - })).toThrow(`Codex does not allow model "${disabledModel}".`); + })).toThrow(`${codexDefinition.label} does not allow model "${disabledModel}".`); db.close(); }); diff --git a/tests/integration/pi-mono-migrations.spec.ts b/tests/integration/pi-mono-migrations.spec.ts new file mode 100644 index 00000000..bb40ae73 --- /dev/null +++ b/tests/integration/pi-mono-migrations.spec.ts @@ -0,0 +1,153 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; + +beforeAll(() => { + // SecretVault gated for test contexts. Used by the credentials path + // touched by migration 28's downstream consumers. + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +const tempDirs: string[] = []; + +async function freshProfile(prefix: string) { + const dir = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +/** + * Migration 27 (`session_entries`) and 28 (`credentials`) ship in the rebuild. + * These tests verify both migrations: + * - run cleanly on a fresh SQLite profile; + * - are idempotent on subsequent `init()` calls (existing-user upgrade); + * - leave their tables queryable. + */ +describe("pi-mono migrations 27 + 28", () => { + it("creates session_entries on first init and accepts inserts", async () => { + const dir = await freshProfile("agentcompany-migration-27-fresh-"); + const db = new AppDatabase(dir); + db.init(); + + db.sessionEntries.append({ + taskSessionId: "test-session", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: "hello" }), + }); + const rows = db.sessionEntries.listActive("test-session"); + expect(rows).toHaveLength(1); + expect(rows[0]?.kind).toBe("message"); + db.close(); + }); + + it("creates credentials on first init and accepts encrypted inserts", async () => { + const dir = await freshProfile("agentcompany-migration-28-fresh-"); + const db = new AppDatabase(dir); + db.init(); + + const cipher = db.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-test-1234567890" })); + db.credentialsTable.upsert({ + connectorId: "test-anthropic", + kind: "api_key", + backend: cipher.backend, + ciphertext: cipher.payload, + displayHint: "sk-test-...7890", + status: "active", + expiresAt: null, + lastRefreshedAt: null, + lastError: null, + }); + + const rows = db.credentialsTable.list(); + expect(rows.some((row) => row.connectorId === "test-anthropic")).toBe(true); + db.close(); + }); + + it("re-running init() on an existing profile does not re-apply migrations or duplicate rows", async () => { + const dir = await freshProfile("agentcompany-migration-idempotent-"); + const dbA = new AppDatabase(dir); + dbA.init(); + dbA.sessionEntries.append({ + taskSessionId: "session-x", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: "first" }), + }); + const cipher = dbA.vault.encrypt(JSON.stringify({ source: "literal", value: "sk-keep-me" })); + dbA.credentialsTable.upsert({ + connectorId: "test-keep", + kind: "api_key", + backend: cipher.backend, + ciphertext: cipher.payload, + displayHint: "sk-keep-...e", + status: "active", + expiresAt: null, + lastRefreshedAt: null, + lastError: null, + }); + dbA.close(); + + const dbB = new AppDatabase(dir); + dbB.init(); + expect(dbB.sessionEntries.listActive("session-x")).toHaveLength(1); + expect(dbB.credentialsTable.list().filter((row) => row.connectorId === "test-keep")).toHaveLength(1); + dbB.close(); + }); + + it("session_entries respects the unique (task_session_id, sequence) index across replace cycles", async () => { + const dir = await freshProfile("agentcompany-migration-replace-"); + const db = new AppDatabase(dir); + db.init(); + + for (let i = 0; i < 3; i += 1) { + db.sessionEntries.append({ + taskSessionId: "replace-test", + kind: "message", + role: "user", + payloadJson: JSON.stringify({ role: "user", content: `t${i}` }), + }); + } + db.sessionEntries.replace({ + taskSessionId: "replace-test", + entries: [ + { kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: "summarised" }) }, + { kind: "message", role: "user", payloadJson: JSON.stringify({ role: "user", content: "post" }) }, + ], + }); + + const active = db.sessionEntries.listActive("replace-test"); + expect(active).toHaveLength(2); + expect(active[0]?.kind).toBe("compaction"); + expect(active[1]?.kind).toBe("message"); + + const all = db.sessionEntries.listAll("replace-test"); + expect(all.length).toBe(5); + expect(all.filter((row) => row.isSuperseded).length).toBe(3); + db.close(); + }); + + it("seedConnectors prunes legacy connector rows and inserts the full pi-mono + Fireworks catalog", async () => { + const dir = await freshProfile("agentcompany-migration-seed-"); + const db = new AppDatabase(dir); + db.init(); + + const rows = db.listSnapshot().connectors.map((entry) => entry.id); + expect(rows.length).toBe(26); + expect(rows).toContain("claude-pro"); + expect(rows).toContain("amazon-bedrock"); + expect(rows).toContain("google-vertex"); + expect(rows).toContain("fireworks"); + expect(rows).toContain("fireworks-firepass"); + expect(rows.find((id) => (id as string) === "claude_local")).toBeUndefined(); + expect(rows.find((id) => (id as string) === "codex_local")).toBeUndefined(); + expect(rows.find((id) => (id as string) === "claude_compatible")).toBeUndefined(); + db.close(); + }); +}); diff --git a/tests/integration/process-recovery.spec.ts b/tests/integration/process-recovery.spec.ts index 2eeebfd5..7133360b 100644 --- a/tests/integration/process-recovery.spec.ts +++ b/tests/integration/process-recovery.spec.ts @@ -51,7 +51,7 @@ interface ScaffoldResult { /** Creates a full entity scaffold: company, workspace, connector, agent, goal, project. */ function scaffold(db: AppDatabase): ScaffoldResult { - const connectorId: ConnectorId = "gemini_local"; + const connectorId: ConnectorId = "gemini-cli"; // Connector db.saveConnector({ @@ -445,7 +445,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { taskId, companyId: s.companyId, agentId: s.agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -466,7 +466,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { const rotationResult = sessionService.evaluateRotation( session!, - "gemini_local", + "gemini-cli", { maxRuns: 1 }, ); expect(rotationResult.shouldRotate).toBe(true); diff --git a/tests/integration/skill-aware-assignment.spec.ts b/tests/integration/skill-aware-assignment.spec.ts index d7b0913a..f82b4b67 100644 --- a/tests/integration/skill-aware-assignment.spec.ts +++ b/tests/integration/skill-aware-assignment.spec.ts @@ -25,7 +25,7 @@ describe("skill-aware capability registry", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -53,7 +53,7 @@ describe("skill-aware capability registry", () => { title: "Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "General software delivery", @@ -66,7 +66,7 @@ describe("skill-aware capability registry", () => { title: "Frontend Specialist", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "General software delivery", @@ -116,7 +116,7 @@ describe("skill-aware capability registry", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -143,7 +143,7 @@ describe("skill-aware capability registry", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -193,7 +193,7 @@ describe("skill-aware capability registry", () => { recentComments: [], socialAccounts: [], allConnectors: snapshot.connectors - .filter((connector) => connector.id === "codex_local") + .filter((connector) => connector.id === "openai-codex") .map((connector) => ({ id: connector.id, label: connector.label, status: connector.status, enabledModels: ["gpt-5.4"] })), companySkillCatalog: db.listCompanySkills(companyId).map((skill) => ({ name: skill.name, diff --git a/tests/integration/thread-wait-heartbeat.spec.ts b/tests/integration/thread-wait-heartbeat.spec.ts index 2567eef1..4bb44213 100644 --- a/tests/integration/thread-wait-heartbeat.spec.ts +++ b/tests/integration/thread-wait-heartbeat.spec.ts @@ -29,7 +29,7 @@ function createNoopLogger() { function createScaffold(db: AppDatabase) { db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -57,7 +57,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -70,7 +70,7 @@ function createScaffold(db: AppDatabase) { title: "Lead Engineer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ship features", @@ -140,7 +140,7 @@ describe("thread wait + heartbeat integration", () => { title: "Chief Marketing Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: db.getTask(taskId).workspaceId ?? null, model: null, capabilities: "Drive marketing work", diff --git a/tests/unit/agent-budget-policy-sync.test.ts b/tests/unit/agent-budget-policy-sync.test.ts index b4c36e21..23898bbc 100644 --- a/tests/unit/agent-budget-policy-sync.test.ts +++ b/tests/unit/agent-budget-policy-sync.test.ts @@ -28,7 +28,7 @@ describe("agent budget policy synchronization", () => { status: "active", }); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -49,7 +49,7 @@ describe("agent budget policy synchronization", () => { title: "Backend Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build backend services", @@ -78,7 +78,7 @@ describe("agent budget policy synchronization", () => { title: "Backend Engineer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build backend services", diff --git a/tests/unit/agent-hire-routing.test.ts b/tests/unit/agent-hire-routing.test.ts index 2a615dd1..dd6b44b6 100644 --- a/tests/unit/agent-hire-routing.test.ts +++ b/tests/unit/agent-hire-routing.test.ts @@ -29,7 +29,7 @@ describe("hire routing guards", () => { status: "active", }); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -50,7 +50,7 @@ describe("hire routing guards", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("hire routing guards", () => { title: "Head of HR", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Run hiring operations", @@ -78,7 +78,7 @@ describe("hire routing guards", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", diff --git a/tests/unit/agent-permissions.test.ts b/tests/unit/agent-permissions.test.ts index c853af93..59c9976b 100644 --- a/tests/unit/agent-permissions.test.ts +++ b/tests/unit/agent-permissions.test.ts @@ -51,7 +51,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("canManageOrganization", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -78,7 +78,7 @@ describe("canManageOrganization", () => { title: "Software Engineer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build features", @@ -120,7 +120,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -133,7 +133,7 @@ describe("canManageOrganization", () => { title: "Operations Specialist", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Coordinate operations", @@ -174,7 +174,7 @@ describe("canManageOrganization", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -187,7 +187,7 @@ describe("canManageOrganization", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -201,7 +201,7 @@ describe("canManageOrganization", () => { title: "Chief Marketing Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead marketing", @@ -215,7 +215,7 @@ describe("canManageOrganization", () => { title: "Frontend Engineer", status: "active", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Build frontend features", diff --git a/tests/unit/agent-schema-validation.test.ts b/tests/unit/agent-schema-validation.test.ts index 08ff2bea..f2516e41 100644 --- a/tests/unit/agent-schema-validation.test.ts +++ b/tests/unit/agent-schema-validation.test.ts @@ -10,7 +10,7 @@ const baseHireRequest = { title: "Backend Engineer", department: "engineering" as const, reportsTo: null, - connectorId: "codex_local" as const, + connectorId: "openai-codex" as const, workspaceId: null, model: null, metadataJson: "{}", diff --git a/tests/unit/approval-hire-routing.test.ts b/tests/unit/approval-hire-routing.test.ts index c8c7fda9..f7d7d19a 100644 --- a/tests/unit/approval-hire-routing.test.ts +++ b/tests/unit/approval-hire-routing.test.ts @@ -24,7 +24,7 @@ describe("hire approval routing", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -51,7 +51,7 @@ describe("hire approval routing", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -64,7 +64,7 @@ describe("hire approval routing", () => { title: "Chief Technology Officer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -80,7 +80,7 @@ describe("hire approval routing", () => { title: "Frontend Engineer", department: "engineering", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, metadataJson: "{}", diff --git a/tests/unit/approval-hook.test.ts b/tests/unit/approval-hook.test.ts index d3968cd1..e4262750 100644 --- a/tests/unit/approval-hook.test.ts +++ b/tests/unit/approval-hook.test.ts @@ -49,7 +49,7 @@ describe("HumanApprovalHook", () => { title: "Eng", status: "running", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId, model: null, capabilities: "Build", diff --git a/tests/unit/approval-review-feedback-writeback.test.ts b/tests/unit/approval-review-feedback-writeback.test.ts index 4ba6972e..649f592e 100644 --- a/tests/unit/approval-review-feedback-writeback.test.ts +++ b/tests/unit/approval-review-feedback-writeback.test.ts @@ -24,7 +24,7 @@ describe("deliverable review feedback writeback", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -51,7 +51,7 @@ describe("deliverable review feedback writeback", () => { title: "Research Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Research and synthesis", diff --git a/tests/unit/atomic-checkout.test.ts b/tests/unit/atomic-checkout.test.ts index 2bfa7ef0..62fffd1d 100644 --- a/tests/unit/atomic-checkout.test.ts +++ b/tests/unit/atomic-checkout.test.ts @@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks", @@ -115,7 +115,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const result = db.checkoutTask(taskId, runId); @@ -137,14 +137,14 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const { id: runId2 } = db.createRun({ companyId, taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId1)).toBe(true); @@ -166,7 +166,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId)).toBe(false); @@ -184,7 +184,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -222,7 +222,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -251,7 +251,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); @@ -291,7 +291,7 @@ describe("CAS checkout", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); db.checkoutTask(taskId, runId); diff --git a/tests/unit/capability-registry.test.ts b/tests/unit/capability-registry.test.ts index bac01225..8b4b5f4c 100644 --- a/tests/unit/capability-registry.test.ts +++ b/tests/unit/capability-registry.test.ts @@ -11,7 +11,7 @@ describe("buildCapabilityRegistrySection", () => { { name: "Primary", localPath: "/tmp/workspace" }, ], allConnectors: [ - { id: "codex_local", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] }, + { id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"] }, ], companyDocuments: [ { @@ -38,7 +38,7 @@ describe("buildCapabilityRegistrySection", () => { expect(section).toContain("# Company Capability Registry"); expect(section).toContain("React Standards (`react-standards`)"); expect(section).toContain("Primary: /tmp/workspace"); - expect(section).toContain("Codex (`codex_local`) — models: gpt-5.4"); + expect(section).toContain("Codex (`openai-codex`) — models: gpt-5.4"); expect(section).toContain("architecture_decision"); }); }); diff --git a/tests/unit/command-paths.test.ts b/tests/unit/command-paths.test.ts index 6d710842..73a294bf 100644 --- a/tests/unit/command-paths.test.ts +++ b/tests/unit/command-paths.test.ts @@ -1,15 +1,10 @@ import { describe, expect, it } from "vitest"; -import { buildWindowsSpawnTarget, normalizeCommandValue } from "@agentcompany/adapter-utils/server-utils"; -import { commandLooksLike } from "@agentcompany/adapter-utils/test-utils"; +import { normalizeCommandValue } from "@main/connector-helpers"; describe("command path normalization", () => { - it("removes surrounding quotes from absolute executable paths", () => { - expect(normalizeCommandValue('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"')).toBe( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ); - expect(normalizeCommandValue("'C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe'")).toBe( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ); + it("trims surrounding whitespace from command values", () => { + expect(normalizeCommandValue(" claude ")).toBe("claude"); + expect(normalizeCommandValue("\tcodex\n")).toBe("codex"); }); it("keeps bare commands unchanged", () => { @@ -17,31 +12,8 @@ describe("command path normalization", () => { expect(normalizeCommandValue("codex")).toBe("codex"); }); - it("matches quoted commands against connector executables", () => { - expect(commandLooksLike('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"', "claude")).toBe(true); - expect(commandLooksLike("'C:\\Users\\First Last\\AppData\\Local\\bin\\codex.exe'", "codex")).toBe(true); - }); - - it("spawns direct exe paths on Windows instead of wrapping them through cmd.exe", () => { - const target = buildWindowsSpawnTarget( - "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - ["--version"], - "cmd.exe", - ); - expect(target).toEqual({ - command: "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe", - args: ["--version"], - }); - }); - - it("still wraps shell scripts through cmd.exe on Windows", () => { - const target = buildWindowsSpawnTarget( - "C:\\Tools\\agentcompany\\launch.cmd", - ["--version"], - "cmd.exe", - ); - expect(target.command).toBe("cmd.exe"); - expect(target.args[0]).toBe("/d"); - expect(target.args.at(-1)).toContain("launch.cmd"); + it("normalizes null and undefined to empty string", () => { + expect(normalizeCommandValue(null)).toBe(""); + expect(normalizeCommandValue(undefined)).toBe(""); }); }); diff --git a/tests/unit/company-mode-switch.test.ts b/tests/unit/company-mode-switch.test.ts index a6be8c23..5f957836 100644 --- a/tests/unit/company-mode-switch.test.ts +++ b/tests/unit/company-mode-switch.test.ts @@ -33,7 +33,7 @@ describe("company mode switching", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -63,7 +63,7 @@ describe("company mode switching", () => { title: "Chief Executive Officer", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -76,7 +76,7 @@ describe("company mode switching", () => { title: "Lead Engineer", status: "active", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ship features", diff --git a/tests/unit/connector-matching.test.ts b/tests/unit/connector-matching.test.ts index aa247130..6bed1932 100644 --- a/tests/unit/connector-matching.test.ts +++ b/tests/unit/connector-matching.test.ts @@ -3,38 +3,37 @@ import { inferTaskType, getConnectorStrengths, TASK_TYPE_PREFERENCE, - CONNECTOR_STRENGTHS as _CONNECTOR_STRENGTHS, ROLE_DEPARTMENT_HINTS as _ROLE_DEPARTMENT_HINTS, } from "../../src/main/connector-matching"; describe("connector-matching", () => { describe("TASK_TYPE_PREFERENCE", () => { - it("maps code tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude_local"); + it("maps code tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.code[0]).toBe("claude-pro"); }); - it("maps research tasks to gemini_local first", () => { - expect(TASK_TYPE_PREFERENCE.research[0]).toBe("gemini_local"); + it("maps research tasks to google first", () => { + expect(TASK_TYPE_PREFERENCE.research[0]).toBe("google"); }); - it("maps content tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude_local"); + it("maps content tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.content[0]).toBe("claude-pro"); }); - it("maps management tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude_local"); + it("maps management tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.management[0]).toBe("claude-pro"); }); - it("maps general tasks to claude_local first", () => { - expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude_local"); + it("maps general tasks to claude-pro first", () => { + expect(TASK_TYPE_PREFERENCE.general[0]).toBe("claude-pro"); }); it("includes all three core connectors for every task type", () => { for (const key of Object.keys(TASK_TYPE_PREFERENCE)) { const prefs = TASK_TYPE_PREFERENCE[key as keyof typeof TASK_TYPE_PREFERENCE]; - expect(prefs).toContain("claude_local"); - expect(prefs).toContain("codex_local"); - expect(prefs).toContain("gemini_local"); + expect(prefs).toContain("claude-pro"); + expect(prefs).toContain("openai-codex"); + expect(prefs).toContain("gemini-cli"); } }); }); @@ -102,22 +101,22 @@ describe("connector-matching", () => { }); describe("getConnectorStrengths", () => { - it("returns non-empty string for claude_local", () => { - const result = getConnectorStrengths("claude_local"); + it("returns non-empty string for claude-pro", () => { + const result = getConnectorStrengths("claude-pro"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); }); - it("returns non-empty string for codex_local", () => { - const result = getConnectorStrengths("codex_local"); + it("returns non-empty string for openai-codex", () => { + const result = getConnectorStrengths("openai-codex"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); }); - it("returns non-empty string for gemini_local", () => { - const result = getConnectorStrengths("gemini_local"); + it("returns non-empty string for gemini-cli", () => { + const result = getConnectorStrengths("gemini-cli"); expect(result).toBeTruthy(); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(10); diff --git a/tests/unit/connector-model-policy.test.ts b/tests/unit/connector-model-policy.test.ts index a428c1b2..af7d5a4c 100644 --- a/tests/unit/connector-model-policy.test.ts +++ b/tests/unit/connector-model-policy.test.ts @@ -8,72 +8,85 @@ function createDeps(overrides: { } = {}) { return { getConnector: () => ({ - id: "claude_compatible" as const, - label: "Claude Compatible", - model: overrides.connectorModel ?? "model-alpha,model-beta,model-gamma", + id: "anthropic" as const, + label: "Anthropic", + model: overrides.connectorModel ?? null, }), getDisabledModels: () => overrides.disabledModels ?? [], }; } describe("connector model policy", () => { - it("derives enabled compat models from connector config minus disabled entries", () => { - const policy = getConnectorModelPolicy( - createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }), - "claude_compatible", + it("excludes disabled models from the enabled set", () => { + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const allIds = policy.enabledModels.map((model) => model.id); + if (allIds.length === 0) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } + + const firstModel = allIds[0]; + const filteredPolicy = getConnectorModelPolicy( + createDeps({ disabledModels: [{ id: firstModel, label: firstModel }] }), + "anthropic", ); - expect(policy.enabledModels.map((model) => model.id)).toEqual(["model-alpha", "model-gamma"]); + expect(filteredPolicy.enabledModels.some((model) => model.id === firstModel)).toBe(false); }); it("accepts an explicit enabled model", () => { - const result = validateConnectorModelSelection(createDeps(), "claude_compatible", "model-alpha"); + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const enabledId = policy.enabledModels[0]?.id; + if (!enabledId) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } + const result = validateConnectorModelSelection(createDeps(), "anthropic", enabledId); - expect(result).toEqual({ ok: true, effectiveModel: "model-alpha" }); + expect(result).toEqual({ ok: true, effectiveModel: enabledId }); }); it("rejects an explicit disabled model", () => { + const policy = getConnectorModelPolicy(createDeps(), "anthropic"); + const enabledId = policy.enabledModels[0]?.id; + if (!enabledId) { + throw new Error("anthropic provider should expose at least one model from pi-ai."); + } const result = validateConnectorModelSelection( - createDeps({ disabledModels: [{ id: "model-beta", label: "model-beta" }] }), - "claude_compatible", - "model-beta", + createDeps({ disabledModels: [{ id: enabledId, label: enabledId }] }), + "anthropic", + enabledId, ); expect(result.ok).toBe(false); if (result.ok) { throw new Error("Expected explicit disabled model to be rejected."); } - expect(result.error).toContain('does not allow model "model-beta"'); + expect(result.error).toContain(`does not allow model "${enabledId}"`); }); }); describe("native connector defaults", () => { - it("uses the persisted connector model as the native default when configured", () => { - const codexModels = getConnectorDefinition("codex_local").module.models ?? []; - const configuredDefault = codexModels[1]?.id; - - if (!configuredDefault) { - throw new Error("codex_local requires at least two models for this default policy test."); - } + it("falls back to definition defaultModel when connector model is empty", () => { + const definition = getConnectorDefinition("openai-codex"); + const configuredDefault = definition.defaultModel; const policy = getConnectorModelPolicy({ getConnector: () => ({ - id: "codex_local" as const, + id: "openai-codex" as const, label: "Codex", - model: configuredDefault, + model: null, }), getDisabledModels: () => [], - }, "codex_local"); + }, "openai-codex"); expect(policy.defaultModel).toBe(configuredDefault); expect(validateConnectorModelSelection({ getConnector: () => ({ - id: "codex_local" as const, + id: "openai-codex" as const, label: "Codex", - model: configuredDefault, + model: null, }), getDisabledModels: () => [], - }, "codex_local", null)).toEqual({ + }, "openai-codex", null)).toEqual({ ok: true, effectiveModel: configuredDefault, }); diff --git a/tests/unit/connectors.spec.ts b/tests/unit/connectors.spec.ts index 50d57cda..7e1a1847 100644 --- a/tests/unit/connectors.spec.ts +++ b/tests/unit/connectors.spec.ts @@ -19,16 +19,20 @@ describe("parseEnvBindingText", () => { describe("buildDefaultConnectorRows", () => { it("preserves previously saved connector commands", () => { - const [codex] = buildDefaultConnectorRows([ + const rows = buildDefaultConnectorRows([ { - id: "codex_local", + id: "openai-codex", command: "/usr/local/bin/codex", status: "ready", notes: "custom note", }, ]); + const codex = rows.find((row) => row.id === "openai-codex"); + if (!codex) { + throw new Error("openai-codex connector not found in default rows."); + } - expect(codex.id).toBe("codex_local"); + expect(codex.id).toBe("openai-codex"); expect(codex.command).toBe("/usr/local/bin/codex"); expect(codex.status).toBe("ready"); expect(codex.notes).toBe("custom note"); diff --git a/tests/unit/contracts.test.ts b/tests/unit/contracts.test.ts index 883ef5e5..faa24ea6 100644 --- a/tests/unit/contracts.test.ts +++ b/tests/unit/contracts.test.ts @@ -10,7 +10,7 @@ import { describe("shared contracts", () => { it("fills connector defaults for optional fields", () => { const parsed = connectorInputSchema.parse({ - id: "codex_local", + id: "openai-codex", command: "codex", }); diff --git a/tests/unit/credential-resolver.test.ts b/tests/unit/credential-resolver.test.ts new file mode 100644 index 00000000..98f89c83 --- /dev/null +++ b/tests/unit/credential-resolver.test.ts @@ -0,0 +1,146 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; +import { CredentialResolver, CredentialError } from "../../src/main/credentials/credential-resolver"; + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +describe("CredentialResolver", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + let resolver: CredentialResolver; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-resolver-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + resolver = new CredentialResolver(store); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("returns the literal apiKey for an api_key connector", async () => { + store.save("anthropic", { + kind: "api_key", + value: { source: "literal", value: "sk-ant-test-1234567890abcd" }, + }); + const resolved = await resolver.resolve("anthropic"); + expect(resolved.apiKey).toBe("sk-ant-test-1234567890abcd"); + expect(resolved.extraEnv).toBeUndefined(); + }); + + it("resolves an api_key connector backed by an env variable", async () => { + process.env.MY_TEST_OPENAI_KEY = "sk-from-env-1234"; + store.save("openai", { + kind: "api_key", + value: { source: "env", value: "MY_TEST_OPENAI_KEY" }, + }); + try { + const resolved = await resolver.resolve("openai"); + expect(resolved.apiKey).toBe("sk-from-env-1234"); + } finally { + delete process.env.MY_TEST_OPENAI_KEY; + } + }); + + it("throws CREDENTIAL_MISSING when the env-backed key is unset", async () => { + delete process.env.UNSET_TEST_KEY; + store.save("xai", { kind: "api_key", value: { source: "env", value: "UNSET_TEST_KEY" } }); + await expect(resolver.resolve("xai")).rejects.toBeInstanceOf(CredentialError); + }); + + it("assembles AWS extraEnv for IAM auth mode", async () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "iam", + region: "us-west-2", + accessKeyId: "AKIA-TEST-FAKE-1234", + secretAccessKey: "secret-fake-secret", + sessionToken: "session-fake-token", + }, + }); + const resolved = await resolver.resolve("amazon-bedrock"); + expect(resolved.apiKey).toBe(""); + expect(resolved.extraEnv).toEqual({ + AWS_REGION: "us-west-2", + AWS_ACCESS_KEY_ID: "AKIA-TEST-FAKE-1234", + AWS_SECRET_ACCESS_KEY: "secret-fake-secret", + AWS_SESSION_TOKEN: "session-fake-token", + }); + }); + + it("assembles AWS extraEnv for profile auth mode plus advanced flags", async () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "profile", + region: "eu-west-1", + awsProfile: "production", + forceCache: true, + endpointUrl: "https://bedrock-runtime.example.com", + skipAuth: true, + forceHttp1: true, + }, + }); + const resolved = await resolver.resolve("amazon-bedrock"); + expect(resolved.extraEnv).toEqual({ + AWS_REGION: "eu-west-1", + AWS_PROFILE: "production", + AWS_BEDROCK_FORCE_CACHE: "1", + AWS_ENDPOINT_URL_BEDROCK_RUNTIME: "https://bedrock-runtime.example.com", + AWS_BEDROCK_SKIP_AUTH: "1", + AWS_BEDROCK_FORCE_HTTP1: "1", + }); + }); + + it("assembles Azure extraEnv with deploymentMap", async () => { + store.save("azure-openai", { + kind: "azure", + value: { + apiKey: "azure-secret-key", + baseUrl: "https://my-resource.openai.azure.com/", + resourceName: "my-resource", + apiVersion: "2024-08-01-preview", + deploymentMap: { "gpt-4o": "gpt4-deploy", "gpt-4o-mini": "gpt4mini-deploy" }, + }, + }); + const resolved = await resolver.resolve("azure-openai"); + expect(resolved.apiKey).toBe("azure-secret-key"); + expect(resolved.extraEnv).toMatchObject({ + AZURE_OPENAI_API_KEY: "azure-secret-key", + AZURE_OPENAI_BASE_URL: "https://my-resource.openai.azure.com/", + AZURE_OPENAI_RESOURCE_NAME: "my-resource", + AZURE_OPENAI_API_VERSION: "2024-08-01-preview", + }); + expect(resolved.extraEnv?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).toContain("gpt-4o=gpt4-deploy"); + }); + + it("assembles Vertex extraEnv from project + location + credentials path", async () => { + store.save("google-vertex", { + kind: "gcp_adc", + value: { + project: "my-gcp-project", + location: "us-central1", + credentialsPath: "/etc/gcp/service-account.json", + }, + }); + const resolved = await resolver.resolve("google-vertex"); + expect(resolved.apiKey).toBe(""); + expect(resolved.extraEnv).toEqual({ + GOOGLE_CLOUD_PROJECT: "my-gcp-project", + GOOGLE_CLOUD_LOCATION: "us-central1", + GOOGLE_APPLICATION_CREDENTIALS: "/etc/gcp/service-account.json", + }); + }); +}); diff --git a/tests/unit/credential-store.test.ts b/tests/unit/credential-store.test.ts new file mode 100644 index 00000000..85ac3670 --- /dev/null +++ b/tests/unit/credential-store.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; + +beforeAll(() => { + // SecretVault throws unless safeStorage is available or this gate is set. + // safeStorage is unavailable inside vitest, so we use the test-only path. + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +describe("CredentialStore", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-credential-store-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("round-trips an api_key literal credential", () => { + const summary = store.save("anthropic", { kind: "api_key", value: { source: "literal", value: "sk-ant-test1234567890abcd" } }); + expect(summary.kind).toBe("api_key"); + expect(summary.status).toBe("active"); + expect(summary.displayHint).toContain("..."); + expect(summary.displayHint).not.toContain("test1234567890"); + + const decrypted = store.read("anthropic"); + expect(decrypted).not.toBeNull(); + expect(decrypted!.kind).toBe("api_key"); + if (decrypted!.kind === "api_key") { + expect(decrypted!.value.source).toBe("literal"); + expect(decrypted!.value.value).toBe("sk-ant-test1234567890abcd"); + } + }); + + it("round-trips an aws credential with bearer mode", () => { + store.save("amazon-bedrock", { + kind: "aws", + value: { + authMode: "bearer", + region: "us-east-2", + bearerToken: "bedrock-bearer-xyz", + }, + }); + const decrypted = store.read("amazon-bedrock"); + expect(decrypted?.kind).toBe("aws"); + if (decrypted?.kind === "aws") { + expect(decrypted.value.authMode).toBe("bearer"); + expect(decrypted.value.region).toBe("us-east-2"); + expect(decrypted.value.bearerToken).toBe("bedrock-bearer-xyz"); + } + }); + + it("removes a credential", () => { + store.save("openai", { kind: "api_key", value: { source: "literal", value: "sk-test-abcdefghij" } }); + expect(store.list().some((row) => row.connectorId === "openai")).toBe(true); + store.remove("openai"); + expect(store.list().some((row) => row.connectorId === "openai")).toBe(false); + expect(store.read("openai")).toBeNull(); + }); + + it("setStatus updates the status row without losing ciphertext", () => { + store.save("groq", { kind: "api_key", value: { source: "env", value: "GROQ_API_KEY" } }); + store.setStatus("groq", "expired", "refresh failed"); + const row = store.list().find((entry) => entry.connectorId === "groq"); + expect(row?.status).toBe("expired"); + expect(row?.lastError).toBe("refresh failed"); + }); +}); diff --git a/tests/unit/credentials-schemas.test.ts b/tests/unit/credentials-schemas.test.ts new file mode 100644 index 00000000..2b9bb264 --- /dev/null +++ b/tests/unit/credentials-schemas.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from "vitest"; +import { + apiKeyValueSchema, + awsCredentialSchema, + azureCredentialSchema, + credentialPayloadSchema, + gcpAdcCredentialSchema, + removeCredentialInputSchema, + saveCredentialInputSchema, + testCredentialInputSchema, +} from "../../src/shared/contracts/schemas-credentials"; +import { + oauthCancelInputSchema, + oauthManualCodeResponseSchema, + oauthPromptResponseSchema, + oauthStartInputSchema, +} from "../../src/shared/contracts/schemas-oauth"; + +/** Verifies the IPC payload schemas reject malformed input and accept valid + * shapes. These are the only contract between renderer ↔ main for the new + * credential / OAuth channels — incorrect validation would let bad data + * reach pi-mono primitives. */ + +describe("credentials Zod schemas", () => { + it("apiKeyValueSchema rejects empty value", () => { + const result = apiKeyValueSchema.safeParse({ source: "literal", value: "" }); + expect(result.success).toBe(false); + }); + + it("apiKeyValueSchema accepts both source kinds", () => { + expect(apiKeyValueSchema.safeParse({ source: "literal", value: "sk-x" }).success).toBe(true); + expect(apiKeyValueSchema.safeParse({ source: "env", value: "MY_KEY" }).success).toBe(true); + }); + + it("apiKeyValueSchema rejects unknown source", () => { + const result = apiKeyValueSchema.safeParse({ source: "shell", value: "$(echo x)" }); + expect(result.success).toBe(false); + }); + + it("awsCredentialSchema requires authMode", () => { + expect(awsCredentialSchema.safeParse({ region: "us-east-1" }).success).toBe(false); + expect(awsCredentialSchema.safeParse({ authMode: "profile", region: "us-east-1", awsProfile: "default" }).success).toBe(true); + }); + + it("awsCredentialSchema accepts all three auth modes", () => { + for (const authMode of ["profile", "iam", "bearer"] as const) { + const result = awsCredentialSchema.safeParse({ authMode, region: "us-east-1" }); + expect(result.success).toBe(true); + } + }); + + it("azureCredentialSchema requires apiKey", () => { + expect(azureCredentialSchema.safeParse({}).success).toBe(false); + expect(azureCredentialSchema.safeParse({ apiKey: "azure-secret" }).success).toBe(true); + }); + + it("gcpAdcCredentialSchema requires project + location", () => { + expect(gcpAdcCredentialSchema.safeParse({ project: "" }).success).toBe(false); + expect(gcpAdcCredentialSchema.safeParse({ project: "p", location: "us-central1" }).success).toBe(true); + }); + + it("credentialPayloadSchema discriminates on kind", () => { + expect( + credentialPayloadSchema.safeParse({ + kind: "api_key", + value: { source: "literal", value: "sk-x" }, + }).success, + ).toBe(true); + expect( + credentialPayloadSchema.safeParse({ + kind: "azure", + value: { source: "literal", value: "wrong-shape" }, + }).success, + ).toBe(false); + }); + + it("saveCredentialInputSchema requires connectorId + payload", () => { + expect( + saveCredentialInputSchema.safeParse({ + connectorId: "anthropic", + payload: { kind: "api_key", value: { source: "literal", value: "sk-x" } }, + }).success, + ).toBe(true); + expect(saveCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + expect(saveCredentialInputSchema.safeParse({ payload: {} }).success).toBe(false); + }); + + it("removeCredentialInputSchema rejects empty connectorId", () => { + expect(removeCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true); + expect(removeCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + expect(removeCredentialInputSchema.safeParse({}).success).toBe(false); + }); + + it("testCredentialInputSchema rejects empty connectorId", () => { + expect(testCredentialInputSchema.safeParse({ connectorId: "openai" }).success).toBe(true); + expect(testCredentialInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + }); +}); + +describe("oauth Zod schemas", () => { + it("oauthStartInputSchema rejects empty connectorId", () => { + expect(oauthStartInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true); + expect(oauthStartInputSchema.safeParse({ connectorId: "" }).success).toBe(false); + }); + + it("oauthCancelInputSchema rejects missing connectorId", () => { + expect(oauthCancelInputSchema.safeParse({ connectorId: "claude-pro" }).success).toBe(true); + expect(oauthCancelInputSchema.safeParse({}).success).toBe(false); + }); + + it("oauthPromptResponseSchema requires connectorId, requestId and a (possibly empty) value", () => { + expect( + oauthPromptResponseSchema.safeParse({ + connectorId: "github-copilot", + requestId: "req-1", + value: "github.com", + }).success, + ).toBe(true); + expect( + oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", requestId: "" }).success, + ).toBe(false); + expect( + oauthPromptResponseSchema.safeParse({ connectorId: "github-copilot", value: "x" }).success, + ).toBe(false); + }); + + it("oauthManualCodeResponseSchema requires non-empty code", () => { + expect( + oauthManualCodeResponseSchema.safeParse({ + connectorId: "anthropic", + requestId: "req-1", + code: "abcd", + }).success, + ).toBe(true); + expect( + oauthManualCodeResponseSchema.safeParse({ + connectorId: "anthropic", + requestId: "req-1", + code: "", + }).success, + ).toBe(false); + }); +}); diff --git a/tests/unit/dashboard-charts.test.ts b/tests/unit/dashboard-charts.test.ts index 441e828f..960dc99e 100644 --- a/tests/unit/dashboard-charts.test.ts +++ b/tests/unit/dashboard-charts.test.ts @@ -20,7 +20,7 @@ function makeRun(overrides: Partial = {}): RunRecord { taskId: "task-1", agentId: "agent-1", workspaceId: null, - connectorId: "claude_local", + connectorId: "claude-pro", status: "succeeded", summary: "", errorMessage: null, @@ -47,7 +47,7 @@ function makeCost(overrides: Partial = {}): CostRecord { runId: "run-1", taskId: null, agentId: null, - connectorId: "claude_local", + connectorId: "claude-pro", amountUsd: 0.5, unattributed: false, createdAt: "2026-03-20T10:00:00Z", @@ -67,7 +67,7 @@ function makeAgent(overrides: Partial = {}): AgentRecord { department: "engineering", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "[]", diff --git a/tests/unit/database.test.ts b/tests/unit/database.test.ts index 4cedff0f..3cc6f9ab 100644 --- a/tests/unit/database.test.ts +++ b/tests/unit/database.test.ts @@ -54,7 +54,7 @@ describe("AppDatabase", () => { title: "Lead operator", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5", capabilities: "Directs execution", @@ -95,7 +95,7 @@ describe("AppDatabase", () => { value: "sk-test-secret", }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "OPENAI_API_KEY=vault-openai", @@ -189,14 +189,14 @@ describe("AppDatabase", () => { title: "Lead", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Leads", budgetMonthlyUsd: 100, }); db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -217,14 +217,14 @@ describe("AppDatabase", () => { taskId, agentId: leadId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const { id: runId2 } = db.createRun({ companyId, taskId, agentId: leadId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); expect(db.checkoutTask(taskId, runId1)).toBe(true); @@ -255,7 +255,7 @@ describe("AppDatabase", () => { title: "Budget Owner", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Ships work", @@ -276,7 +276,7 @@ describe("AppDatabase", () => { taskId, agentId, workspaceId, - connectorId: "codex_local", + connectorId: "openai-codex", }); const outcome = db.finishRun({ @@ -310,7 +310,7 @@ describe("AppDatabase", () => { title: "Manager", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Manages", @@ -323,7 +323,7 @@ describe("AppDatabase", () => { title: "Pending Hire", status: "pending_approval", reportsTo: managerId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Will execute after approval", @@ -362,7 +362,7 @@ describe("AppDatabase", () => { title: "Active", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Ships", @@ -375,7 +375,7 @@ describe("AppDatabase", () => { title: "Pending", status: "pending_approval", reportsTo: activeAgentId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Waiting", @@ -479,7 +479,7 @@ describe("AppDatabase", () => { title: "Test", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: wsId, model: null, capabilities: "test", @@ -495,7 +495,7 @@ describe("AppDatabase", () => { status: "todo", }); - const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "codex_local" }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId: wsId, connectorId: "openai-codex" }); await db.appendRunLog(run.id, "stdout", "test log line\n"); // Finish the run with a very old timestamp @@ -544,7 +544,7 @@ describe("AppDatabase", () => { title: "CEO", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "manage", @@ -557,7 +557,7 @@ describe("AppDatabase", () => { title: "IC", status: "pending_approval", reportsTo: managerId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "code", diff --git a/tests/unit/edge-case-budget.test.ts b/tests/unit/edge-case-budget.test.ts index e9a73ac8..4153aa5d 100644 --- a/tests/unit/edge-case-budget.test.ts +++ b/tests/unit/edge-case-budget.test.ts @@ -26,7 +26,7 @@ function scaffold(db: AppDatabase) { status: "active", }); db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -47,7 +47,7 @@ function scaffold(db: AppDatabase) { title: "Developer", status: "active", reportsTo: null, - connectorId: "gemini_local", + connectorId: "gemini-cli", workspaceId, model: null, capabilities: "Executes tasks", @@ -134,7 +134,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run1.id); db.markRunStatus(run1.id, "running", ""); @@ -154,7 +154,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId: taskId2, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId2, run0.id); db.markRunStatus(run0.id, "running", ""); @@ -222,7 +222,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -271,7 +271,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -329,7 +329,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -382,7 +382,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); @@ -429,7 +429,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); db.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); diff --git a/tests/unit/edge-case-concurrent.test.ts b/tests/unit/edge-case-concurrent.test.ts index 152dccef..c1f4c245 100644 --- a/tests/unit/edge-case-concurrent.test.ts +++ b/tests/unit/edge-case-concurrent.test.ts @@ -37,7 +37,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -64,7 +64,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "gemini_local", + connectorId: "gemini-cli", workspaceId, model: null, capabilities: "Executes tasks", @@ -116,14 +116,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const { id: runIdB } = db.createRun({ companyId, taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); // Agent-A claims first @@ -152,14 +152,14 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId: taskId1, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const { id: runIdB1 } = db.createRun({ companyId, taskId: taskId1, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); // Agent-A wins first task @@ -174,7 +174,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId: taskId2, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const resultB2 = db.checkoutTask(taskId2, runIdB2); expect(resultB2).toBe(true); @@ -198,7 +198,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -208,7 +208,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(false); @@ -231,7 +231,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -252,7 +252,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(false); @@ -270,7 +270,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -306,7 +306,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentA, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); expect(db.checkoutTask(taskId, runIdA)).toBe(true); @@ -328,7 +328,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { taskId, agentId: agentB, workspaceId, - connectorId: "gemini_local", + connectorId: "gemini-cli", }); const result = db.checkoutTask(taskId, runIdB); expect(result).toBe(true); diff --git a/tests/unit/edge-case-session.test.ts b/tests/unit/edge-case-session.test.ts index 90c40e4c..4560c7f1 100644 --- a/tests/unit/edge-case-session.test.ts +++ b/tests/unit/edge-case-session.test.ts @@ -20,16 +20,16 @@ import type { ConnectorId } from "@shared/types"; // Helpers // --------------------------------------------------------------------------- -function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini_local") { +function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { const companyId = db.saveCompany({ name: "Session Edge Case Co", description: "Session rotation edge case tests (D-05.3)", status: "active", }); - // Always save gemini_local (needed for most tests) + // Always save gemini-cli (needed for most tests) db.saveConnector({ - id: "gemini_local", + id: "gemini-cli", command: "gemini", model: "gemini-pro", envBindingText: "", @@ -119,7 +119,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -135,7 +135,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { expect(session!.runCount).toBe(DEFAULT_THRESHOLDS.maxRuns); // Evaluate rotation -- should trigger - const result = sessionService.evaluateRotation(session!, "gemini_local"); + const result = sessionService.evaluateRotation(session!, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -148,7 +148,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -162,7 +162,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const totalTokens = session!.totalInputTokens + session!.totalOutputTokens; expect(totalTokens).toBe(550000); - const result = sessionService.evaluateRotation(session!, "gemini_local"); + const result = sessionService.evaluateRotation(session!, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -175,7 +175,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -191,7 +191,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local" as ConnectorId, + connectorId: "gemini-cli" as ConnectorId, }); db.finishRun({ runId: run.id, @@ -233,7 +233,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeKey: "abc" }), sessionDisplayId: "gemini-session-1", }); @@ -249,7 +249,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeKey: "abc" }), sessionDisplayId: "gemini-session-2", }); @@ -266,15 +266,15 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { expect(newSession!.totalOutputTokens).toBe(0); }); - it("claude connector is exempt from rotation", () => { - const { companyId, agentId, taskId } = scaffold(db, "gemini_local"); + it("claude connector triggers rotation under pi-mono (no native exemption)", () => { + const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionId = db.createNewSession({ taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -285,21 +285,21 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { } const session = db.getActiveTaskSession(taskId)!; - // Per Pitfall 6: claude_local has nativeContextManagement: true, exempt from rotation - const result = sessionService.evaluateRotation(session, "claude_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. + const result = sessionService.evaluateRotation(session, "claude-pro"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); - it("codex connector is exempt from rotation", () => { - const { companyId, agentId, taskId } = scaffold(db, "gemini_local"); + it("codex connector triggers rotation under pi-mono (no native exemption)", () => { + const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionId = db.createNewSession({ taskId, companyId, agentId, - adapterType: "codex_local", + adapterType: "openai-codex", sessionParamsJson: null, sessionDisplayId: null, }); @@ -310,10 +310,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { } const session = db.getActiveTaskSession(taskId)!; - // Per Pitfall 6: codex_local has nativeContextManagement: true, exempt from rotation - const result = sessionService.evaluateRotation(session, "codex_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. + const result = sessionService.evaluateRotation(session, "openai-codex"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); it("handoff markdown available for next run context after rotation", () => { @@ -325,7 +325,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -337,7 +337,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { taskId, agentId, workspaceId, - connectorId: "gemini_local" as ConnectorId, + connectorId: "gemini-cli" as ConnectorId, }); db.finishRun({ runId: run.id, diff --git a/tests/unit/heartbeat-coalescing.test.ts b/tests/unit/heartbeat-coalescing.test.ts index 838a930e..2843cf43 100644 --- a/tests/unit/heartbeat-coalescing.test.ts +++ b/tests/unit/heartbeat-coalescing.test.ts @@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks", diff --git a/tests/unit/ipc-agent-graph-handlers.test.ts b/tests/unit/ipc-agent-graph-handlers.test.ts index 73aa346e..2a0da632 100644 --- a/tests/unit/ipc-agent-graph-handlers.test.ts +++ b/tests/unit/ipc-agent-graph-handlers.test.ts @@ -32,13 +32,13 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { db.init(); db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - db.setDisabledModels("codex_local", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.setDisabledModels("openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const companyId = db.saveCompany({ name: "Handler Test Co", @@ -60,7 +60,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -93,7 +93,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { department: "executive", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: "gpt-5.4", metadataJson: "{}", diff --git a/tests/unit/ipc-core-admin-handlers.test.ts b/tests/unit/ipc-core-admin-handlers.test.ts index 4c7c9e32..8c01aeb2 100644 --- a/tests/unit/ipc-core-admin-handlers.test.ts +++ b/tests/unit/ipc-core-admin-handlers.test.ts @@ -26,11 +26,17 @@ afterEach(async () => { }); describe("ipc-core-admin-handlers connector saves", () => { - it("accepts compatible connector model catalogs stored as csv", async () => { + it("accepts a valid pi-ai model for a pi-mono connector", async () => { const profileDir = await createTempDir("agentcompany-admin-handler-"); const db = new AppDatabase(profileDir); db.init(); + const { getModels } = await import("@mariozechner/pi-ai"); + const anthropicModel = getModels("anthropic" as never)[0]?.id; + if (!anthropicModel) { + throw new Error("pi-ai anthropic provider must expose at least one model."); + } + const handlers = new Map(); registerCoreAdminHandlers({ registerHandle: (channel, handler) => { @@ -50,16 +56,26 @@ describe("ipc-core-admin-handlers connector saves", () => { throw new Error("saveConnector handler was not registered."); } + // Pi-mono connectors default to an empty command; the schema requires a + // non-empty one, so seed it before hitting the IPC handler. + db.saveConnector({ + id: "anthropic", + command: "pi-mono", + model: null, + envBindingText: "", + notes: "", + }); + const result = await saveConnector(null as unknown as IpcMainInvokeEvent, { - id: "claude_compatible", - command: "claude", - model: "model-alpha,model-beta,model-gamma", + id: "anthropic", + command: "pi-mono", + model: anthropicModel, envBindingText: "ANTHROPIC_BASE_URL=https://example.com/anthropic", - notes: "Compat provider", + notes: "Pi-mono connector", }) as { ok: boolean; error?: { code: string; message: string } }; expect(result.ok).toBe(true); - expect(db.getConnector("claude_compatible").model).toBe("model-alpha,model-beta,model-gamma"); + expect(db.getConnector("anthropic").model).toBe(anthropicModel); db.close(); }); diff --git a/tests/unit/log-redactor.test.ts b/tests/unit/log-redactor.test.ts new file mode 100644 index 00000000..be3650a6 --- /dev/null +++ b/tests/unit/log-redactor.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { redactTokens } from "../../src/main/worker/log-redactor"; + +/** Verifies the worker process token redactor strips known + * credential-shaped substrings from log strings before they leave the + * worker process. */ +describe("worker log redactor", () => { + it("redacts Bearer tokens", () => { + const input = "Authorization: Bearer abc.def.ghi-jkl_mnop"; + expect(redactTokens(input)).toBe("Authorization: Bearer [REDACTED]"); + }); + + it("redacts authorization header values regardless of case", () => { + expect(redactTokens("authorization=secret-value-1234567890")).toContain("[REDACTED]"); + expect(redactTokens("Authorization: secret-value-1234567890")).toContain("[REDACTED]"); + }); + + it("redacts access_token query strings", () => { + expect(redactTokens("https://api.example.com/?access_token=abc123xyz&foo=1")).toBe( + "https://api.example.com/?access_token=[REDACTED]&foo=1", + ); + }); + + it("redacts refresh_token values", () => { + expect(redactTokens("refresh_token=long.refresh.tok&grant_type=refresh_token")).toContain( + "refresh_token=[REDACTED]", + ); + }); + + it("redacts api_key key/value pairs", () => { + const input = "config { api_key: \"sk-test-1234567890abcdef\", model: \"gpt-4o\" }"; + const result = redactTokens(input); + expect(result).not.toContain("sk-test-1234567890abcdef"); + expect(result).toContain("[REDACTED]"); + }); + + it("redacts sk- and sk-ant- prefixed keys anywhere in a string", () => { + expect(redactTokens("error using sk-ant-api03_abcdefghijklmnop, retrying")).toContain("sk-[REDACTED]"); + expect(redactTokens("token=sk-test1234567890_abcd")).toContain("sk-[REDACTED]"); + }); + + it("redacts GitHub personal access token shapes", () => { + expect(redactTokens("git push https://x:ghp_aaaaaaaaaaaaaaaaaa@github.com/foo")).toContain("ghp_[REDACTED]"); + }); + + it("redacts AWS access key id prefixes", () => { + expect(redactTokens("aws cred AKIAIOSFODNN7EXAMPLE in trace")).toContain("AKIA[REDACTED]"); + }); + + it("leaves credential-free strings unchanged", () => { + const safe = "Just a regular log line about model claude-opus-4-5 with usage 123 tokens."; + expect(redactTokens(safe)).toBe(safe); + }); + + it("redacts multiple matches in one string", () => { + const input = "Bearer abc.def.ghi.jkl Authorization=secretsecretsecret"; + const result = redactTokens(input); + expect(result).not.toContain("abc.def.ghi.jkl"); + expect(result).not.toContain("secretsecretsecret"); + }); +}); diff --git a/tests/unit/message-dispatch.test.ts b/tests/unit/message-dispatch.test.ts index aa3c559b..256865e8 100644 --- a/tests/unit/message-dispatch.test.ts +++ b/tests/unit/message-dispatch.test.ts @@ -20,7 +20,7 @@ afterEach(async () => { function createScaffold(db: AppDatabase) { db.updateConnectorHealth({ - id: "codex_local", + id: "openai-codex", status: "ready", authState: "authenticated", version: "test", @@ -48,7 +48,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Executive Officer", status: "idle", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead the company", @@ -61,7 +61,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Technology Officer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead engineering", @@ -75,7 +75,7 @@ function createScaffold(db: AppDatabase) { title: "Chief Marketing Officer", status: "idle", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Lead marketing", @@ -89,7 +89,7 @@ function createScaffold(db: AppDatabase) { title: "Pending Hire", status: "pending_approval", reportsTo: ctoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Pending", @@ -103,7 +103,7 @@ function createScaffold(db: AppDatabase) { title: "Former COO", status: "terminated", reportsTo: ceoId, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "No longer active", diff --git a/tests/unit/message-search.test.ts b/tests/unit/message-search.test.ts index 91412320..5a93a8b7 100644 --- a/tests/unit/message-search.test.ts +++ b/tests/unit/message-search.test.ts @@ -39,7 +39,7 @@ describe("AppDatabase.searchMessages", () => { title: "Senior Engineer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "coding", @@ -53,7 +53,7 @@ describe("AppDatabase.searchMessages", () => { title: "Lead Designer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "design", diff --git a/tests/unit/oauth-login-service.test.ts b/tests/unit/oauth-login-service.test.ts new file mode 100644 index 00000000..fedfd0d6 --- /dev/null +++ b/tests/unit/oauth-login-service.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { AppDatabase } from "../../src/main/database"; +import { CredentialStore } from "../../src/main/credentials/credential-store"; +import { OAuthLoginService } from "../../src/main/credentials/oauth-login-service"; + +vi.mock("electron", () => ({ + shell: { openExternal: vi.fn(async () => true) }, +})); + +beforeAll(() => { + process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true"; +}); + +const noopLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + +describe("OAuthLoginService", () => { + let tempDir: string; + let db: AppDatabase; + let store: CredentialStore; + let service: OAuthLoginService; + const emit = vi.fn(); + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-oauth-service-")); + db = new AppDatabase(tempDir); + db.init(); + store = new CredentialStore({ db }); + service = new OAuthLoginService({ + store, + emitEvent: emit, + refreshConnectorHealth: async () => {}, + logger: noopLogger, + }); + emit.mockClear(); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("rejects start() for a non-OAuth connector", async () => { + await expect(service.start("anthropic")).rejects.toThrow(/does not use OAuth/); + }); + + it("rejects start() for an unknown connector id", async () => { + await expect(service.start("definitely-not-a-connector")).rejects.toThrow(/Unknown connector/); + }); + + it("submitPromptResponse returns false when there is no pending prompt", () => { + expect(service.submitPromptResponse("nonexistent-request-id", "value")).toBe(false); + }); + + it("submitManualCode returns false when there is no pending request", () => { + expect(service.submitManualCode("nonexistent-request-id", "code")).toBe(false); + }); + + it("cancel() of a connector with no active flow is a no-op (does not throw)", () => { + expect(() => service.cancel("claude-pro")).not.toThrow(); + }); +}); diff --git a/tests/unit/prompt-manager-classification.test.ts b/tests/unit/prompt-manager-classification.test.ts index 2b6442aa..b5e0cdaa 100644 --- a/tests/unit/prompt-manager-classification.test.ts +++ b/tests/unit/prompt-manager-classification.test.ts @@ -14,7 +14,7 @@ function buildCtx(overrides: Partial = {}): PromptContext { department: "engineering", status: "active", reportsTo: "ceo-1", - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Lead engineering", @@ -46,7 +46,7 @@ function buildCtx(overrides: Partial = {}): PromptContext { department: "executive", status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId: null, model: null, capabilities: "Lead the company", diff --git a/tests/unit/session-management.test.ts b/tests/unit/session-management.test.ts index d12d63b6..2b661c59 100644 --- a/tests/unit/session-management.test.ts +++ b/tests/unit/session-management.test.ts @@ -41,7 +41,7 @@ function scaffold(db: AppDatabase) { title: "Developer", status: "active", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId, model: "claude-4-opus", capabilities: "Writes code", @@ -107,7 +107,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: JSON.stringify({ sessionId: "abc123" }), sessionDisplayId: "claude-abc123", }); @@ -118,7 +118,7 @@ describe("task_sessions CRUD", () => { expect(session!.taskId).toBe(taskId); expect(session!.companyId).toBe(companyId); expect(session!.agentId).toBe(agentId); - expect(session!.adapterType).toBe("claude_local"); + expect(session!.adapterType).toBe("claude-pro"); expect(session!.sessionParamsJson).toBe(JSON.stringify({ sessionId: "abc123" })); expect(session!.sessionDisplayId).toBe("claude-abc123"); expect(session!.runCount).toBe(0); @@ -135,7 +135,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -163,7 +163,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -183,7 +183,7 @@ describe("task_sessions CRUD", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ key: "val" }), sessionDisplayId: "gemini-session-1", }); @@ -195,14 +195,14 @@ describe("task_sessions CRUD", () => { const session = db.getActiveTaskSession(taskId); expect(session).not.toBeNull(); expect(session!.id).toBe(sessionId); - expect(session!.adapterType).toBe("gemini_local"); + expect(session!.adapterType).toBe("gemini-cli"); }); it("getRecentRunsForTask returns last N runs ordered by finished_at DESC", () => { const { taskId, companyId, agentId, workspaceId } = scaffold(db); // Create some runs - const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" }); + const run1 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" }); db.finishRun({ runId: run1.id, status: "succeeded", @@ -216,7 +216,7 @@ describe("task_sessions CRUD", () => { unattributedCost: false, }); - const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude_local" }); + const run2 = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "claude-pro" }); db.finishRun({ runId: run2.id, status: "failed", @@ -242,22 +242,25 @@ describe("task_sessions CRUD", () => { }); // =========================================================================== -// adapter exemption +// connector definition lookup // =========================================================================== -describe("adapter exemption", () => { - it("claude_local connector has nativeContextManagement: true", () => { - const def = getConnectorDefinition("claude_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(true); +describe("connector definition lookup", () => { + it("resolves claude-pro definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("claude-pro" as ConnectorId); + expect(def.id).toBe("claude-pro"); + expect(def.provider).toBe("anthropic"); }); - it("codex_local connector has nativeContextManagement: true", () => { - const def = getConnectorDefinition("codex_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(true); + it("resolves openai-codex definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("openai-codex" as ConnectorId); + expect(def.id).toBe("openai-codex"); + expect(def.provider).toBe("openai-codex"); }); - it("gemini_local connector has nativeContextManagement: false", () => { - const def = getConnectorDefinition("gemini_local" as ConnectorId); - expect(def.module.nativeContextManagement).toBe(false); + it("resolves gemini-cli definition from the pi-mono catalog", () => { + const def = getConnectorDefinition("gemini-cli" as ConnectorId); + expect(def.id).toBe("gemini-cli"); + expect(def.provider).toBe("google-gemini-cli"); }); }); @@ -298,7 +301,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -312,7 +315,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(false); expect(result.reason).toBeNull(); }); @@ -324,7 +327,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -338,7 +341,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -350,7 +353,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -364,7 +367,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -377,7 +380,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -391,24 +394,24 @@ describe("session management service", () => { createdAt: oldDate, updatedAt: oldDate, }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_age_hours"); }); }); // ------------------------------------------------------------------------- - // adapter exemption in service + // uniform rotation under pi-mono // ------------------------------------------------------------------------- - describe("adapter exemption in evaluateRotation", () => { - it("returns shouldRotate: false for nativeContextManagement=true even when thresholds exceeded", () => { + describe("uniform rotation under pi-mono", () => { + it("returns shouldRotate: true for all connectors when thresholds exceeded (pi-mono has no native exemption)", () => { const svc = createSessionManagementService({ db, logger: mockLogger }); const session: TaskSessionRecord = { id: "sess-1", companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -422,9 +425,9 @@ describe("session management service", () => { createdAt: new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString(), // Way old updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "claude_local"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); + const result = svc.evaluateRotation(session, "claude-pro"); + expect(result.shouldRotate).toBe(true); + expect(result.reason).not.toBeNull(); }); }); @@ -439,7 +442,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -453,7 +456,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local", { maxRuns: 5 }); + const result = svc.evaluateRotation(session, "gemini-cli", { maxRuns: 5 }); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -465,7 +468,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -479,7 +482,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local", { maxTokens: 100000 }); + const result = svc.evaluateRotation(session, "gemini-cli", { maxTokens: 100000 }); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -491,7 +494,7 @@ describe("session management service", () => { companyId: "co-1", taskId: "task-1", agentId: "agent-1", - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, lastRunId: null, @@ -505,7 +508,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini_local"); + const result = svc.evaluateRotation(session, "gemini-cli"); expect(result.shouldRotate).toBe(false); expect(result.reason).toBeNull(); @@ -526,7 +529,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "claude_local", + adapterType: "claude-pro", sessionParamsJson: null, sessionDisplayId: null, }); @@ -552,13 +555,13 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); // Create a completed run for history - const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini_local" as ConnectorId }); + const run = db.createRun({ companyId, taskId, agentId, workspaceId, connectorId: "gemini-cli" as ConnectorId }); db.finishRun({ runId: run.id, status: "succeeded", @@ -597,7 +600,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: JSON.stringify({ resumeId: "old-abc" }), sessionDisplayId: "gemini-old", }); @@ -644,7 +647,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -663,7 +666,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -684,7 +687,7 @@ describe("session management service", () => { taskId, companyId, agentId, - adapterType: "gemini_local", + adapterType: "gemini-cli", sessionParamsJson: null, sessionDisplayId: null, }); @@ -773,7 +776,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 200, 100, "run-1"); @@ -789,10 +792,10 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); - expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini_local"); + expect(sessionService.evaluateRotation).toHaveBeenCalledWith(mockSession, "gemini-cli"); // evaluateRotation called AFTER accumulateTokenUsage const accOrder = sessionService.accumulateTokenUsage.mock.invocationCallOrder[0]; const evalOrder = sessionService.evaluateRotation.mock.invocationCallOrder[0]; @@ -815,7 +818,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.rotateSession).toHaveBeenCalledWith(updatedSession); @@ -832,7 +835,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.rotateSession).not.toHaveBeenCalled(); @@ -847,7 +850,7 @@ describe("wiring integration", () => { db, null, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(db.getActiveTaskSession).not.toHaveBeenCalled(); @@ -863,7 +866,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: 200, outputTokens: 100, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).not.toHaveBeenCalled(); @@ -879,7 +882,7 @@ describe("wiring integration", () => { db, { id: "task-1" }, { inputTokens: null, outputTokens: null, runId: "run-1" }, - "gemini_local", + "gemini-cli", ); expect(sessionService.accumulateTokenUsage).toHaveBeenCalledWith("sess-1", 0, 0, "run-1"); diff --git a/tests/unit/skill-injection.test.ts b/tests/unit/skill-injection.test.ts index d2e31be4..24789009 100644 --- a/tests/unit/skill-injection.test.ts +++ b/tests/unit/skill-injection.test.ts @@ -6,7 +6,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4", "gpt-5.4-mini"], @@ -22,7 +22,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: ["gpt-5.4"], @@ -38,7 +38,7 @@ describe("buildConnectorSection", () => { const section = buildConnectorSection({ allConnectors: [ { - id: "codex_local", + id: "openai-codex", label: "Codex", status: "ready", enabledModels: [], diff --git a/tests/unit/skill-types.test.ts b/tests/unit/skill-types.test.ts index 87d49ef6..81ec710d 100644 --- a/tests/unit/skill-types.test.ts +++ b/tests/unit/skill-types.test.ts @@ -17,36 +17,35 @@ import type { describe("skill-types", () => { describe("TOKEN_BUDGETS", () => { - it("has entries for all supported adapter types", () => { - expect(TOKEN_BUDGETS).toHaveProperty("claude_local"); - expect(TOKEN_BUDGETS).toHaveProperty("codex_local"); - expect(TOKEN_BUDGETS).toHaveProperty("gemini_local"); - expect(TOKEN_BUDGETS).toHaveProperty("claude_compatible"); + it("has entries for all supported connector types", () => { + expect(TOKEN_BUDGETS).toHaveProperty("claude-pro"); + expect(TOKEN_BUDGETS).toHaveProperty("anthropic"); + expect(TOKEN_BUDGETS).toHaveProperty("gemini-cli"); + expect(TOKEN_BUDGETS).toHaveProperty("google"); }); - it("has numeric token caps for each adapter", () => { - expect(TOKEN_BUDGETS.claude_local).toBe(50000); - expect(TOKEN_BUDGETS.codex_local).toBe(30000); - expect(TOKEN_BUDGETS.gemini_local).toBe(50000); - expect(TOKEN_BUDGETS.claude_compatible).toBe(50000); + it("has numeric token caps for each connector", () => { + expect(TOKEN_BUDGETS["claude-pro"]).toBe(50000); + expect(TOKEN_BUDGETS["anthropic"]).toBe(50000); + expect(TOKEN_BUDGETS["gemini-cli"]).toBe(50000); + expect(TOKEN_BUDGETS["google"]).toBe(50000); }); }); describe("resolveTokenBudget", () => { - it("returns exact match for known adapter types", () => { - expect(resolveTokenBudget("claude_local")).toBe(50000); - expect(resolveTokenBudget("codex_local")).toBe(30000); - expect(resolveTokenBudget("gemini_local")).toBe(50000); + it("returns exact match for known connector types", () => { + expect(resolveTokenBudget("claude-pro")).toBe(50000); + expect(resolveTokenBudget("anthropic")).toBe(50000); + expect(resolveTokenBudget("gemini-cli")).toBe(50000); }); - it("resolves claude_compat_* variants to claude_compatible budget", () => { - expect(resolveTokenBudget("claude_compat_2")).toBe(50000); - expect(resolveTokenBudget("claude_compat_10")).toBe(50000); - expect(resolveTokenBudget("claude_compatible")).toBe(50000); + it("resolves anthropic-family connectors to the larger injection budget", () => { + expect(resolveTokenBudget("amazon-bedrock")).toBe(50000); + expect(resolveTokenBudget("google-vertex")).toBe(50000); }); - it("returns 30000 default for unknown adapter types", () => { - expect(resolveTokenBudget("unknown_adapter")).toBe(30000); + it("returns 30000 default for unknown connector types", () => { + expect(resolveTokenBudget("unknown_connector")).toBe(30000); }); }); diff --git a/tests/unit/sqlite-session-store.test.ts b/tests/unit/sqlite-session-store.test.ts new file mode 100644 index 00000000..93f3b9b8 --- /dev/null +++ b/tests/unit/sqlite-session-store.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import { AppDatabase } from "../../src/main/database"; +import { SqliteSessionStore } from "../../src/main/agent-runtime/sqlite-session-store"; + +/** + * Verifies the SqliteSessionStore round-trips pi `AgentMessage` values + * correctly through the new `session_entries` table (migration 27). + */ +describe("SqliteSessionStore", () => { + let tempDir: string; + let db: AppDatabase; + const TASK_SESSION_ID = "task-session-test-1"; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "agentcompany-session-store-")); + db = new AppDatabase(tempDir); + db.init(); + }); + + afterEach(async () => { + db.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + it("appends and reloads pi user/assistant messages preserving order", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + const userMessage: AgentMessage = { + role: "user", + content: [{ type: "text", text: "hello" }], + timestamp: Date.now(), + } as AgentMessage; + const assistantMessage: AgentMessage = { + role: "assistant", + content: [{ type: "text", text: "hi" }], + stopReason: "stop", + provider: "anthropic", + model: { id: "claude-opus-4-5" }, + usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, totalTokens: 15, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + timestamp: Date.now(), + } as unknown as AgentMessage; + store.appendMessage(userMessage); + store.appendMessage(assistantMessage); + const loaded = store.loadMessages(); + expect(loaded).toHaveLength(2); + expect((loaded[0] as { role: string }).role).toBe("user"); + expect((loaded[1] as { role: string }).role).toBe("assistant"); + }); + + it("appends a compaction marker that does not appear as a message", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + store.appendMessage({ role: "user", content: [{ type: "text", text: "first" }], timestamp: Date.now() } as AgentMessage); + store.appendCompaction("Earlier conversation summarised: planning phase complete."); + const loaded = store.loadMessages(); + expect(loaded).toHaveLength(1); + }); + + it("replaceMessages supersedes prior rows and inserts new ones in order", () => { + const store = new SqliteSessionStore(db, TASK_SESSION_ID); + store.appendMessage({ role: "user", content: [{ type: "text", text: "old-1" }], timestamp: Date.now() } as AgentMessage); + store.appendMessage({ role: "assistant", content: [{ type: "text", text: "old-2" }], timestamp: Date.now() } as AgentMessage); + + const compacted: AgentMessage[] = [ + { role: "user", content: [{ type: "text", text: "context summary" }], timestamp: Date.now() } as AgentMessage, + ]; + store.replaceMessages(compacted, "Summary of prior turns."); + + const active = store.loadMessages(); + expect(active).toHaveLength(1); + expect((active[0] as { content: { text: string }[] }).content[0]?.text).toBe("context summary"); + + // Check: pre-replace messages must still exist as superseded audit rows. + const all = db.sessionEntries.listAll(TASK_SESSION_ID); + const supersededCount = all.filter((row) => row.isSuperseded).length; + expect(supersededCount).toBe(2); + }); +}); diff --git a/tests/unit/store.test.ts b/tests/unit/store.test.ts index 8a3fed56..ca54874b 100644 --- a/tests/unit/store.test.ts +++ b/tests/unit/store.test.ts @@ -285,7 +285,7 @@ const fakeRun = (id: string): RunRecord => ({ taskId: "t-1", agentId: "ag-1", workspaceId: null, - connectorId: "claude_local", + connectorId: "claude-pro", status: "queued", summary: "", errorMessage: null, @@ -311,7 +311,7 @@ const fakeAgent = (id: string, name = "Agent"): AgentRecord => ({ department: "engineering", status: "idle", reportsTo: null, - connectorId: "claude_local", + connectorId: "claude-pro", workspaceId: null, model: null, capabilities: "", diff --git a/tests/unit/wakeup-lifecycle.test.ts b/tests/unit/wakeup-lifecycle.test.ts index 2a19ff99..cc39bd82 100644 --- a/tests/unit/wakeup-lifecycle.test.ts +++ b/tests/unit/wakeup-lifecycle.test.ts @@ -28,7 +28,7 @@ function createTestCompany(db: AppDatabase): string { function createTestConnector(db: AppDatabase): void { db.saveConnector({ - id: "codex_local", + id: "openai-codex", command: "codex", model: "gpt-5", envBindingText: "", @@ -55,7 +55,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string title: name, status: "active", reportsTo: null, - connectorId: "codex_local", + connectorId: "openai-codex", workspaceId, model: null, capabilities: "Executes tasks",