claude-code-session-manager 0.93.0 → 0.95.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/{DataModel-DDGx3qrn.js → DataModel-DZK9QZpV.js} +1 -1
- package/dist/assets/{History-CHBjYZyD.js → History-BDyJUGtR.js} +2 -2
- package/dist/assets/{Hooks-DG4c-Rzz.js → Hooks-CmK6l0uf.js} +3 -3
- package/dist/assets/HostBilko-D8TIx-Bx.js +1 -0
- package/dist/assets/{Library-CN1h5rAE.js → Library-DTxpOjx8.js} +25 -25
- package/dist/assets/{MarkdownEditor-DmBxrhdn.js → MarkdownEditor-BPJyrevq.js} +1 -1
- package/dist/assets/McpServers-B_rvIYDs.js +2 -0
- package/dist/assets/{Memory-Dk4uDCjg.js → Memory-B-9GlHqe.js} +6 -6
- package/dist/assets/{Permissions-BovobeZw.js → Permissions-DhMMp27n.js} +3 -3
- package/dist/assets/{Plugins-DvOf_B62.js → Plugins-DKfAlmMY.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Dutz_4OY.js → ProvenanceBadge-ptlItdCK.js} +1 -1
- package/dist/assets/{SaveBar-CT5u6ZCJ.js → SaveBar-CSbNSl2W.js} +1 -1
- package/dist/assets/Scheduler--rn1QBYc.js +16 -0
- package/dist/assets/{ScopeSwitcher-BUYDpz2v.js → ScopeSwitcher-BJof-GWH.js} +1 -1
- package/dist/assets/{Settings-DIRmr3tL.js → Settings-Dsx6XZP5.js} +3 -3
- package/dist/assets/{SkillReferenceGraph-CdGr6d3_.js → SkillReferenceGraph-CGl4B89-.js} +2 -2
- package/dist/assets/{Skills-BQOheQv4.js → Skills-D522Zfol.js} +2 -2
- package/dist/assets/{SystemPrompt-5LsimKbX.js → SystemPrompt-DKj-q39J.js} +1 -1
- package/dist/assets/{TagLibrary-DreLsT3n.js → TagLibrary-BH7CG7-6.js} +1 -1
- package/dist/assets/{TiptapBody-C9kmlBnI.js → TiptapBody-BbcJL9az.js} +1 -1
- package/dist/assets/{Toggle-BKLCBu9A.js → Toggle-DAslvnnv.js} +1 -1
- package/dist/assets/{index-Db_SK9rW.js → index-Yf8e7VCB.js} +821 -821
- package/dist/assets/{settingsSchema-B5jBhSUP.js → settingsSchema-pzp0TgvN.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -3
- package/scripts/README.md +6 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +5 -2
- package/src/main/__tests__/chat-exit-close-race.test.cjs +8 -2
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +5 -2
- package/src/main/__tests__/health-tick-liveness.test.cjs +12 -0
- package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
- package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
- package/src/main/__tests__/runVerify.test.cjs +12 -1
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +15 -0
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +89 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +45 -10
- package/src/main/crashDiagnostics.cjs +9 -0
- package/src/main/git.cjs +9 -2
- package/src/main/health.cjs +25 -1
- package/src/main/historyAggregator.cjs +2 -19
- package/src/main/index.cjs +7 -8
- package/src/main/ipcSchemas.cjs +13 -3
- package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
- package/src/main/lib/__tests__/loopDelay.test.cjs +68 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +86 -17
- package/src/main/lib/agentPersonaSchema.cjs +2 -0
- package/src/main/lib/childWithLog.cjs +92 -51
- package/src/main/lib/delegationReadiness.cjs +3 -1
- package/src/main/lib/epicSpawnPlan.cjs +27 -8
- package/src/main/lib/epicTranscriptPath.cjs +5 -1
- package/src/main/lib/headTailBuffer.cjs +43 -0
- package/src/main/lib/intradayRefresh.cjs +33 -0
- package/src/main/lib/loopDelay.cjs +44 -0
- package/src/main/lib/lruCache.cjs +39 -0
- package/src/main/lib/openExternalApp.cjs +27 -9
- package/src/main/lib/opsErrorLog.cjs +22 -0
- package/src/main/lib/promptSessionSchema.cjs +2 -0
- package/src/main/lib/rendererRecovery.cjs +141 -0
- package/src/main/lib/scheduleJobSchema.cjs +3 -0
- package/src/main/lib/usageCircuit.cjs +40 -12
- package/src/main/runVerify.cjs +3 -1
- package/src/main/scheduler.cjs +409 -260
- package/src/main/transcripts.cjs +54 -11
- package/src/main/usage.cjs +9 -1
- package/src/preload/api.d.ts +20 -3
- package/dist/assets/HostBilko-DHXcpV2c.js +0 -1
- package/dist/assets/McpServers-Beb_HD6I.js +0 -2
- package/dist/assets/Scheduler-B7GSnZj5.js +0 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ay as b,aj as n,S as N,aN as O,aw as K}from"./index-Db_SK9rW.js";function T({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function B(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:q([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function q(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=B(K)),$}export{T as E,V as s,G as u};
|
|
1
|
+
import{az as b,ak as n,S as N,aO as O,ax as K}from"./index-Yf8e7VCB.js";function T({node:e,targetScope:r,onOverride:s,schema:a,rootLabel:m}){const[f,x]=b.useState(""),[u,o]=b.useState(!0),g=[],d=e.kind==="object"&&Object.keys(e.children).length===0,y=((a==null?void 0:a.rootKeys())??[]).length>0;return n.jsxs("div",{className:"h-full flex flex-col",children:[n.jsxs("div",{className:"shrink-0 border-b border-line bg-bg-elev px-3 py-1.5 flex items-center gap-2 text-xs",children:[n.jsxs("span",{className:"text-fg-faint",children:["merged · overrides → ",n.jsx("span",{className:"text-fg-dim",children:N[r].label})]}),y&&n.jsxs("label",{className:"flex items-center gap-1 text-fg-faint cursor-pointer",children:[n.jsx("input",{type:"checkbox",checked:u,onChange:t=>o(t.target.checked),className:"accent-accent"}),"show unset keys"]}),n.jsx("div",{className:"flex-1"}),n.jsx("input",{value:f,onChange:t=>x(t.target.value),placeholder:"filter keys or descriptions…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-56"})]}),n.jsx("div",{className:"flex-1 overflow-auto px-3 py-2 font-mono text-xs",children:d&&!u?n.jsx("div",{className:"text-fg-faint italic px-2 py-4",children:"no settings resolved — all scope files empty or missing"}):n.jsx(E,{node:e,path:g,label:m??"(root)",targetScope:r,onOverride:s,filter:f,depth:0,schema:a,showGhosts:u,defaultOpen:!0})})]})}function E(e){return e.node.kind==="object"?n.jsx(P,{...e,node:e.node}):e.node.kind==="array"?n.jsx(J,{...e,node:e.node}):n.jsx(R,{...e,node:e.node})}function A(e,r){return r.trim()?e.toLowerCase().includes(r.toLowerCase()):!0}function w(e,r,s){return!!(!s.trim()||e.some(a=>A(a,s))||r!=null&&r.description&&A(r.description,s))}function P({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u,showGhosts:o,defaultOpen:g}){const[d,h]=b.useState(g??x<2),y=(u==null?void 0:u.at(r))??null,t=new Set(Object.keys(e.children)),i=o?(y==null?void 0:y.knownChildren)??[]:[],c=Array.from(new Set([...t,...i])).sort(),l=b.useMemo(()=>f.trim()?c.filter(p=>{const v=(u==null?void 0:u.at([...r,p]))??null;return w([...r,p],v,f)?!0:t.has(p)?L(e.children[p],[...r,p],f,u):!1}):c,[c.join(","),f,e,r.join("."),u]);if(f.trim()&&l.length===0&&!w(r,y,f))return null;if(r.length===0)return n.jsx("div",{children:l.map(p=>j(p))});return n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:x*14},onClick:()=>h(!d),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:d?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" {",t.size,i.length>t.size?`/${new Set([...t,...i]).size}`:"","}"]}),(y==null?void 0:y.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:y.description,children:["— ",y.description]})]}),d&&l.map(p=>j(p))]});function j(p){if(t.has(p))return n.jsx(E,{node:e.children[p],path:[...r,p],label:p,targetScope:a,onOverride:m,filter:f,depth:x+1,schema:u,showGhosts:o},p);const v=(u==null?void 0:u.at([...r,p]))??null;return v?n.jsx(I,{path:[...r,p],label:p,info:v,targetScope:a,onSet:m,depth:x+1,filter:f},p):null}}function L(e,r,s,a){const m=(a==null?void 0:a.at(r))??null;if(w(r,m,s))return!0;if(e.kind!=="object")return!1;for(const f of Object.keys(e.children))if(L(e.children[f],[...r,f],s,a))return!0;return!1}function J({node:e,path:r,label:s,filter:a,depth:m,schema:f}){const[x,u]=b.useState(!0),o=(f==null?void 0:f.at(r))??null;return w(r,o,a)?n.jsxs("div",{children:[n.jsxs("div",{className:"flex items-center gap-1 py-0.5 hover:bg-bg-elev/50 rounded cursor-pointer",style:{paddingLeft:m*14},onClick:()=>u(!x),children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0",children:x?"▾":"▸"}),n.jsx("span",{className:"text-fg",children:s}),n.jsxs("span",{className:"text-fg-faint",children:[" [",e.items.length,"]"]}),n.jsx(S,{tone:"warn",children:"accumulated"}),(o==null?void 0:o.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:o.description,children:["— ",o.description]})]}),x&&e.items.map((g,d)=>n.jsxs("div",{className:"flex items-start gap-2 py-0.5",style:{paddingLeft:(m+1)*14},children:[n.jsx("span",{className:"text-fg-faint w-3 shrink-0"}),n.jsxs("span",{className:"text-fg-faint",children:[d,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-xl",children:k(g.value)}),n.jsx(C,{scope:g.fromScope})]},d))]}):null}function R({node:e,path:r,label:s,targetScope:a,onOverride:m,filter:f,depth:x,schema:u}){const[o,g]=b.useState(!1),d=(u==null?void 0:u.at(r))??null;if(!w(r,d,f))return null;const h=e.winner!==a,y=e.shadowed.length>0;return n.jsxs("div",{children:[n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded",style:{paddingLeft:x*14},children:[n.jsx("span",{className:`w-3 shrink-0 ${y?"text-fg-faint cursor-pointer":""}`,onClick:()=>y&&g(!o),children:y?o?"▾":"▸":""}),n.jsxs("span",{className:"text-fg",children:[s,":"]}),n.jsx("span",{className:"text-fg-dim truncate max-w-md",children:k(e.value)}),n.jsx(C,{scope:e.winner}),(d==null?void 0:d.deprecated)&&n.jsx(S,{tone:"warn",children:"deprecated"}),(d==null?void 0:d.default)!==void 0&&_(e.value,d.default)&&n.jsx(S,{tone:"dim",children:"= default"}),e.typeMismatch&&n.jsx("span",{className:"text-yellow-500/80",title:"shadowed scopes have a different value type",children:"⚠"}),(d==null?void 0:d.description)&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:d.description,children:["— ",d.description]}),n.jsx("div",{className:"flex-1"}),(d==null?void 0:d.enum)&&n.jsx(F,{values:d.enum}),n.jsx("button",{disabled:!h,onClick:()=>m(r,e.value),className:`opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border rounded transition-opacity ${h?"border-accent/50 text-accent hover:bg-accent hover:text-bg":"border-line text-fg-faint cursor-not-allowed"}`,title:h?`copy into ${N[a].label} draft`:`already winning in ${N[a].label}`,children:"override"})]}),o&&e.shadowed.map((t,i)=>n.jsxs("div",{className:"flex items-center gap-2 py-0.5 text-fg-faint line-through",style:{paddingLeft:(x+1)*14+12},children:[n.jsx("span",{children:"shadowed:"}),n.jsx("span",{className:"truncate max-w-xl",children:k(t.value)}),n.jsx(C,{scope:t.scope,dim:!0})]},i))]})}function I({path:e,label:r,info:s,targetScope:a,onSet:m,depth:f,filter:x}){if(!w(e,s,x))return null;const u=s.default!==void 0?s.default:M(s.type),o=Array.isArray(s.type)?s.type.join("|"):s.type??"?";return n.jsxs("div",{className:"group flex items-center gap-2 py-0.5 hover:bg-bg-elev/50 rounded opacity-60",style:{paddingLeft:f*14},children:[n.jsx("span",{className:"w-3 shrink-0"}),n.jsx("span",{className:"text-fg-dim",children:r}),n.jsx(S,{tone:"dim",children:o}),s.required&&n.jsx(S,{tone:"warn",children:"required"}),s.default!==void 0&&n.jsxs("span",{className:"text-fg-faint",children:["default: ",k(s.default)]}),s.description&&n.jsxs("span",{className:"text-fg-faint truncate max-w-md",title:s.description,children:["— ",s.description]}),n.jsx("div",{className:"flex-1"}),s.enum&&n.jsx(F,{values:s.enum}),n.jsx("button",{onClick:()=>m(e,u),className:"opacity-0 group-hover:opacity-100 px-1.5 py-0 text-[10px] border border-accent/50 rounded text-accent hover:bg-accent hover:text-bg transition-opacity",title:`write to ${N[a].label} draft`,children:"set"})]})}function _(e,r){return e===r?!0:typeof e!=typeof r?!1:e&&r&&typeof e=="object"?JSON.stringify(e)===JSON.stringify(r):!1}function M(e){switch(Array.isArray(e)?e[0]:e){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};case"null":return null;default:return null}}function k(e){if(typeof e=="string")return JSON.stringify(e);if(e===null)return"null";if(typeof e=="object"){const r=JSON.stringify(e);return r.length>100?r.slice(0,100)+"…":r}return String(e)}function C({scope:e,dim:r}){const s=e==="user"?"bg-bg-hi text-fg-dim border-line":e==="project"?"bg-blue-950/30 text-blue-400/80 border-blue-900/40":"bg-purple-950/30 text-purple-400/80 border-purple-900/40";return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s} ${r?"opacity-60":""}`,children:N[e].label})}function S({children:e,tone:r="default"}){const s={default:"bg-bg-hi text-fg-dim border-line",warn:"bg-yellow-950/30 text-yellow-500/80 border-yellow-900/40",dim:"bg-bg-elev text-fg-faint border-line"}[r];return n.jsx("span",{className:`text-[10px] px-1 py-0 rounded border uppercase tracking-wide shrink-0 ${s}`,children:e})}function F({values:e}){const r=e.map(s=>String(s)).join(" | ");return n.jsxs("span",{className:"text-[10px] text-fg-faint truncate max-w-xs",title:`allowed: ${r}`,children:["∈ ",r.length>30?r.slice(0,30)+"…":r]})}function G(e,r,s){const a=O(o=>o.files),m=O(o=>o.loadJson),f=O(o=>o.watchFile),x=O(o=>o.unwatchFile),u=b.useMemo(()=>{const o={};for(const g of e.scopes){const d=e.resolve(g,r??"",s);d&&(o[g]=d)}return o},[e,r,s]);return b.useEffect(()=>{const o=Object.values(u).filter(Boolean);return o.forEach(g=>{a[g]||m(g),f(g)}),()=>{o.forEach(g=>x(g))}},[JSON.stringify(u)]),u}function z(e){const r=e??{},s={...r.$defs??{},...r.definitions??{}},a=t=>{if(!t)return{};if(!t.$ref)return x(t);const i=/^#\/(\$defs|definitions)\/(.+)$/.exec(t.$ref);if(!i)return x(t);const c=s[i[2]];return c?a({...c,...u(t)}):x(t)},m=new Set(["__proto__","constructor","prototype"]),f=t=>{if(!t)return{};const i={};for(const[c,l]of Object.entries(t))m.has(c)||(i[c]=l);return i},x=t=>{if(!t.allOf||t.allOf.length===0)return t;const i={...t};delete i.allOf;for(const c of t.allOf){const l=a(c);Object.assign(i,{properties:{...f(i.properties),...f(l.properties)},required:B([...i.required??[],...l.required??[]])}),!i.description&&l.description&&(i.description=l.description),i.default===void 0&&l.default!==void 0&&(i.default=l.default),!i.type&&l.type&&(i.type=l.type)}return i},u=t=>{const{$ref:i,...c}=t;return c},o=(t,i)=>{let c=a(t);for(const l of i){if(!c)return null;if(/^\d+$/.test(l)){const v=Array.isArray(c.items)?c.items[Number(l)]:c.items;c=v?a(v):null;continue}const j=c.properties;if(j&&!m.has(l)&&l in j){c=a(j[l]);continue}const p=c.additionalProperties;if(p&&typeof p=="object"){c=a(p);continue}return null}return c},g=(t,i,c)=>{if(!t)return null;const l={};if(t.description&&(l.description=t.description),t.type&&(l.type=t.type),t.default!==void 0&&(l.default=t.default),t.enum&&(l.enum=t.enum),t.const!==void 0&&(l.const=t.const),t.deprecated&&(l.deprecated=!0),t.examples&&(l.examples=t.examples),t.properties&&(l.knownChildren=Object.keys(t.properties)),c&&(i!=null&&i.includes(c))&&(l.required=!0),t.items&&!Array.isArray(t.items)){const j=a(t.items),p=g(j,void 0,null);p&&(l.itemsInfo=p)}return(t.additionalProperties===!0||typeof t.additionalProperties=="object")&&(l.open=!0),l},d=t=>i=>{if(i.length===0)return g(a(t),void 0,null);const c=o(t,i.slice(0,-1));if(!c)return null;const l=i[i.length-1],j=o(c,[l]);return g(j,c.required,l)},h=t=>{const i=a(t);return Object.keys(i.properties??{})},y=t=>({at:d(t),rootKeys:()=>h(t),rootedAt:i=>{const c=o(t,i);return y(c??{})}});return y(r)}function B(e){return Array.from(new Set(e))}let $=null;function V(){return $||($=z(K)),$}export{T as E,V as s,G as u};
|
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-Yf8e7VCB.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="./assets/index-CKH5Uxik.css">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.95.0",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"dev:renderer": "vite",
|
|
34
34
|
"dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
|
|
35
35
|
"build": "vite build",
|
|
36
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.main.json",
|
|
37
37
|
"start": "electron .",
|
|
38
38
|
"postinstall": "node scripts/postinstall.cjs",
|
|
39
39
|
"prepack": "node scripts/write-build-info.cjs",
|
|
40
40
|
"prepublishOnly": "node scripts/write-build-info.cjs && vite build",
|
|
41
41
|
"test:unit": "vitest run",
|
|
42
|
+
"bench": "node scripts/bench/main-bench.cjs",
|
|
43
|
+
"bench:renderer": "vitest run --config vitest.bench.config.ts",
|
|
42
44
|
"test:e2e": "xvfb-run -a playwright test",
|
|
43
45
|
"smoke:darwin": "playwright test tests/smoke/darwin-boot.spec.ts",
|
|
44
46
|
"test:e2e:mic": "xvfb-run -a playwright test e2e/mic.spec.mjs",
|
|
@@ -53,7 +55,10 @@
|
|
|
53
55
|
"lint:unregistered-tests": "node scripts/check-unregistered-tests.cjs",
|
|
54
56
|
"lint:docs": "node scripts/check-doc-hierarchy.cjs",
|
|
55
57
|
"lint:paths": "node scripts/check-ops-path-literals.cjs",
|
|
56
|
-
"lint": "
|
|
58
|
+
"lint:any": "node scripts/check-explicit-any.cjs",
|
|
59
|
+
"lint:main-ts-check": "node scripts/check-main-ts-check.cjs",
|
|
60
|
+
"lint:filenames": "node scripts/check-tracked-filenames.cjs",
|
|
61
|
+
"lint": "npm run lint:selectors && npm run lint:hooks && npm run lint:unregistered-tests && npm run lint:docs && npm run lint:paths && npm run lint:any && npm run lint:main-ts-check && npm run lint:filenames",
|
|
57
62
|
"sync:settings-schema": "node scripts/sync-settings-schema.cjs"
|
|
58
63
|
},
|
|
59
64
|
"keywords": [
|
package/scripts/README.md
CHANGED
|
@@ -9,9 +9,13 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
|
|
|
9
9
|
| script | invoked by | shipped on npm |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| audit-ops-hygiene.cjs | manual; named in `ops-sweep` + `develop` SKILL.md | yes |
|
|
12
|
+
| bench/main-bench.cjs | `npm run bench` (main-process micro-benches, plain Node); renderer half is `npm run bench:renderer` (`tests/bench/`, `vitest.bench.config.ts`, not in `test:unit`) | no |
|
|
12
13
|
| check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
|
|
13
14
|
| check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
|
|
14
15
|
| check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
|
|
16
|
+
| check-explicit-any.cjs | `npm run lint:any` (in `lint`) — bans explicit `any`/`@ts-ignore` in renderer+preload; per-line `// lint-allow-any: <reason>` | no |
|
|
17
|
+
| check-main-ts-check.cjs | `npm run lint:main-ts-check` (in `lint`) — `tsconfig.main.json` `include` ⇔ `// @ts-check` first line, both directions; explicit list only, `checkJs` false | no |
|
|
18
|
+
| check-tracked-filenames.cjs | `npm run lint:filenames` (in `lint`) — fails on tracked paths with Windows-illegal chars, trailing dot/space, or reserved device names | no |
|
|
15
19
|
| check-unregistered-tests.cjs | `npm run lint:unregistered-tests` (in `lint`) | no |
|
|
16
20
|
| check-unstable-selectors.cjs | `npm run lint:selectors` (in `lint`) | no |
|
|
17
21
|
| cleanup-nested-queue-stubs.cjs | manual one-shot; dispatches to cleanup-worktree-ops-stubs.cjs | no |
|
|
@@ -51,6 +55,8 @@ The four `hooks/guard-*.cjs` are adopted by reference, not copied: `src/main/lib
|
|
|
51
55
|
|
|
52
56
|
`cleanup-nested-queue-stubs.cjs` removes nested queue shards and then dispatches to `cleanup-worktree-ops-stubs.cjs` (its `main()`), which handles the worktree-side stubs.
|
|
53
57
|
|
|
58
|
+
Bench numbers are machine-relative — compare runs on the same machine only; there are no pass/fail thresholds.
|
|
59
|
+
|
|
54
60
|
## npm shipping
|
|
55
61
|
|
|
56
62
|
`ops-sweep.cjs` and `audit-ops-hygiene.cjs` ship: they need only Node built-ins and `src/main/` (also packed), no dev-only deps. `package.json` `files` lists every shipped script individually; `__tests__/package-files.test.cjs` asserts it.
|
|
@@ -37,6 +37,9 @@ process.env.SM_CLAUDE_BIN = stubPath;
|
|
|
37
37
|
|
|
38
38
|
const cr = require('../chatRunner.cjs');
|
|
39
39
|
|
|
40
|
+
// Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
|
|
41
|
+
const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
|
|
42
|
+
|
|
40
43
|
const tick = () => new Promise((r) => setImmediate(r));
|
|
41
44
|
|
|
42
45
|
test('cancelling an ACTIVE run broadcasts a terminal event (unsticks the UI)', async () => {
|
|
@@ -50,7 +53,7 @@ test('cancelling an ACTIVE run broadcasts a terminal event (unsticks the UI)', a
|
|
|
50
53
|
},
|
|
51
54
|
});
|
|
52
55
|
|
|
53
|
-
cr.run({ tabId: 'T', sessionId: 'S', prompt: 'hello', cwd:
|
|
56
|
+
cr.run({ tabId: 'T', sessionId: 'S', prompt: 'hello', cwd: scratchCwd, resume: false });
|
|
54
57
|
|
|
55
58
|
// Let the spawn happen and started event fire.
|
|
56
59
|
for (let i = 0; i < 6; i++) await tick();
|
|
@@ -86,7 +89,7 @@ test('cancel() resolves only AFTER the terminal broadcast fires, not immediately
|
|
|
86
89
|
},
|
|
87
90
|
});
|
|
88
91
|
|
|
89
|
-
cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'hello', cwd:
|
|
92
|
+
cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'hello', cwd: scratchCwd, resume: false });
|
|
90
93
|
|
|
91
94
|
for (let i = 0; i < 6; i++) await tick();
|
|
92
95
|
|
|
@@ -32,6 +32,9 @@ import { test, afterAll } from 'vitest';
|
|
|
32
32
|
const assert = require('node:assert/strict');
|
|
33
33
|
const EventEmitter = require('node:events');
|
|
34
34
|
const cp = require('node:child_process');
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const os = require('node:os');
|
|
37
|
+
const path = require('node:path');
|
|
35
38
|
|
|
36
39
|
// Replace spawn BEFORE chatRunner.cjs is first required, so the module's
|
|
37
40
|
// top-level `const { spawn } = require('node:child_process')` destructuring
|
|
@@ -50,6 +53,9 @@ cp.spawn = () => {
|
|
|
50
53
|
|
|
51
54
|
const cr = require('../chatRunner.cjs');
|
|
52
55
|
|
|
56
|
+
// Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
|
|
57
|
+
const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
|
|
58
|
+
|
|
53
59
|
const tick = () => new Promise((r) => setImmediate(r));
|
|
54
60
|
|
|
55
61
|
test('a final-text-only turn whose stdout chunk lands after exit still surfaces chat:run:complete', async () => {
|
|
@@ -63,7 +69,7 @@ test('a final-text-only turn whose stdout chunk lands after exit still surfaces
|
|
|
63
69
|
},
|
|
64
70
|
});
|
|
65
71
|
|
|
66
|
-
cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'show me a sample post', cwd:
|
|
72
|
+
cr.run({ tabId: 'T2', sessionId: 'S2', prompt: 'show me a sample post', cwd: scratchCwd, resume: false });
|
|
67
73
|
|
|
68
74
|
for (let i = 0; i < 20 && !nextChild; i++) await tick();
|
|
69
75
|
const child = nextChild;
|
|
@@ -108,7 +114,7 @@ test('a crashed process with no result event ever emitted still fires the fallba
|
|
|
108
114
|
},
|
|
109
115
|
});
|
|
110
116
|
|
|
111
|
-
cr.run({ tabId: 'T3', sessionId: 'S3', prompt: 'anything', cwd:
|
|
117
|
+
cr.run({ tabId: 'T3', sessionId: 'S3', prompt: 'anything', cwd: scratchCwd, resume: false });
|
|
112
118
|
|
|
113
119
|
for (let i = 0; i < 20 && !nextChild; i++) await tick();
|
|
114
120
|
const child = nextChild;
|
|
@@ -18,6 +18,9 @@ const assert = require('node:assert/strict');
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
// Scratch project dir under os.tmpdir() — never the real repo root (opsErrorLog would pollute it).
|
|
23
|
+
const scratchCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chat-cwd-'));
|
|
21
24
|
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
22
25
|
|
|
23
26
|
function writeStub(lines) {
|
|
@@ -70,7 +73,7 @@ test('MCP consent denial in a tool_result surfaces a chat:run:notice event', asy
|
|
|
70
73
|
},
|
|
71
74
|
});
|
|
72
75
|
|
|
73
|
-
cr.run({ tabId: 'T-notice', sessionId: 'S-notice', prompt: 'do a design thing', cwd:
|
|
76
|
+
cr.run({ tabId: 'T-notice', sessionId: 'S-notice', prompt: 'do a design thing', cwd: scratchCwd, resume: false });
|
|
74
77
|
|
|
75
78
|
for (let i = 0; i < 60 && !events.some((e) => isTerminal(e.channel)); i++) {
|
|
76
79
|
await new Promise((r) => setTimeout(r, 25));
|
|
@@ -120,7 +123,7 @@ test('a normal tool_result with no consent marker does not fire chat:run:notice'
|
|
|
120
123
|
},
|
|
121
124
|
});
|
|
122
125
|
|
|
123
|
-
cr.run({ tabId: 'T-clean', sessionId: 'S-clean', prompt: 'do a normal thing', cwd:
|
|
126
|
+
cr.run({ tabId: 'T-clean', sessionId: 'S-clean', prompt: 'do a normal thing', cwd: scratchCwd, resume: false });
|
|
124
127
|
|
|
125
128
|
for (let i = 0; i < 60 && !events.some((e) => isTerminal(e.channel)); i++) {
|
|
126
129
|
await new Promise((r) => setTimeout(r, 25));
|
|
@@ -92,6 +92,18 @@ test('no false positive: billing utilization at/above utilizationThreshold (when
|
|
|
92
92
|
assert.strictEqual(result.reason, 'utilization-at-threshold');
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
test('utilization hold with reset days out is flagged longHold; within 5h is benign', () => {
|
|
96
|
+
const far = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, utilizationWindow: 'weekly_all', nextReset: new Date(NOW + 3 * 86_400_000).toISOString() }, NOW);
|
|
97
|
+
assert.strictEqual(far.stalled, false);
|
|
98
|
+
assert.strictEqual(far.longHold, true);
|
|
99
|
+
assert.strictEqual(far.window, 'weekly_all');
|
|
100
|
+
const unknown = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, nextReset: null }, NOW);
|
|
101
|
+
assert.strictEqual(unknown.longHold, true);
|
|
102
|
+
const near = evaluateTickLiveness(baseQueue(), { ts: NOW - 30_000, utilization: 95, utilizationWindow: 'five_hour', nextReset: new Date(NOW + 2 * 3_600_000).toISOString() }, NOW);
|
|
103
|
+
assert.strictEqual(near.stalled, false);
|
|
104
|
+
assert.strictEqual(near.longHold, undefined);
|
|
105
|
+
});
|
|
106
|
+
|
|
95
107
|
test('no false positive: no pending jobs at all', () => {
|
|
96
108
|
const q = baseQueue({ jobs: [] });
|
|
97
109
|
const result = evaluateTickLiveness(q, null, NOW);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
const { createIntradayRefresh } = require('../lib/intradayRefresh.cjs');
|
|
3
|
+
|
|
4
|
+
const mk = (over = {}) => {
|
|
5
|
+
const logs = { writeLine: vi.fn() };
|
|
6
|
+
const releaseLock = vi.fn();
|
|
7
|
+
const deps = { tryAcquireLock: vi.fn(() => true), releaseLock, lockPath: '/x.lock', refresh: vi.fn(async () => {}), logs, ...over };
|
|
8
|
+
return { deps, run: createIntradayRefresh(deps), logs, releaseLock };
|
|
9
|
+
};
|
|
10
|
+
const flush = () => new Promise((r) => setTimeout(r, 0));
|
|
11
|
+
|
|
12
|
+
test('sync throw from refresh releases the lock and logs', async () => {
|
|
13
|
+
const { run, logs, releaseLock } = mk({ refresh: () => { throw new Error('boom'); } });
|
|
14
|
+
expect(() => run()).not.toThrow();
|
|
15
|
+
await flush();
|
|
16
|
+
expect(releaseLock).toHaveBeenCalledWith('/x.lock');
|
|
17
|
+
expect(logs.writeLine).toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('async rejection releases the lock', async () => {
|
|
21
|
+
const { run, releaseLock } = mk({ refresh: async () => { throw new Error('x'); } });
|
|
22
|
+
run(); await flush();
|
|
23
|
+
expect(releaseLock).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('lock-acquire throw is logged, not escaped, and no release', async () => {
|
|
27
|
+
const { run, logs, releaseLock, deps } = mk({ tryAcquireLock: () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); } });
|
|
28
|
+
expect(() => run()).not.toThrow();
|
|
29
|
+
expect(logs.writeLine).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(releaseLock).not.toHaveBeenCalled();
|
|
31
|
+
expect(deps.refresh).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('contended lock skips refresh', () => {
|
|
35
|
+
const { run, deps, releaseLock } = mk({ tryAcquireLock: () => false });
|
|
36
|
+
run();
|
|
37
|
+
expect(deps.refresh).not.toHaveBeenCalled();
|
|
38
|
+
expect(releaseLock).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
const Module = require('node:module');
|
|
3
|
+
const origLoad = Module._load;
|
|
4
|
+
Module._load = function (req, ...rest) {
|
|
5
|
+
if (req === 'electron') return { shell: { openPath: async () => '' } };
|
|
6
|
+
return origLoad.call(this, req, ...rest);
|
|
7
|
+
};
|
|
8
|
+
const { spawnDetached } = require('../lib/openExternalApp.cjs');
|
|
9
|
+
Module._load = origLoad;
|
|
10
|
+
|
|
11
|
+
test('non-existent command resolves ok:false with no uncaught exception', async () => {
|
|
12
|
+
const onUncaught = vi.fn();
|
|
13
|
+
process.on('uncaughtException', onUncaught);
|
|
14
|
+
const r = await spawnDetached('sm-definitely-not-a-real-binary-xyz', ['/tmp']);
|
|
15
|
+
await new Promise((res) => setTimeout(res, 20));
|
|
16
|
+
process.off('uncaughtException', onUncaught);
|
|
17
|
+
expect(r.ok).toBe(false);
|
|
18
|
+
expect(r.error).toMatch(/failed to launch/);
|
|
19
|
+
expect(onUncaught).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('real command resolves ok:true with opener name', async () => {
|
|
23
|
+
const r = await spawnDetached('true', []);
|
|
24
|
+
expect(r).toEqual({ ok: true, opener: 'true' });
|
|
25
|
+
});
|
|
@@ -85,3 +85,25 @@ test('appendError merges caller tags without duplicating auto-derived ones', ()
|
|
|
85
85
|
assert.equal(count, 1);
|
|
86
86
|
assert.ok(line.tags.includes('silent-probe'));
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test('under vitest, a real project root is refused: no file written, no telemetry', () => {
|
|
90
|
+
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
|
91
|
+
const logsBefore = fs.existsSync(path.join(repoRoot, 'session-manager-operations', 'logs'))
|
|
92
|
+
? fs.readdirSync(path.join(repoRoot, 'session-manager-operations', 'logs')).map((f) => [f, fs.statSync(path.join(repoRoot, 'session-manager-operations', 'logs', f)).mtimeMs])
|
|
93
|
+
: null;
|
|
94
|
+
const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
|
|
95
|
+
const saved = require.cache[telemetryPath];
|
|
96
|
+
let calls = 0;
|
|
97
|
+
require.cache[telemetryPath] = { id: telemetryPath, filename: telemetryPath, loaded: true, exports: { reportError: () => { calls++; }, logLine: () => { calls++; } } };
|
|
98
|
+
try {
|
|
99
|
+
opsErrorLog.appendError({ cwd: repoRoot, scope: 'chatRunner', tabId: 'T', message: 'fixture' });
|
|
100
|
+
opsErrorLog.appendError({ cwd: repoRoot, scope: 'chatRunner', level: 'warn', message: 'fixture' });
|
|
101
|
+
} finally {
|
|
102
|
+
if (saved) require.cache[telemetryPath] = saved; else delete require.cache[telemetryPath];
|
|
103
|
+
}
|
|
104
|
+
const logsAfter = fs.existsSync(path.join(repoRoot, 'session-manager-operations', 'logs'))
|
|
105
|
+
? fs.readdirSync(path.join(repoRoot, 'session-manager-operations', 'logs')).map((f) => [f, fs.statSync(path.join(repoRoot, 'session-manager-operations', 'logs', f)).mtimeMs])
|
|
106
|
+
: null;
|
|
107
|
+
assert.deepEqual(logsAfter, logsBefore);
|
|
108
|
+
assert.equal(calls, 0);
|
|
109
|
+
});
|
|
@@ -259,3 +259,24 @@ test('spawn() still refuses session_unreachable without any node-pty spawn', ()
|
|
|
259
259
|
expect(result.pid).toBeNull();
|
|
260
260
|
expect(result.error).toMatch(/cannot be resumed/);
|
|
261
261
|
});
|
|
262
|
+
|
|
263
|
+
test('planEpicSpawn with a REAL on-disk index: project-root caller resumes, vanished-worktree caller is refused spawn_cwd_missing', () => {
|
|
264
|
+
const { planEpicSpawn, __resetForTests } = require('../lib/epicSpawnPlan.cjs');
|
|
265
|
+
__resetForTests();
|
|
266
|
+
const projectRoot = fs.mkdtempSync(path.join(tmpHome, 'sm-plan-root-'));
|
|
267
|
+
const deadWt = path.join(os.tmpdir(), 'sm-plan-dead-worktree-xyz');
|
|
268
|
+
const sessionId = 'epic-session-root-probe';
|
|
269
|
+
writeActiveIndexWithWorktree(projectRoot, { epicId: 'epic-rp', claudeSessionId: sessionId, worktreeDir: deadWt });
|
|
270
|
+
const tdir = path.join(tmpHome, '.claude', 'projects', projectRoot.replace(/[^a-zA-Z0-9]/g, '-'));
|
|
271
|
+
fs.mkdirSync(tdir, { recursive: true });
|
|
272
|
+
fs.writeFileSync(path.join(tdir, `${sessionId}.jsonl`), '{"type":"user"}\n');
|
|
273
|
+
const deps = { homeDir: tmpHome, restoreWorktree: () => null };
|
|
274
|
+
|
|
275
|
+
expect(planEpicSpawn({ cwd: projectRoot, claudeSessionId: sessionId, deps }))
|
|
276
|
+
.toMatchObject({ ok: true, execCwd: projectRoot, useResume: true });
|
|
277
|
+
__resetForTests();
|
|
278
|
+
const refused = planEpicSpawn({ cwd: deadWt, claudeSessionId: sessionId, deps });
|
|
279
|
+
expect(refused).toMatchObject({ ok: false, code: 'spawn_cwd_missing' });
|
|
280
|
+
expect(refused.message).toContain(deadWt);
|
|
281
|
+
expect(refused.message).toMatch(/project cwd/);
|
|
282
|
+
});
|
|
@@ -20,6 +20,8 @@ const {
|
|
|
20
20
|
nextBackoffMs,
|
|
21
21
|
shouldWarnFailureStreak,
|
|
22
22
|
shouldEscalateFailureStreak,
|
|
23
|
+
restoreFailureStreakWarnedAt,
|
|
24
|
+
persistedStreakMinutes,
|
|
23
25
|
BACKOFF_MAX_MS,
|
|
24
26
|
FAILURE_STREAK_WARN_THRESHOLD,
|
|
25
27
|
FAILURE_STREAK_ESCALATION_MS,
|
|
@@ -112,3 +114,15 @@ test('shouldEscalateFailureStreak: re-arms after a streak clears (lastEscalatedA
|
|
|
112
114
|
const laterStreakNow = firstStreakNow + 5 * 60_000; // well under 30 min later
|
|
113
115
|
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, laterStreakNow)).toBe(true);
|
|
114
116
|
});
|
|
117
|
+
|
|
118
|
+
test('restoreFailureStreakWarnedAt backfills now when warned=true has no numeric timestamp', () => {
|
|
119
|
+
expect(restoreFailureStreakWarnedAt(true, null, 5000)).toBe(5000);
|
|
120
|
+
expect(restoreFailureStreakWarnedAt(true, 1000, 5000)).toBe(1000);
|
|
121
|
+
expect(restoreFailureStreakWarnedAt(false, null, 5000)).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('persistedStreakMinutes is always a number, never null', () => {
|
|
125
|
+
expect(persistedStreakMinutes(null, 5000)).toBe(0);
|
|
126
|
+
expect(persistedStreakMinutes(0, 120_000)).toBe(2);
|
|
127
|
+
expect(persistedStreakMinutes(restoreFailureStreakWarnedAt(true, undefined, 5000), 5000)).toBe(0);
|
|
128
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'runVerify.cjs'), 'utf8');
|
|
5
|
+
|
|
6
|
+
test('verdicts sidecar write goes through config.writeJsonSync, not bare writeFileSync', () => {
|
|
7
|
+
expect(src).not.toMatch(/writeFileSync\(verdictsPath/);
|
|
8
|
+
expect(src).toMatch(/config\.cjs'\)\.writeJsonSync\(verdictsPath/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('conclude() invokes the atomic helper at runtime', async () => {
|
|
12
|
+
const config = require('../config.cjs');
|
|
13
|
+
const spy = vi.spyOn(config, 'writeJsonSync').mockReturnValue({ ok: true, mtimeMs: 0 });
|
|
14
|
+
const { verifyRun } = require('../runVerify.cjs');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rv-atomic-'));
|
|
17
|
+
try {
|
|
18
|
+
try {
|
|
19
|
+
await verifyRun({ queueEntry: { slug: 's' }, runDir: dir, prdPath: path.join(dir, 'none.md'), exitCode: 0 });
|
|
20
|
+
} catch { /* signature drift is fine; assert only on the spy */ }
|
|
21
|
+
expect(spy.mock.calls.some(([p]) => p === path.join(dir, 's.verdicts.json'))).toBe(true);
|
|
22
|
+
} finally {
|
|
23
|
+
spy.mockRestore();
|
|
24
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-landed-commit-outranks.test.cjs
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { test } from 'vitest';
|
|
35
|
+
import { test, beforeAll, afterAll, vi } from 'vitest';
|
|
36
36
|
const assert = require('node:assert/strict');
|
|
37
37
|
const os = require('node:os');
|
|
38
38
|
const fs = require('node:fs');
|
|
@@ -40,6 +40,16 @@ const path = require('node:path');
|
|
|
40
40
|
const { execFileSync } = require('node:child_process');
|
|
41
41
|
const { verifyRun } = require('../runVerify.cjs');
|
|
42
42
|
|
|
43
|
+
// Sidecar goes through config.writeJsonSync, whose allow-list rejects os.tmpdir() fixtures.
|
|
44
|
+
let writeSpy;
|
|
45
|
+
beforeAll(() => {
|
|
46
|
+
writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
|
|
47
|
+
require('node:fs').writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
|
|
48
|
+
return { ok: true, mtimeMs: 0 };
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
afterAll(() => writeSpy.mockRestore());
|
|
52
|
+
|
|
43
53
|
const SLUG = '1218-fo-01-move-scripts-lib-into-src-main-lib';
|
|
44
54
|
const FIXTURE_PATH = path.join(__dirname, 'fixtures', `${SLUG}.log`);
|
|
45
55
|
const LANDED_COMMIT = 'd1edf15be50a783f08a55e6de328780f0feb3cb0';
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { test } from 'vitest';
|
|
17
|
+
import { test, beforeAll, afterAll, vi } from 'vitest';
|
|
18
18
|
const assert = require('node:assert/strict');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const fs = require('node:fs');
|
|
@@ -22,6 +22,16 @@ const path = require('node:path');
|
|
|
22
22
|
const { execFileSync } = require('node:child_process');
|
|
23
23
|
const { verifyRun } = require('../runVerify.cjs');
|
|
24
24
|
|
|
25
|
+
// Sidecar goes through config.writeJsonSync, whose allow-list rejects os.tmpdir() fixtures.
|
|
26
|
+
let writeSpy;
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
|
|
29
|
+
require('node:fs').writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
|
|
30
|
+
return { ok: true, mtimeMs: 0 };
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
afterAll(() => writeSpy.mockRestore());
|
|
34
|
+
|
|
25
35
|
// ─── helpers (mirrors runVerify.test.cjs) ─────────────────────────────────────
|
|
26
36
|
|
|
27
37
|
function makeTmpDir() {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Expected: clean → null (caller may stamp 'completed')
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { test } from 'vitest';
|
|
22
|
+
import { test, beforeAll, afterAll, vi } from 'vitest';
|
|
23
23
|
const assert = require('node:assert/strict');
|
|
24
24
|
const os = require('node:os');
|
|
25
25
|
const fs = require('node:fs');
|
|
@@ -27,6 +27,17 @@ const path = require('node:path');
|
|
|
27
27
|
const { execFileSync } = require('node:child_process');
|
|
28
28
|
const { verifyRun, parseLog } = require('../runVerify.cjs');
|
|
29
29
|
|
|
30
|
+
// The verdicts sidecar goes through config.writeJsonSync, whose write allow-list
|
|
31
|
+
// rejects os.tmpdir() fixtures. Route it to a plain write for these fixtures.
|
|
32
|
+
let writeSpy;
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
writeSpy = vi.spyOn(require('../config.cjs'), 'writeJsonSync').mockImplementation((abs, data) => {
|
|
35
|
+
fs.writeFileSync(abs, JSON.stringify(data, null, 2) + '\n');
|
|
36
|
+
return { ok: true, mtimeMs: 0 };
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
afterAll(() => writeSpy.mockRestore());
|
|
40
|
+
|
|
30
41
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
31
42
|
|
|
32
43
|
/** Create a temp directory that is cleaned up after the test. */
|
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
billingResetForPause,
|
|
32
32
|
computeEffectiveResumeAt,
|
|
33
33
|
computeResumeDelay,
|
|
34
|
+
computeDegradedBudget,
|
|
34
35
|
} = require('../scheduler.cjs');
|
|
35
36
|
const billing = require('../usage.cjs');
|
|
36
37
|
|
|
@@ -135,3 +136,17 @@ test('computeResumeDelay: a reset far enough out DOES trip the overflow guard',
|
|
|
135
136
|
const { tooFar } = computeResumeDelay(thirtyDaysOut, now);
|
|
136
137
|
assert.strictEqual(tooFar, true);
|
|
137
138
|
});
|
|
139
|
+
|
|
140
|
+
test('refreshNextReset (via billingResetForPause) seeds lastGoodUsagePayload: a following degraded cycle yields the fetched percent, not 100', async () => {
|
|
141
|
+
const originalFetchUsage = billing.fetchUsage;
|
|
142
|
+
billing.fetchUsage = async () => ({
|
|
143
|
+
kind: 'ok',
|
|
144
|
+
data: { usage: { limits: [{ kind: 'weekly_all', percent: 64, resets_at: '2026-09-24T17:00:00Z', scope: null, is_active: true }] } },
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
await billingResetForPause();
|
|
148
|
+
assert.strictEqual(computeDegradedBudget().utilization, 64);
|
|
149
|
+
} finally {
|
|
150
|
+
billing.fetchUsage = originalFetchUsage;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-utilization-hold.test.cjs — the when-available gate holds on the
|
|
3
|
+
* BINDING window and records a machine-readable hold (window/percent/
|
|
4
|
+
* threshold/reset) on the snapshot; under threshold it dispatches.
|
|
5
|
+
* Run: timeout 180 npx vitest run src/main/__tests__/scheduler-utilization-hold.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let scheduler;
|
|
17
|
+
let billing;
|
|
18
|
+
let originalFetchUsage;
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
originalHome = process.env.HOME;
|
|
22
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-utilhold-'));
|
|
23
|
+
process.env.HOME = tmpHome;
|
|
24
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
26
|
+
scheduler = require('../scheduler.cjs');
|
|
27
|
+
billing = require('../usage.cjs');
|
|
28
|
+
originalFetchUsage = billing.fetchUsage;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
billing.fetchUsage = originalFetchUsage;
|
|
33
|
+
process.env.HOME = originalHome;
|
|
34
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => { billing.fetchUsage = originalFetchUsage; });
|
|
38
|
+
|
|
39
|
+
const RESET = '2026-09-24T17:00:00Z';
|
|
40
|
+
function usageWith(weeklyPct) {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'ok',
|
|
43
|
+
data: {
|
|
44
|
+
usage: {
|
|
45
|
+
five_hour: { utilization: 10, resets_at: '2026-09-21T20:00:00Z' },
|
|
46
|
+
limits: [
|
|
47
|
+
{ kind: 'session', percent: 10, scope: null, resets_at: '2026-09-21T20:00:00Z' },
|
|
48
|
+
{ kind: 'weekly_all', percent: weeklyPct, scope: null, resets_at: RESET },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// dependsOn never resolves so a tick reaches the picker (stamping
|
|
56
|
+
// lastDispatchAttemptAt) without ever spawning a real job.
|
|
57
|
+
function state() {
|
|
58
|
+
return {
|
|
59
|
+
config: { firePolicy: 'when-available', utilizationThreshold: 90 },
|
|
60
|
+
paused: null,
|
|
61
|
+
jobs: [{ slug: '9002-hold', title: 'x', cwd: tmpHome, status: 'pending', dependsOn: ['never'] }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('holds (no tick) and records the hold when the binding weekly window is over threshold', async () => {
|
|
66
|
+
billing.fetchUsage = async () => usageWith(91);
|
|
67
|
+
await scheduler.refreshNextReset();
|
|
68
|
+
const logs = [];
|
|
69
|
+
const orig = console.log;
|
|
70
|
+
console.log = (...a) => { logs.push(a.join(' ')); };
|
|
71
|
+
try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
|
|
72
|
+
const payload = scheduler.buildScheduleStatePayload(state());
|
|
73
|
+
expect(payload.utilizationHold).toEqual({ window: 'weekly_all', percent: 91, threshold: 90, resetsAt: RESET });
|
|
74
|
+
expect(payload.utilizationWindow).toBe('weekly_all');
|
|
75
|
+
expect(logs.some((l) => l.includes('utilization-held') && l.includes('weekly_all'))).toBe(true);
|
|
76
|
+
expect(logs.some((l) => l.includes('— ticking'))).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('ticks and clears the hold when the binding window is under threshold (weekly_all 64)', async () => {
|
|
80
|
+
billing.fetchUsage = async () => usageWith(64);
|
|
81
|
+
await scheduler.refreshNextReset();
|
|
82
|
+
const logs = [];
|
|
83
|
+
const orig = console.log;
|
|
84
|
+
console.log = (...a) => { logs.push(a.join(' ')); };
|
|
85
|
+
try { await scheduler.maybeLaunchWhenAvailable(state()); } finally { console.log = orig; }
|
|
86
|
+
await scheduler.tickQueue();
|
|
87
|
+
expect(scheduler.buildScheduleStatePayload(state()).utilizationHold).toBeNull();
|
|
88
|
+
expect(logs.some((l) => l.includes('— ticking'))).toBe(true);
|
|
89
|
+
});
|