@hellcoder/companion 0.111.4-preview.20260728084411.d032373 → 0.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentsPage-Dc23pQBM.js → AgentsPage-D17hofXo.js} +1 -1
- package/dist/assets/{CronManager-CKvnO3df.js → CronManager-YyyPK6hb.js} +1 -1
- package/dist/assets/{DashboardPage-_gU5oXmV.js → DashboardPage-ukAmlbn5.js} +1 -1
- package/dist/assets/{IntegrationsPage-zBdnuEDO.js → IntegrationsPage-C5rUwiN9.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-v4LYNLyW.js → LinearOAuthSettingsPage-6OAQZdqq.js} +1 -1
- package/dist/assets/{LinearSettingsPage-C-Gm_aeW.js → LinearSettingsPage-BQSZm4Bq.js} +1 -1
- package/dist/assets/{Playground-BheM7iin.js → Playground-Bq0Tks1d.js} +4 -4
- package/dist/assets/{PromptsPage-Bt2idDR8.js → PromptsPage-Bz9Ml49y.js} +1 -1
- package/dist/assets/{RunsPage-ib31kftd.js → RunsPage-CxlLKCM8.js} +1 -1
- package/dist/assets/{SandboxManager-Ch0-1iE8.js → SandboxManager-VUkfrmv0.js} +1 -1
- package/dist/assets/{SettingsPage-fqy7b9b8.js → SettingsPage-BL8vCPyR.js} +1 -1
- package/dist/assets/{TailscalePage-Cf9_ENdO.js → TailscalePage-DvlPwchP.js} +1 -1
- package/dist/assets/index-C86AT0KJ.css +1 -0
- package/dist/assets/{index-Baw3PrqK.js → index-S0nvfWUo.js} +30 -30
- package/dist/assets/{sw-register-D-9XveNW.js → sw-register-CZ9DmSD-.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/claude-e2e.test.ts +305 -0
- package/dist/assets/index-C9eyQq6F.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-S0nvfWUo.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
|
package/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="Companion ME" />
|
|
12
12
|
<meta name="theme-color" content="#d97757" />
|
|
13
13
|
<title>The Companion — Moritz Edition</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-S0nvfWUo.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C86AT0KJ.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
|
18
18
|
<div id="root"></div>
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class V{constructor(e={}){this.cacheName=C.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"d25d768699a108499b7a2b21180c1be0","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-CZ9DmSD-.js"},{"revision":null,"url":"assets/index-S0nvfWUo.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/TailscalePage-DvlPwchP.js"},{"revision":null,"url":"assets/SettingsPage-BL8vCPyR.js"},{"revision":null,"url":"assets/SandboxManager-VUkfrmv0.js"},{"revision":null,"url":"assets/RunsPage-CxlLKCM8.js"},{"revision":null,"url":"assets/PromptsPage-Bz9Ml49y.js"},{"revision":null,"url":"assets/Playground-Bq0Tks1d.js"},{"revision":null,"url":"assets/LinearSettingsPage-BQSZm4Bq.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-6OAQZdqq.js"},{"revision":null,"url":"assets/IntegrationsPage-C5rUwiN9.js"},{"revision":null,"url":"assets/DashboardPage-ukAmlbn5.js"},{"revision":null,"url":"assets/CronManager-YyyPK6hb.js"},{"revision":null,"url":"assets/AgentsPage-D17hofXo.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hellcoder/companion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web UI for launching and interacting with Claude Code agents — Moritz Edition (fork of the-companion)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end integration test against a REAL Claude Code CLI process.
|
|
3
|
+
*
|
|
4
|
+
* Every other test in this suite drives a mock `Subprocess`, which means they
|
|
5
|
+
* validate our *model* of CLI behaviour rather than the CLI itself. That gap is
|
|
6
|
+
* not theoretical: the 0.111.1 silence probe was built on the assumption that
|
|
7
|
+
* "a healthy CLI replies to control requests promptly even while working". It
|
|
8
|
+
* does not — the CLI does not service control requests while a synchronous tool
|
|
9
|
+
* runs — and because every test used a mock that answered on demand, nothing
|
|
10
|
+
* could contradict the assumption. Healthy sessions were killed in production
|
|
11
|
+
* before it was caught.
|
|
12
|
+
*
|
|
13
|
+
* These tests spawn the real binary with the exact argv cli-launcher builds and
|
|
14
|
+
* assert against what it actually emits.
|
|
15
|
+
*
|
|
16
|
+
* OPT-IN. Skipped unless COMPANION_E2E=1, because it spawns a real CLI, spends
|
|
17
|
+
* real tokens and needs working credentials. CI does not run it.
|
|
18
|
+
*
|
|
19
|
+
* COMPANION_E2E=1 bun run test -- claude-e2e
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
|
22
|
+
|
|
23
|
+
// Stub Bun.hash: vitest runs under Node, but the adapter's CLI-message dedup
|
|
24
|
+
// path calls it. Without this the stdout reader throws "Bun is not defined" on
|
|
25
|
+
// the FIRST frame and dies, so the adapter observes a permanently silent CLI —
|
|
26
|
+
// which the silence probe then correctly reports as a dead transport. Mirrors
|
|
27
|
+
// the stub in ws-bridge.test.ts.
|
|
28
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
29
|
+
(globalThis as unknown as { Bun: unknown }).Bun = {
|
|
30
|
+
hash(input: string | Uint8Array): number {
|
|
31
|
+
const str = typeof input === "string" ? input : new TextDecoder().decode(input);
|
|
32
|
+
let h = 0;
|
|
33
|
+
for (let i = 0; i < str.length; i++) h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
|
|
34
|
+
return h >>> 0;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
39
|
+
import { execSync } from "node:child_process";
|
|
40
|
+
|
|
41
|
+
const E2E = process.env.COMPANION_E2E === "1";
|
|
42
|
+
|
|
43
|
+
// Timings are compressed via the same env knobs production exposes, so the
|
|
44
|
+
// probe behaviour under test is the real code path rather than a re-implementation.
|
|
45
|
+
process.env.COMPANION_SILENCE_PROBE_AFTER_MS ||= "3000";
|
|
46
|
+
process.env.COMPANION_SILENCE_PROBE_TIMEOUT_MS ||= "3000";
|
|
47
|
+
process.env.COMPANION_SILENCE_CHECK_INTERVAL_MS ||= "500";
|
|
48
|
+
|
|
49
|
+
let ClaudeAdapter: typeof import("./claude-adapter.js").ClaudeAdapter;
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
// Imported dynamically so the env overrides above are in place before the
|
|
53
|
+
// module's top-level constants are evaluated.
|
|
54
|
+
({ ClaudeAdapter } = await import("./claude-adapter.js"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function claudeAvailable(): boolean {
|
|
58
|
+
try {
|
|
59
|
+
execSync("command -v claude", { stdio: "ignore" });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The argv cli-launcher builds for a stdio session. */
|
|
67
|
+
const ARGV = [
|
|
68
|
+
"--print",
|
|
69
|
+
"--input-format", "stream-json",
|
|
70
|
+
"--output-format", "stream-json",
|
|
71
|
+
"--include-partial-messages",
|
|
72
|
+
"--verbose",
|
|
73
|
+
"--permission-prompt-tool", "stdio",
|
|
74
|
+
"--permission-mode", "bypassPermissions",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Adapt a Node child process to the shape ClaudeAdapter.attachStdio expects.
|
|
79
|
+
* The adapter only uses stdin.write, stdout as a web ReadableStream, exited,
|
|
80
|
+
* exitCode, killed, pid and kill().
|
|
81
|
+
*/
|
|
82
|
+
function adaptProc(child: ChildProcessWithoutNullStreams) {
|
|
83
|
+
const proc = {
|
|
84
|
+
pid: child.pid,
|
|
85
|
+
stdin: { write: (data: Uint8Array) => (child.stdin.write(data), data.length) },
|
|
86
|
+
// Built from 'data' events rather than Readable.toWeb(): under vitest the
|
|
87
|
+
// toWeb wrapper never delivered a chunk, so the adapter saw an entirely
|
|
88
|
+
// silent CLI while a raw spawn in the same environment streamed fine.
|
|
89
|
+
stdout: new ReadableStream<Uint8Array>({
|
|
90
|
+
start(controller) {
|
|
91
|
+
child.stdout.on("data", (d: Buffer) => controller.enqueue(new Uint8Array(d)));
|
|
92
|
+
child.stdout.on("end", () => { try { controller.close(); } catch { /* already closed */ } });
|
|
93
|
+
child.on("exit", () => { try { controller.close(); } catch { /* already closed */ } });
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
exitCode: null as number | null,
|
|
97
|
+
killed: false,
|
|
98
|
+
kill(signal?: NodeJS.Signals) {
|
|
99
|
+
proc.killed = true;
|
|
100
|
+
child.kill(signal ?? "SIGTERM");
|
|
101
|
+
},
|
|
102
|
+
exited: new Promise<number>((resolve) => {
|
|
103
|
+
child.on("exit", (code) => {
|
|
104
|
+
proc.exitCode = code ?? 0;
|
|
105
|
+
resolve(code ?? 0);
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
return proc;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const live: ChildProcessWithoutNullStreams[] = [];
|
|
113
|
+
|
|
114
|
+
function spawnClaude(cwd = "/tmp") {
|
|
115
|
+
const child = spawn("claude", ARGV, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
116
|
+
child.stderr.on("data", (d) => console.error("[cli:stderr]", String(d).trim().slice(0, 300)));
|
|
117
|
+
child.on("error", (e) => console.error("[cli:error]", e.message));
|
|
118
|
+
child.on("exit", (code, sig) => console.error("[cli:exit]", code, sig));
|
|
119
|
+
live.push(child);
|
|
120
|
+
return child;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
for (const c of live.splice(0)) {
|
|
125
|
+
try {
|
|
126
|
+
c.kill("SIGKILL");
|
|
127
|
+
} catch {
|
|
128
|
+
// Already gone.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function userMessage(text: string): string {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
type: "user",
|
|
136
|
+
message: { role: "user", content: [{ type: "text", text }] },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe.runIf(E2E)("claude CLI end-to-end (real process)", () => {
|
|
141
|
+
it("completes a turn: init → assistant text → result", async () => {
|
|
142
|
+
expect(claudeAvailable()).toBe(true);
|
|
143
|
+
|
|
144
|
+
const adapter = new ClaudeAdapter("e2e-basic");
|
|
145
|
+
const seen: string[] = [];
|
|
146
|
+
let assistantText = "";
|
|
147
|
+
adapter.onBrowserMessage((msg) => {
|
|
148
|
+
seen.push(msg.type);
|
|
149
|
+
if (msg.type === "assistant") {
|
|
150
|
+
const content = (msg as { message?: { content?: Array<{ type: string; text?: string }> } }).message?.content ?? [];
|
|
151
|
+
for (const b of content) if (b.type === "text" && b.text) assistantText += b.text;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const child = spawnClaude();
|
|
156
|
+
adapter.attachStdio(adaptProc(child) as never);
|
|
157
|
+
|
|
158
|
+
adapter.sendRawNDJSON(userMessage("Reply with exactly the word PONG and nothing else."));
|
|
159
|
+
|
|
160
|
+
const done = await Promise.race([
|
|
161
|
+
new Promise<boolean>((resolve) => {
|
|
162
|
+
const timer = setInterval(() => {
|
|
163
|
+
if (seen.includes("result")) {
|
|
164
|
+
clearInterval(timer);
|
|
165
|
+
resolve(true);
|
|
166
|
+
}
|
|
167
|
+
}, 250);
|
|
168
|
+
}),
|
|
169
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), 90_000)),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
expect(done, `types seen: ${JSON.stringify(seen)}`).toBe(true);
|
|
173
|
+
// The handshake and turn shape the whole bridge depends on.
|
|
174
|
+
expect(seen, `types seen: ${JSON.stringify(seen)}`).toContain("session_init");
|
|
175
|
+
expect(seen).toContain("assistant");
|
|
176
|
+
expect(assistantText.toUpperCase()).toContain("PONG");
|
|
177
|
+
}, 120_000);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A healthy session must survive a tool call that outruns the probe window.
|
|
181
|
+
*
|
|
182
|
+
* SCOPE — read before trusting this as a regression guard. It was written to
|
|
183
|
+
* reproduce the 0.111.1 bug (probe kills a session busy in a synchronous tool
|
|
184
|
+
* call) and it does NOT reproduce it on CLI 2.1.220: the test passes with the
|
|
185
|
+
* tool-call grace removed, because this CLI keeps emitting frames throughout a
|
|
186
|
+
* Bash call, so the transport never goes silent and the probe never arms. The
|
|
187
|
+
* self-validation below confirms a real tool call ran and outlasted the probe
|
|
188
|
+
* window, so that result is a genuine observation rather than a vacuous pass.
|
|
189
|
+
*
|
|
190
|
+
* The production report came from a different host and CLI build, so the
|
|
191
|
+
* starvation it describes is evidently version- or load-dependent. What this
|
|
192
|
+
* test does guarantee is the invariant that matters day to day: a real session
|
|
193
|
+
* doing real tool work is not torn down. Reproducing the starvation itself
|
|
194
|
+
* needs a CLI that actually stops emitting — not yet reproduced locally.
|
|
195
|
+
*/
|
|
196
|
+
it("does not disconnect a healthy session during a long synchronous tool call", async () => {
|
|
197
|
+
expect(claudeAvailable()).toBe(true);
|
|
198
|
+
|
|
199
|
+
const adapter = new ClaudeAdapter("e2e-longtool");
|
|
200
|
+
let disconnected = false;
|
|
201
|
+
let sawResult = false;
|
|
202
|
+
let sawToolUse = false;
|
|
203
|
+
const started = Date.now();
|
|
204
|
+
let disconnectedAfterMs = -1;
|
|
205
|
+
adapter.onDisconnect(() => { disconnected = true; disconnectedAfterMs = Date.now() - started; });
|
|
206
|
+
adapter.onBrowserMessage((msg) => {
|
|
207
|
+
if (msg.type === "result") sawResult = true;
|
|
208
|
+
// Confirm the model actually issued a tool call — without one the CLI is
|
|
209
|
+
// never blocked and the test would pass no matter what the probe does.
|
|
210
|
+
const blocks = (msg as { message?: { content?: Array<{ type: string }> } }).message?.content ?? [];
|
|
211
|
+
if (blocks.some((b) => b.type === "tool_use")) sawToolUse = true;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const child = spawnClaude();
|
|
215
|
+
adapter.attachStdio(adaptProc(child) as never);
|
|
216
|
+
|
|
217
|
+
adapter.sendRawNDJSON(
|
|
218
|
+
userMessage("Run this exact bash command and then reply DONE: sleep 12 && echo slept"),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const finished = await Promise.race([
|
|
222
|
+
new Promise<boolean>((resolve) => {
|
|
223
|
+
const timer = setInterval(() => {
|
|
224
|
+
if (sawResult || disconnected) {
|
|
225
|
+
clearInterval(timer);
|
|
226
|
+
resolve(true);
|
|
227
|
+
}
|
|
228
|
+
}, 250);
|
|
229
|
+
}),
|
|
230
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), 120_000)),
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const elapsed = Date.now() - started;
|
|
234
|
+
expect(finished).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Self-validation: this test is only meaningful if the CLI was actually
|
|
237
|
+
// blocked in a tool call for longer than the probe window. Without these
|
|
238
|
+
// guards a fast, tool-free reply would pass even with the fix removed —
|
|
239
|
+
// a green test proving nothing.
|
|
240
|
+
expect(sawToolUse, "model did not issue a tool call; test is inconclusive").toBe(true);
|
|
241
|
+
expect(elapsed, "turn finished before the probe window; test is inconclusive")
|
|
242
|
+
.toBeGreaterThan(Number(process.env.COMPANION_SILENCE_PROBE_AFTER_MS) +
|
|
243
|
+
Number(process.env.COMPANION_SILENCE_PROBE_TIMEOUT_MS));
|
|
244
|
+
|
|
245
|
+
// The assertion that matters: the session survived a tool call far longer
|
|
246
|
+
// than the probe window.
|
|
247
|
+
expect(disconnected, `disconnected after ${disconnectedAfterMs}ms`).toBe(false);
|
|
248
|
+
expect(sawResult).toBe(true);
|
|
249
|
+
}, 150_000);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Orphaned MCP servers were the largest resource bug found in production (123
|
|
253
|
+
* processes holding 11.9GB). This asserts the narrower invariant that is true
|
|
254
|
+
* for any session: killing the CLI must not leave its descendants behind.
|
|
255
|
+
*/
|
|
256
|
+
it("leaves no descendants behind after the process is killed", async () => {
|
|
257
|
+
expect(claudeAvailable()).toBe(true);
|
|
258
|
+
const { getDescendants, countDescendants } = await import("./proc-diagnostics.js");
|
|
259
|
+
|
|
260
|
+
const adapter = new ClaudeAdapter("e2e-orphans");
|
|
261
|
+
const child = spawnClaude();
|
|
262
|
+
adapter.attachStdio(adaptProc(child) as never);
|
|
263
|
+
|
|
264
|
+
adapter.sendRawNDJSON(userMessage("Reply with exactly: HI"));
|
|
265
|
+
await new Promise((r) => setTimeout(r, 8000));
|
|
266
|
+
|
|
267
|
+
const pid = child.pid!;
|
|
268
|
+
const before = getDescendants(pid);
|
|
269
|
+
|
|
270
|
+
child.kill("SIGKILL");
|
|
271
|
+
await new Promise((r) => child.on("exit", r));
|
|
272
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
273
|
+
|
|
274
|
+
// Any descendant still alive after the parent is gone is an orphan: it has
|
|
275
|
+
// no client and can never acquire one.
|
|
276
|
+
const stillAlive = before.filter((d) => {
|
|
277
|
+
try {
|
|
278
|
+
process.kill(d.pid, 0);
|
|
279
|
+
return true;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
for (const d of stillAlive) {
|
|
286
|
+
try { process.kill(d.pid, "SIGKILL"); } catch { /* cleanup */ }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
expect(countDescendants(pid)).toBe(0);
|
|
290
|
+
// Reported rather than asserted: a stock session with no MCP servers
|
|
291
|
+
// configured has nothing to orphan, so a zero here is not proof of the fix.
|
|
292
|
+
if (stillAlive.length > 0) {
|
|
293
|
+
console.warn(`[e2e] ${stillAlive.length} descendant(s) outlived the CLI:`,
|
|
294
|
+
stillAlive.map((d) => `${d.pid}:${d.comm}`).join(", "));
|
|
295
|
+
}
|
|
296
|
+
}, 60_000);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe.runIf(!E2E)("claude CLI end-to-end (skipped)", () => {
|
|
300
|
+
it("is opt-in via COMPANION_E2E=1", () => {
|
|
301
|
+
// Present so the file is never silently empty, which would look like
|
|
302
|
+
// passing coverage while testing nothing.
|
|
303
|
+
expect(E2E).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
});
|