Update coverage reports and fix Ollama client abort handling

This commit is contained in:
2026-05-06 23:44:29 +02:00
parent 0920e0959e
commit cec8302dbe
15 changed files with 1945 additions and 1469 deletions
+181 -181
View File
@@ -23,16 +23,16 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">90.36% </span> <span class="strong">92.55% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>197/218</span> <span class='fraction'>199/215</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">80.28% </span> <span class="strong">85.71% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>57/71</span> <span class='fraction'>60/70</span>
</div> </div>
@@ -44,9 +44,9 @@
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">92.15% </span> <span class="strong">94.52% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>188/204</span> <span class='fraction'>190/201</span>
</div> </div>
@@ -565,22 +565,22 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -596,11 +596,11 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">36x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -640,8 +640,8 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">136x</span> <span class="cline-any cline-yes">146x</span>
<span class="cline-any cline-yes">23x</span> <span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -649,71 +649,71 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span> <span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span> <span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span> <span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">22x</span> <span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">282x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-yes">262x</span> <span class="cline-any cline-yes">262x</span>
<span class="cline-any cline-yes">262x</span>
<span class="cline-any cline-yes">109x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">153x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">153x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-yes">153x</span>
<span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-yes">247x</span>
<span class="cline-any cline-yes">247x</span>
<span class="cline-any cline-yes">104x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">143x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">143x</span>
<span class="cline-any cline-yes">143x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-yes">128x</span> <span class="cline-any cline-yes">133x</span>
<span class="cline-any cline-yes">3656x</span> <span class="cline-any cline-yes">3661x</span>
<span class="cline-any cline-yes">24x</span> <span class="cline-any cline-yes">24x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">262x</span> <span class="cline-any cline-yes">282x</span>
<span class="cline-any cline-yes">33x</span> <span class="cline-any cline-yes">43x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -787,59 +787,61 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">138x</span> <span class="cline-any cline-yes">292x</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">146x</span> <span class="cline-any cline-yes">158x</span>
<span class="cline-any cline-yes">13x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">13x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -852,7 +854,7 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -873,20 +875,20 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -895,109 +897,107 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -1011,8 +1011,8 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-yes">16x</span> <span class="cline-any cline-yes">21x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -1185,12 +1185,12 @@ export class ChatView extends ItemView {
&nbsp; &nbsp;
// Re-attach streaming message if it exists // Re-attach streaming message if it exists
const streamingMessage = messagesSnapshot.find((msg) =&gt; msg.isStreaming); const streamingMessage = messagesSnapshot.find((msg) =&gt; msg.isStreaming);
<span class="missing-if-branch" title="if path not taken" >I</span>if (streamingMessage &amp;&amp; this.lastMessageEl) { if (streamingMessage &amp;&amp; this.lastMessageEl) {
const existingStreamingEl = <span class="cstat-no" title="statement not covered" >container.querySelector(</span> const existingStreamingEl = container.querySelector(
`.ollama-message[data-msg-id="${streamingMessage.id}"]` `.ollama-message[data-msg-id="${streamingMessage.id}"]`
); );
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!existingStreamingEl) {</span> if (!existingStreamingEl) {
<span class="cstat-no" title="statement not covered" > container.appendChild(this.lastMessageEl);</span> container.appendChild(this.lastMessageEl);
} }
} }
} }
@@ -1306,14 +1306,16 @@ export class ChatView extends ItemView {
<span class="cstat-no" title="statement not covered" > context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';</span> <span class="cstat-no" title="statement not covered" > context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';</span>
} }
&nbsp; &nbsp;
const systemContent = context
? <span class="branch-0 cbranch-no" title="branch not covered" >`You are a helpful assistant.\n\nRelevant vault context:\n${context}`</span>
: 'You are a helpful assistant.';
const systemMessage: OllamaMessage = { const systemMessage: OllamaMessage = {
role: 'system', role: 'system',
content: 'You are a helpful assistant.', content: systemContent,
}; };
const userContent = context ? <span class="branch-0 cbranch-no" title="branch not covered" >`${context}\n\n${userMessage}` </span>: userMessage;
const userMessageWithContext: OllamaMessage = { const userMessageWithContext: OllamaMessage = {
role: 'user', role: 'user',
content: userContent, content: userMessage,
}; };
&nbsp; &nbsp;
const messages: OllamaMessage[] = [ const messages: OllamaMessage[] = [
@@ -1421,9 +1423,9 @@ export class ChatView extends ItemView {
for (const result of settledResults) { for (const result of settledResults) {
if (result.status === 'fulfilled') { if (result.status === 'fulfilled') {
toolResults.push(result.value); toolResults.push(result.value);
} else <span class="missing-if-branch" title="else path not taken" >E</span>{ } else {
// Use centralized error handler for tool errors // Use centralized error handler for tool errors
<span class="cstat-no" title="statement not covered" > ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');</span> ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
} }
} }
&nbsp; &nbsp;
@@ -1448,23 +1450,21 @@ export class ChatView extends ItemView {
content: fullResponse, content: fullResponse,
isStreaming: false, isStreaming: false,
}); });
} else <span class="missing-if-branch" title="else path not taken" >E</span>{ } else {
// Even if no tool results were successful, mark streaming as complete // Even if no tool results were successful, mark streaming as complete
// to prevent the assistant message from disappearing // to prevent the assistant message from disappearing
<span class="cstat-no" title="statement not covered" > this.updateMessageById(assistantMessageId, {</span> this.updateMessageById(assistantMessageId, {
content: fullResponse, content: fullResponse,
isStreaming: false, isStreaming: false,
}); });
} }
} }
&nbsp; &nbsp;
// Update last message immutably — only if no tool calls were processed // Update assistant message immutably — only if no tool calls were processed
if (toolCalls.length === 0) { if (toolCalls.length === 0) {
const lastMessageIndex = this.messages.length - 1; this.updateMessageById(assistantMessageId, {
if (lastMessageIndex &gt;= 0) { isStreaming: false,
const lastMessage = { ...this.messages[lastMessageIndex], isStreaming: false }; });
this.messages = [...this.messages.slice(0, lastMessageIndex), lastMessage];
}
} }
&nbsp; &nbsp;
// Limit conversation history to prevent memory issues // Limit conversation history to prevent memory issues
@@ -1501,7 +1501,7 @@ export class ChatView extends ItemView {
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+23 -23
View File
@@ -214,30 +214,30 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">8x</span> <span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -272,15 +272,15 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span> <span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">3x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span> <span class="cline-any cline-yes">9x</span>
@@ -475,7 +475,7 @@ export class ErrorHandler {
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+47 -47
View File
@@ -23,30 +23,30 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">74.47% </span> <span class="strong">93.41% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>499/670</span> <span class='fraction'>624/668</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">59.06% </span> <span class="strong">82.09% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>176/298</span> <span class='fraction'>243/296</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">78.21% </span> <span class="strong">93.06% </span>
<span class="quiet">Functions</span> <span class="quiet">Functions</span>
<span class='fraction'>79/101</span> <span class='fraction'>94/101</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">75.31% </span> <span class="strong">94.51% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>482/640</span> <span class='fraction'>603/638</span>
</div> </div>
@@ -61,7 +61,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class='status-line medium'></div> <div class='status-line high'></div>
<div class="pad1"> <div class="pad1">
<table class="coverage-summary"> <table class="coverage-summary">
<thead> <thead>
@@ -80,17 +80,17 @@
</thead> </thead>
<tbody><tr> <tbody><tr>
<td class="file high" data-value="chat-view.ts"><a href="chat-view.ts.html">chat-view.ts</a></td> <td class="file high" data-value="chat-view.ts"><a href="chat-view.ts.html">chat-view.ts</a></td>
<td data-value="90.36" class="pic high"> <td data-value="92.55" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 90%"></div><div class="cover-empty" style="width: 10%"></div></div> <div class="chart"><div class="cover-fill" style="width: 92%"></div><div class="cover-empty" style="width: 8%"></div></div>
</td> </td>
<td data-value="90.36" class="pct high">90.36%</td> <td data-value="92.55" class="pct high">92.55%</td>
<td data-value="218" class="abs high">197/218</td> <td data-value="215" class="abs high">199/215</td>
<td data-value="80.28" class="pct high">80.28%</td> <td data-value="85.71" class="pct high">85.71%</td>
<td data-value="71" class="abs high">57/71</td> <td data-value="70" class="abs high">60/70</td>
<td data-value="82.85" class="pct high">82.85%</td> <td data-value="82.85" class="pct high">82.85%</td>
<td data-value="35" class="abs high">29/35</td> <td data-value="35" class="abs high">29/35</td>
<td data-value="92.15" class="pct high">92.15%</td> <td data-value="94.52" class="pct high">94.52%</td>
<td data-value="204" class="abs high">188/204</td> <td data-value="201" class="abs high">190/201</td>
</tr> </tr>
<tr> <tr>
@@ -109,18 +109,18 @@
</tr> </tr>
<tr> <tr>
<td class="file low" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td> <td class="file high" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td>
<td data-value="17.7" class="pic low"> <td data-value="91.91" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div> <div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
</td> </td>
<td data-value="17.7" class="pct low">17.7%</td> <td data-value="91.91" class="pct high">91.91%</td>
<td data-value="96" class="abs low">17/96</td> <td data-value="99" class="abs high">91/99</td>
<td data-value="4" class="pct low">4%</td> <td data-value="72" class="pct medium">72%</td>
<td data-value="50" class="abs low">2/50</td> <td data-value="50" class="abs medium">36/50</td>
<td data-value="36.36" class="pct low">36.36%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="11" class="abs low">4/11</td> <td data-value="11" class="abs high">11/11</td>
<td data-value="18.47" class="pct low">18.47%</td> <td data-value="91.57" class="pct high">91.57%</td>
<td data-value="92" class="abs low">17/92</td> <td data-value="95" class="abs high">87/95</td>
</tr> </tr>
<tr> <tr>
@@ -154,33 +154,33 @@
</tr> </tr>
<tr> <tr>
<td class="file low" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td> <td class="file high" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="44.32" class="pic low"> <td data-value="95.87" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 44%"></div><div class="cover-empty" style="width: 56%"></div></div> <div class="chart"><div class="cover-fill" style="width: 95%"></div><div class="cover-empty" style="width: 5%"></div></div>
</td> </td>
<td data-value="44.32" class="pct low">44.32%</td> <td data-value="95.87" class="pct high">95.87%</td>
<td data-value="97" class="abs low">43/97</td> <td data-value="97" class="abs high">93/97</td>
<td data-value="22.91" class="pct low">22.91%</td> <td data-value="85.41" class="pct high">85.41%</td>
<td data-value="48" class="abs low">11/48</td> <td data-value="48" class="abs high">41/48</td>
<td data-value="43.75" class="pct low">43.75%</td> <td data-value="93.75" class="pct high">93.75%</td>
<td data-value="16" class="abs low">7/16</td> <td data-value="16" class="abs high">15/16</td>
<td data-value="43.01" class="pct low">43.01%</td> <td data-value="96.77" class="pct high">96.77%</td>
<td data-value="93" class="abs low">40/93</td> <td data-value="93" class="abs high">90/93</td>
</tr> </tr>
<tr> <tr>
<td class="file high" data-value="vault-indexer.ts"><a href="vault-indexer.ts.html">vault-indexer.ts</a></td> <td class="file high" data-value="vault-indexer.ts"><a href="vault-indexer.ts.html">vault-indexer.ts</a></td>
<td data-value="92.1" class="pic high"> <td data-value="92.85" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 92%"></div><div class="cover-empty" style="width: 8%"></div></div> <div class="chart"><div class="cover-fill" style="width: 92%"></div><div class="cover-empty" style="width: 8%"></div></div>
</td> </td>
<td data-value="92.1" class="pct high">92.1%</td> <td data-value="92.85" class="pct high">92.85%</td>
<td data-value="114" class="abs high">105/114</td> <td data-value="112" class="abs high">104/112</td>
<td data-value="78.26" class="pct medium">78.26%</td> <td data-value="80" class="pct high">80%</td>
<td data-value="46" class="abs medium">36/46</td> <td data-value="45" class="abs high">36/45</td>
<td data-value="100" class="pct high">100%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td> <td data-value="15" class="abs high">15/15</td>
<td data-value="94.33" class="pct high">94.33%</td> <td data-value="95.19" class="pct high">95.19%</td>
<td data-value="106" class="abs high">100/106</td> <td data-value="104" class="abs high">99/104</td>
</tr> </tr>
</tbody> </tbody>
@@ -191,7 +191,7 @@
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+300 -276
View File
@@ -23,30 +23,30 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">17.7% </span> <span class="strong">91.91% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>17/96</span> <span class='fraction'>91/99</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">4% </span> <span class="strong">72% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>2/50</span> <span class='fraction'>36/50</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">36.36% </span> <span class="strong">100% </span>
<span class="quiet">Functions</span> <span class="quiet">Functions</span>
<span class='fraction'>4/11</span> <span class='fraction'>11/11</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">18.47% </span> <span class="strong">91.57% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>17/92</span> <span class='fraction'>87/95</span>
</div> </div>
@@ -61,7 +61,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class='status-line low'></div> <div class='status-line high'></div>
<pre><table class="coverage"> <pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a> <a name='L2'></a><a href='#L2'>2</a>
@@ -293,7 +293,15 @@
<a name='L228'></a><a href='#L228'>228</a> <a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a> <a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a> <a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span> <a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
@@ -306,19 +314,38 @@
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span> <span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span> <span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -327,29 +354,161 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">68x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">67x</span>
<span class="cline-any cline-yes">67x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">53x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">52x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">25x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">4x</span>
@@ -357,166 +516,23 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -560,12 +576,12 @@ export class OllamaClient {
yield* this.streamChatWithRetry(messages, tools, 0); yield* this.streamChatWithRetry(messages, tools, 0);
} }
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > async </span>streamChatAsPromise( async streamChatAsPromise(
messages: OllamaMessage[], messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[]</span> tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[]</span>
): Promise&lt;OllamaMessage[]&gt; { ): Promise&lt;OllamaMessage[]&gt; {
const chunks: OllamaMessage[] = <span class="cstat-no" title="statement not covered" >[];</span> const chunks: OllamaMessage[] = [];
<span class="cstat-no" title="statement not covered" > for await (const chunk of this.streamChat(messages, tools)) {</span> for await (const chunk of this.streamChat(messages, tools)) {
<span class="cstat-no" title="statement not covered" > chunks.push(chunk);</span> <span class="cstat-no" title="statement not covered" > chunks.push(chunk);</span>
} }
<span class="cstat-no" title="statement not covered" > return chunks;</span> <span class="cstat-no" title="statement not covered" > return chunks;</span>
@@ -578,8 +594,8 @@ export class OllamaClient {
): AsyncGenerator&lt;OllamaMessage, void, unknown&gt; { ): AsyncGenerator&lt;OllamaMessage, void, unknown&gt; {
const prevController = this.abortController; const prevController = this.abortController;
this.abortController = new AbortController(); this.abortController = new AbortController();
<span class="missing-if-branch" title="if path not taken" >I</span>if (prevController) { if (prevController) {
<span class="cstat-no" title="statement not covered" > prevController.abort();</span> prevController.abort();
} }
&nbsp; &nbsp;
try { try {
@@ -597,70 +613,70 @@ export class OllamaClient {
signal: this.abortController.signal, signal: this.abortController.signal,
}); });
&nbsp; &nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (!response.ok) { if (!response.ok) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {</span> if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span> const retryDelay = Math.pow(2, attempt) * 100;
<span class="cstat-no" title="statement not covered" > Logger.warn(</span> Logger.warn(
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, `Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
'ollama-client' 'ollama-client'
); );
<span class="cstat-no" title="statement not covered" > await new Promise(<span class="fstat-no" title="function not covered" >(r</span>esolve) =&gt; <span class="cstat-no" title="statement not covered" >setTimeout(resolve, retryDelay))</span>;</span> await new Promise((resolve) =&gt; setTimeout(resolve, retryDelay));
<span class="cstat-no" title="statement not covered" > yield* this.streamChatWithRetry(messages, tools, attempt + 1);</span> yield* this.streamChatWithRetry(messages, tools, attempt + 1);
<span class="cstat-no" title="statement not covered" > return;</span> return;
} }
<span class="cstat-no" title="statement not covered" > throw new ApiError(`Ollama API error: ${response.status}`, response.status);</span> throw new ApiError(`Ollama API error: ${response.status}`, response.status);
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.body) {</span> if (!response.body) {
<span class="cstat-no" title="statement not covered" > throw new Error('No response body');</span> throw new Error('No response body');
} }
&nbsp; &nbsp;
const contentType = <span class="cstat-no" title="statement not covered" >response.headers.get('content-type');</span> const contentType = response.headers.get('content-type');
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!contentType || (!contentType.includes('ndjson') &amp;&amp; !contentType.includes('json'))) {</span> if (!contentType || (!contentType.includes('ndjson') &amp;&amp; !contentType.includes('json'))) {
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid response format');</span> throw new Error('Invalid response format');
} }
&nbsp; &nbsp;
const reader = <span class="cstat-no" title="statement not covered" >response.body.getReader();</span> const reader = response.body.getReader();
const decoder = <span class="cstat-no" title="statement not covered" >new TextDecoder();</span> const decoder = new TextDecoder();
let buffer = <span class="cstat-no" title="statement not covered" >'';</span> let buffer = '';
let malformedCount = <span class="cstat-no" title="statement not covered" >0;</span> let malformedCount = 0;
const maxMalformed = <span class="cstat-no" title="statement not covered" >50;</span> const maxMalformed = 50;
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > try {</span> try {
<span class="cstat-no" title="statement not covered" > while (true) {</span> while (true) {
const { done, value } = <span class="cstat-no" title="statement not covered" >await reader.read();</span> const { done, value } = await reader.read();
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (done) <span class="cstat-no" title="statement not covered" >break;</span></span> if (done) break;
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > buffer += decoder.decode(value, { stream: true });</span> buffer += decoder.decode(value, { stream: true });
&nbsp; &nbsp;
const lines = <span class="cstat-no" title="statement not covered" >buffer.split('\n');</span> const lines = buffer.split('\n');
<span class="cstat-no" title="statement not covered" > buffer = lines.pop() ?? '';</span> buffer = lines.pop() ?? <span class="branch-1 cbranch-no" title="branch not covered" >'';</span>
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > for (const line of lines) {</span> for (const line of lines) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!line.trim()) <span class="cstat-no" title="statement not covered" >continue;</span></span> if (!line.trim()) continue;
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > try {</span> try {
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(line) as Record&lt;string, unknown&gt;;</span> const parsed = JSON.parse(line) as Record&lt;string, unknown&gt;;
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span> this.throwIfOllamaError(parsed);
&nbsp; &nbsp;
const message = <span class="cstat-no" title="statement not covered" >this.toOllamaMessage(parsed.message);</span> const message = this.toOllamaMessage(parsed.message);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!message) {</span> <span class="missing-if-branch" title="if path not taken" >I</span>if (!message) {
<span class="cstat-no" title="statement not covered" > continue;</span> <span class="cstat-no" title="statement not covered" > continue;</span>
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > malformedCount = 0;</span> malformedCount = 0;
<span class="cstat-no" title="statement not covered" > yield message;</span> yield message;
} catch (error) { } catch (error) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {</span> if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {
<span class="cstat-no" title="statement not covered" > throw error;</span> throw error;
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > malformedCount++;</span> malformedCount++;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (malformedCount &gt; maxMalformed) {</span> if (malformedCount &gt; maxMalformed) {
<span class="cstat-no" title="statement not covered" > throw new Error('Too many malformed chunks in stream');</span> throw new Error('Too many malformed chunks in stream');
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > Logger.warn(</span> Logger.warn(
`Skipped malformed chunk: ${line.substring(0, 80)}... - ${(error as Error).message}`, `Skipped malformed chunk: ${line.substring(0, 80)}... - ${(error as Error).message}`,
'ollama-client' 'ollama-client'
); );
@@ -668,14 +684,14 @@ export class OllamaClient {
} }
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (buffer.trim()) {</span> if (buffer.trim()) {
<span class="cstat-no" title="statement not covered" > try {</span> try {
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(buffer) as Record&lt;string, unknown&gt;;</span> const parsed = JSON.parse(buffer) as Record&lt;string, unknown&gt;;
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span> this.throwIfOllamaError(parsed);
&nbsp; &nbsp;
const message = <span class="cstat-no" title="statement not covered" >this.toOllamaMessage(parsed.message);</span> const message = this.toOllamaMessage(parsed.message);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (message) {</span> if (message) {
<span class="cstat-no" title="statement not covered" > yield message;</span> yield message;
} }
} catch (error) { } catch (error) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {</span> <span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {</span>
@@ -688,67 +704,75 @@ export class OllamaClient {
} }
} }
} finally { } finally {
<span class="cstat-no" title="statement not covered" > reader.releaseLock();</span> reader.releaseLock();
} }
} finally { } finally {
this.abortController = null; this.abortController = null;
} }
} }
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > async </span>chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise&lt;OllamaMessage&gt; { async chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise&lt;OllamaMessage&gt; {
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, 0);</span> return this.chatWithRetry(messages, tools, 0);
} }
&nbsp; &nbsp;
private <span class="fstat-no" title="function not covered" >async </span>chatWithRetry( private async chatWithRetry(
messages: OllamaMessage[], messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[],</span> tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[],</span>
attempt: number = <span class="branch-0 cbranch-no" title="branch not covered" >0</span> attempt: number = <span class="branch-0 cbranch-no" title="branch not covered" >0</span>
): Promise&lt;OllamaMessage&gt; { ): Promise&lt;OllamaMessage&gt; {
const response = <span class="cstat-no" title="statement not covered" >await this.fetchFn(`${this.baseURL}/api/chat`, {</span> this.abortController = new AbortController();
method: 'POST', try {
headers: { const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
'Content-Type': 'application/json', method: 'POST',
}, headers: {
body: JSON.stringify({ 'Content-Type': 'application/json',
model: this.model, },
messages, body: JSON.stringify({
tools, model: this.model,
stream: false, messages,
}), tools,
}); stream: false,
}),
signal: this.abortController.signal,
});
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.ok) {</span> if (!response.ok) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {</span> if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span> const retryDelay = Math.pow(2, attempt) * 100;
<span class="cstat-no" title="statement not covered" > Logger.warn(</span> Logger.warn(
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, `Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
'ollama-client' 'ollama-client'
); );
<span class="cstat-no" title="statement not covered" > await new Promise(<span class="fstat-no" title="function not covered" >(r</span>esolve) =&gt; <span class="cstat-no" title="statement not covered" >setTimeout(resolve, retryDelay))</span>;</span> await new Promise((resolve) =&gt; setTimeout(resolve, retryDelay));
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, attempt + 1);</span> return this.chatWithRetry(messages, tools, attempt + 1);
}
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
} }
<span class="cstat-no" title="statement not covered" > throw new ApiError(`Ollama API error: ${response.status}`, response.status);</span>
}
&nbsp; &nbsp;
const data = (<span class="cstat-no" title="statement not covered" >await response.json())</span> as OllamaChatResponse; const data = (await response.json()) as OllamaChatResponse;
<span class="cstat-no" title="statement not covered" > return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };</span> return (
} this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
&nbsp; );
private <span class="fstat-no" title="function not covered" >throwIfOllamaError(</span>parsed: Record&lt;string, unknown&gt;): void { } finally {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.error) {</span> this.abortController = null;
<span class="cstat-no" title="statement not covered" > throw new Error(`Ollama error: ${String(parsed.error)}`);</span>
} }
} }
&nbsp; &nbsp;
private <span class="fstat-no" title="function not covered" >toOllamaMessage(</span>value: unknown): OllamaMessage | null { private throwIfOllamaError(parsed: Record&lt;string, unknown&gt;): void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!value || typeof value !== 'object') {</span> if (parsed.error) {
<span class="cstat-no" title="statement not covered" > return null;</span> throw new Error(`Ollama error: ${String(parsed.error)}`);
}
}
&nbsp;
private toOllamaMessage(value: unknown): OllamaMessage | null {
if (!value || typeof value !== 'object') {
return null;
} }
&nbsp; &nbsp;
const record = <span class="cstat-no" title="statement not covered" >value as Partial&lt;OllamaMessage&gt;;</span> const record = value as Partial&lt;OllamaMessage&gt;;
<span class="cstat-no" title="statement not covered" > return {</span> return {
role: record.role ?? 'assistant', role: record.role ?? 'assistant',
content: typeof record.content === 'string' ? record.content : '', content: typeof record.content === 'string' ? record.content : <span class="branch-1 cbranch-no" title="branch not covered" >'',</span>
tool_calls: record.tool_calls ?? [], tool_calls: record.tool_calls ?? [],
}; };
} }
@@ -760,7 +784,7 @@ export class OllamaClient {
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+3 -3
View File
@@ -210,8 +210,8 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">58x</span> <span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-yes">58x</span> <span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -463,7 +463,7 @@ export class ToolExecutor {
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+7 -7
View File
@@ -261,9 +261,9 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">19x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -281,9 +281,9 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -604,7 +604,7 @@ export interface VaultIndexEntry {
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+167 -179
View File
@@ -23,30 +23,30 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">44.32% </span> <span class="strong">95.87% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>43/97</span> <span class='fraction'>93/97</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">22.91% </span> <span class="strong">85.41% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>11/48</span> <span class='fraction'>41/48</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">43.75% </span> <span class="strong">93.75% </span>
<span class="quiet">Functions</span> <span class="quiet">Functions</span>
<span class='fraction'>7/16</span> <span class='fraction'>15/16</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">43.01% </span> <span class="strong">96.77% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>40/93</span> <span class='fraction'>90/93</span>
</div> </div>
@@ -61,7 +61,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class='status-line low'></div> <div class='status-line high'></div>
<pre><table class="coverage"> <pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a> <a name='L2'></a><a href='#L2'>2</a>
@@ -267,15 +267,7 @@
<a name='L202'></a><a href='#L202'>202</a> <a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a> <a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a> <a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a> <a name='L205'></a><a href='#L205'>205</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">5x</span>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
@@ -293,35 +285,35 @@
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">64x</span>
<span class="cline-any cline-yes">63x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -329,71 +321,71 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -402,67 +394,67 @@
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">75x</span> <span class="cline-any cline-yes">117x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">116x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">75x</span> <span class="cline-any cline-yes">116x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">83x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">75x</span> <span class="cline-any cline-yes">58x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">49x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">27x</span> <span class="cline-any cline-yes">41x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span> <span class="cline-any cline-yes">40x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-yes">27x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">39x</span>
<span class="cline-any cline-yes">75x</span> <span class="cline-any cline-yes">39x</span>
<span class="cline-any cline-yes">49x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">78x</span> <span class="cline-any cline-yes">37x</span>
<span class="cline-any cline-yes">121x</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">62x</span>
<span class="cline-any cline-yes">183x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-yes">84x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">59x</span>
<span class="cline-any cline-yes">49x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">37x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -470,20 +462,16 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span> <span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">// src/utils.ts <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">export enum LogLevel {
&nbsp;
// ==================== Logger ====================
&nbsp;
enum LogLevel {
DEBUG = 0, DEBUG = 0,
INFO = 1, INFO = 1,
WARN = 2, WARN = 2,
@@ -500,24 +488,24 @@ const SEVERITY_ORDER: Record&lt;string, number&gt; = {
export class Logger { export class Logger {
private static minLevel: LogLevel = LogLevel.DEBUG; private static minLevel: LogLevel = LogLevel.DEBUG;
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > static </span>setLevel(level: string | LogLevel): void { static setLevel(level: string | LogLevel): void {
<span class="cstat-no" title="statement not covered" > if (typeof level === 'string') {</span> if (typeof level === 'string') {
const lowerLevel = <span class="cstat-no" title="statement not covered" >level.toLowerCase();</span> const lowerLevel = level.toLowerCase();
<span class="cstat-no" title="statement not covered" > Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;</span> Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? LogLevel.DEBUG;
} else { } else {
<span class="cstat-no" title="statement not covered" > Logger.minLevel = level;</span> Logger.minLevel = level;
} }
} }
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > static </span>debug(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void { static debug(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (LogLevel.DEBUG &gt;= Logger.minLevel) {</span> if (LogLevel.DEBUG &gt;= Logger.minLevel) {
<span class="cstat-no" title="statement not covered" > console.debug(`[${category}] DEBUG: ${message}`);</span> console.debug(`[${category}] DEBUG: ${message}`);
} }
} }
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > static </span>info(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void { static info(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (LogLevel.INFO &gt;= Logger.minLevel) {</span> if (LogLevel.INFO &gt;= Logger.minLevel) {
<span class="cstat-no" title="statement not covered" > console.info(`[${category}] INFO: ${message}`);</span> console.info(`[${category}] INFO: ${message}`);
} }
} }
&nbsp; &nbsp;
@@ -527,81 +515,81 @@ export class Logger {
} }
} }
&nbsp; &nbsp;
<span class="fstat-no" title="function not covered" > static </span>error(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void { static error(message: string, category: string = <span class="branch-0 cbranch-no" title="branch not covered" >'general')</span>: void {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (LogLevel.ERROR &gt;= Logger.minLevel) {</span> if (LogLevel.ERROR &gt;= Logger.minLevel) {
<span class="cstat-no" title="statement not covered" > console.error(`[${category}] ERROR: ${message}`);</span> console.error(`[${category}] ERROR: ${message}`);
} }
} }
} }
&nbsp; &nbsp;
// ==================== URL &amp; Model Validation ==================== // ==================== URL &amp; Model Validation ====================
&nbsp; &nbsp;
export function <span class="fstat-no" title="function not covered" >validateOllamaUrl(</span>url: string): { valid: boolean; error?: string } { export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (typeof url !== 'string' || !url.trim()) {</span> if (typeof url !== 'string' || !url.trim()) {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL cannot be empty' };</span> return { valid: false, error: 'URL cannot be empty' };
} }
&nbsp; &nbsp;
const trimmedUrl = <span class="cstat-no" title="statement not covered" >url.trim();</span> const trimmedUrl = url.trim();
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedUrl.endsWith('/')) {</span> if (trimmedUrl.endsWith('/')) {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL should not end with a slash' };</span> return { valid: false, error: 'URL should not end with a slash' };
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > try {</span> try {
const parsed = <span class="cstat-no" title="statement not covered" >new URL(trimmedUrl);</span> const parsed = new URL(trimmedUrl);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.protocol !== 'http:' &amp;&amp; parsed.protocol !== 'https:') {</span> <span class="missing-if-branch" title="if path not taken" >I</span>if (parsed.protocol !== 'http:' &amp;&amp; parsed.protocol !== 'https:') {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span> <span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span>
} }
<span class="cstat-no" title="statement not covered" > return { valid: true };</span> return { valid: true };
} catch { } catch {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span> return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
} }
} }
&nbsp; &nbsp;
export function <span class="fstat-no" title="function not covered" >validateModelName(</span>model: string): { valid: boolean; error?: string } { export function validateModelName(model: string): { valid: boolean; error?: string } {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (typeof model !== 'string') {</span> if (typeof model !== 'string') {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be a string' };</span> return { valid: false, error: 'Model name must be a string' };
} }
&nbsp; &nbsp;
const trimmedModel = <span class="cstat-no" title="statement not covered" >model.trim();</span> const trimmedModel = model.trim();
&nbsp; &nbsp;
// Explicit check for empty string after trimming // Explicit check for empty string after trimming
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!trimmedModel || trimmedModel.length === 0) {</span> if (!trimmedModel || trimmedModel.length === 0) {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name cannot be empty' };</span> return { valid: false, error: 'Model name cannot be empty' };
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedModel.length &lt; 2) {</span> if (trimmedModel.length &lt; 2) {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be at least 2 characters long' };</span> return { valid: false, error: 'Model name must be at least 2 characters long' };
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedModel.length &gt; 100) {</span> if (trimmedModel.length &gt; 100) {
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be less than 100 characters long' };</span> return { valid: false, error: 'Model name must be less than 100 characters long' };
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {</span> if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {
<span class="cstat-no" title="statement not covered" > return {</span> return {
valid: false, valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons', error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons',
}; };
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > return { valid: true };</span> return { valid: true };
} }
&nbsp; &nbsp;
export function <span class="fstat-no" title="function not covered" >validatePluginSettings(</span>settings: { ollamaUrl: string; model: string }): string[] { export function validatePluginSettings(settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = <span class="cstat-no" title="statement not covered" >[];</span> const errors: string[] = [];
&nbsp; &nbsp;
const urlValidation = <span class="cstat-no" title="statement not covered" >validateOllamaUrl(settings.ollamaUrl);</span> const urlValidation = validateOllamaUrl(settings.ollamaUrl);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!urlValidation.valid) {</span> if (!urlValidation.valid) {
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Ollama URL: ${urlValidation.error}`);</span> errors.push(`Invalid Ollama URL: ${urlValidation.error}`);
} }
&nbsp; &nbsp;
const modelValidation = <span class="cstat-no" title="statement not covered" >validateModelName(settings.model);</span> const modelValidation = validateModelName(settings.model);
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!modelValidation.valid) {</span> if (!modelValidation.valid) {
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Model Name: ${modelValidation.error}`);</span> errors.push(`Invalid Model Name: ${modelValidation.error}`);
} }
&nbsp; &nbsp;
<span class="cstat-no" title="statement not covered" > return errors;</span> return errors;
} }
&nbsp; &nbsp;
// ==================== Safe JSON Parsing ==================== // ==================== Safe JSON Parsing ====================
@@ -610,8 +598,8 @@ const MAX_JSON_SIZE = 1_000_000;
const MAX_JSON_NESTING = 24; const MAX_JSON_NESTING = 24;
&nbsp; &nbsp;
function countNestingDepth(value: unknown, depth: number = 0): number { function countNestingDepth(value: unknown, depth: number = 0): number {
<span class="missing-if-branch" title="if path not taken" >I</span>if (depth &gt; MAX_JSON_NESTING) { if (depth &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > return depth;</span> return depth;
} }
<span class="missing-if-branch" title="if path not taken" >I</span>if (Array.isArray(value)) { <span class="missing-if-branch" title="if path not taken" >I</span>if (Array.isArray(value)) {
<span class="cstat-no" title="statement not covered" > return Math.max(...value.map(<span class="fstat-no" title="function not covered" >(i</span>tem) =&gt; <span class="cstat-no" title="statement not covered" >countNestingDepth(item, depth + 1))</span>, depth);</span> <span class="cstat-no" title="statement not covered" > return Math.max(...value.map(<span class="fstat-no" title="function not covered" >(i</span>tem) =&gt; <span class="cstat-no" title="statement not covered" >countNestingDepth(item, depth + 1))</span>, depth);</span>
@@ -625,12 +613,12 @@ function countNestingDepth(value: unknown, depth: number = 0): number {
} }
&nbsp; &nbsp;
export function safeParseJson(jsonString: string): unknown { export function safeParseJson(jsonString: string): unknown {
<span class="missing-if-branch" title="if path not taken" >I</span>if (typeof jsonString !== 'string') { if (typeof jsonString !== 'string') {
<span class="cstat-no" title="statement not covered" > throw new Error('Input must be a string');</span> throw new Error('Input must be a string');
} }
&nbsp; &nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (jsonString.length &gt; MAX_JSON_SIZE) { if (jsonString.length &gt; MAX_JSON_SIZE) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON input too large');</span> throw new Error('JSON input too large');
} }
&nbsp; &nbsp;
let parsed: unknown; let parsed: unknown;
@@ -647,8 +635,8 @@ export function safeParseJson(jsonString: string): unknown {
} }
&nbsp; &nbsp;
const dangerousKeys = ['constructor', 'prototype', '__proto__']; const dangerousKeys = ['constructor', 'prototype', '__proto__'];
<span class="missing-if-branch" title="if path not taken" >I</span>if (dangerousKeys.some((key) =&gt; Object.keys(obj).includes(key))) { if (dangerousKeys.some((key) =&gt; Object.keys(obj).includes(key))) {
<span class="cstat-no" title="statement not covered" > return true;</span> return true;
} }
&nbsp; &nbsp;
// Recursively check nested objects // Recursively check nested objects
@@ -661,13 +649,13 @@ export function safeParseJson(jsonString: string): unknown {
return false; return false;
}; };
&nbsp; &nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (checkDangerousPatterns(parsed)) { if (checkDangerousPatterns(parsed)) {
<span class="cstat-no" title="statement not covered" > throw new Error('dangerous code pattern detected');</span> throw new Error('dangerous code pattern detected');
} }
&nbsp; &nbsp;
// Check nesting depth // Check nesting depth
<span class="missing-if-branch" title="if path not taken" >I</span>if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) { if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON nesting too deep');</span> throw new Error('JSON nesting too deep');
} }
&nbsp; &nbsp;
return parsed; return parsed;
@@ -677,12 +665,12 @@ export function safeParseJson(jsonString: string): unknown {
&nbsp; &nbsp;
// ==================== HTTP Helpers ==================== // ==================== HTTP Helpers ====================
&nbsp; &nbsp;
export function <span class="fstat-no" title="function not covered" >isValidHttpUrl(</span>url: string): boolean { export function isValidHttpUrl(url: string): boolean {
<span class="cstat-no" title="statement not covered" > try {</span> try {
const parsed = <span class="cstat-no" title="statement not covered" >new URL(url);</span> const parsed = new URL(url);
<span class="cstat-no" title="statement not covered" > return parsed.protocol === 'http:' || parsed.protocol === 'https:';</span> return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch { } catch {
<span class="cstat-no" title="statement not covered" > return false;</span> return false;
} }
} }
&nbsp; &nbsp;
@@ -694,7 +682,7 @@ export function <span class="fstat-no" title="function not covered" >isValidHttp
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+26 -38
View File
@@ -23,16 +23,16 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">92.1% </span> <span class="strong">92.85% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>105/114</span> <span class='fraction'>104/112</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">78.26% </span> <span class="strong">80% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>36/46</span> <span class='fraction'>36/45</span>
</div> </div>
@@ -44,9 +44,9 @@
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">94.33% </span> <span class="strong">95.19% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>100/106</span> <span class='fraction'>99/104</span>
</div> </div>
@@ -333,11 +333,7 @@
<a name='L268'></a><a href='#L268'>268</a> <a name='L268'></a><a href='#L268'>268</a>
<a name='L269'></a><a href='#L269'>269</a> <a name='L269'></a><a href='#L269'>269</a>
<a name='L270'></a><a href='#L270'>270</a> <a name='L270'></a><a href='#L270'>270</a>
<a name='L271'></a><a href='#L271'>271</a> <a name='L271'></a><a href='#L271'>271</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<a name='L272'></a><a href='#L272'>272</a>
<a name='L273'></a><a href='#L273'>273</a>
<a name='L274'></a><a href='#L274'>274</a>
<a name='L275'></a><a href='#L275'>275</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
@@ -370,27 +366,27 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">62x</span> <span class="cline-any cline-yes">67x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">62x</span> <span class="cline-any cline-yes">67x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</span> <span class="cline-any cline-yes">33x</span>
<span class="cline-any cline-yes">2x</span> <span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">50x</span> <span class="cline-any cline-yes">50x</span>
<span class="cline-any cline-yes">38x</span> <span class="cline-any cline-yes">38x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -401,11 +397,11 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -442,16 +438,12 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span> <span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span> <span class="cline-any cline-yes">100x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">95x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -478,10 +470,10 @@
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">95x</span> <span class="cline-any cline-yes">100x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">362x</span> <span class="cline-any cline-yes">367x</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span> <span class="cline-any cline-neutral">&nbsp;</span>
@@ -715,10 +707,6 @@ class VaultIndexer {
(result): result is NonNullable&lt;typeof result&gt; =&gt; result !== null (result): result is NonNullable&lt;typeof result&gt; =&gt; result !== null
); );
results.push(...validResults); results.push(...validResults);
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (results.length &gt;= 50) {
<span class="cstat-no" title="statement not covered" > break;</span>
}
} }
&nbsp; &nbsp;
return results; return results;
@@ -892,7 +880,7 @@ export { VaultIndexer };
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T20:36:57.831Z at 2026-05-06T21:42:52.284Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>
+656 -660
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -4,15 +4,15 @@ module.exports = {
testMatch: ['**/tests/**/*.test.ts'], testMatch: ['**/tests/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'json', 'node'], moduleFileExtensions: ['ts', 'js', 'json', 'node'],
transform: { transform: {
'^.+\\.ts$': 'ts-jest', '^.+\\.ts$': [
'ts-jest',
{
tsconfig: 'tsconfig.test.json',
},
],
}, },
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
collectCoverage: true, collectCoverage: true,
coverageReporters: ['text', 'lcov'], coverageReporters: ['text', 'lcov'],
setupFiles: ['./jest.setup.js'], setupFiles: ['./jest.setup.js'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',
},
},
}; };
+33 -25
View File
@@ -179,34 +179,42 @@ export class OllamaClient {
tools: OllamaTool[] = [], tools: OllamaTool[] = [],
attempt: number = 0 attempt: number = 0
): Promise<OllamaMessage> { ): Promise<OllamaMessage> {
const response = await this.fetchFn(`${this.baseURL}/api/chat`, { this.abortController = new AbortController();
method: 'POST', try {
headers: { const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
'Content-Type': 'application/json', method: 'POST',
}, headers: {
body: JSON.stringify({ 'Content-Type': 'application/json',
model: this.model, },
messages, body: JSON.stringify({
tools, model: this.model,
stream: false, messages,
}), tools,
}); stream: false,
}),
signal: this.abortController.signal,
});
if (!response.ok) { if (!response.ok) {
if (response.status >= 500 && attempt < this.maxRetries) { if (response.status >= 500 && attempt < this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100; const retryDelay = Math.pow(2, attempt) * 100;
Logger.warn( Logger.warn(
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, `Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
'ollama-client' 'ollama-client'
); );
await new Promise((resolve) => setTimeout(resolve, retryDelay)); await new Promise((resolve) => setTimeout(resolve, retryDelay));
return this.chatWithRetry(messages, tools, attempt + 1); return this.chatWithRetry(messages, tools, attempt + 1);
}
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
} }
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
}
const data = (await response.json()) as OllamaChatResponse; const data = (await response.json()) as OllamaChatResponse;
return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }; return (
this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
);
} finally {
this.abortController = null;
}
} }
private throwIfOllamaError(parsed: Record<string, unknown>): void { private throwIfOllamaError(parsed: Record<string, unknown>): void {
+1 -5
View File
@@ -1,8 +1,4 @@
// src/utils.ts export enum LogLevel {
// ==================== Logger ====================
enum LogLevel {
DEBUG = 0, DEBUG = 0,
INFO = 1, INFO = 1,
WARN = 2, WARN = 2,
+209 -3
View File
@@ -1,5 +1,6 @@
import { ChatView } from '../src/chat-view'; import { ChatView } from '../src/chat-view';
import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types'; import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types';
import { ErrorHandler } from '../src/error-handler';
// Mock Obsidian types // Mock Obsidian types
interface MockVault { interface MockVault {
@@ -198,6 +199,68 @@ describe('ChatView', () => {
expect(lastMessage.isStreaming).toBe(false); expect(lastMessage.isStreaming).toBe(false);
}); });
it('should handle streaming re-attach when existing streaming element is found', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
(view as any).lastMessageEl = document.createElement('div');
(view as any).lastMessageEl.setAttribute('data-msg-id', 'streaming-123');
// Mock querySelector to return existing element
const container = document.createElement('div');
const existingEl = document.createElement('div');
existingEl.setAttribute('data-msg-id', 'streaming-123');
container.appendChild(existingEl);
// Mock the contentEl to return our container
const originalContentEl = (view as any).contentEl;
(view as any).contentEl = container;
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(container.contains((view as any).lastMessageEl)).toBe(true);
// Restore original contentEl
(view as any).contentEl = originalContentEl;
});
it('should append streaming element to container when no existing element found', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
// Mock querySelector to return null (no existing element)
const container = document.createElement('div');
const originalQuerySelector = container.querySelector;
container.querySelector = jest.fn().mockReturnValue(null);
// Mock the contentEl to return our container
const originalContentEl = (view as any).contentEl;
(view as any).contentEl = container;
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield { role: 'assistant', content: 'response' };
})()
);
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(container.childElementCount).toBeGreaterThan(0);
// Restore original contentEl and querySelector
(view as any).contentEl = originalContentEl;
container.querySelector = originalQuerySelector;
});
it('should limit conversation history to maxMessageHistory', async () => { it('should limit conversation history to maxMessageHistory', async () => {
view['sendButton'] = document.createElement('button'); view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea'); view['inputEl'] = document.createElement('textarea');
@@ -325,6 +388,147 @@ describe('ChatView', () => {
expect((view as any).messages.length).toBeGreaterThan(1); expect((view as any).messages.length).toBeGreaterThan(1);
}); });
it('should handle tool call errors gracefully and continue with partial results', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'create_file', arguments: '{"path":"test.md","content":"test"}' },
},
{
id: 'call_2',
type: 'function',
function: { name: 'nonexistent_tool', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Follow-up response',
tool_calls: [],
});
// Mock tool executor to return mixed results
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async (call) => {
if (call.function.name === 'create_file') {
return { success: true, message: 'File created successfully' };
} else {
throw new Error('Tool not found');
}
});
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).toHaveBeenCalled(); // Should still call follow-up with partial results
expect(errorHandlerSpy).toHaveBeenCalledWith(expect.any(Error), 'ChatView.handleUserInput');
expect((view as any).messages.length).toBeGreaterThan(1);
consoleSpy.mockRestore();
errorHandlerSpy.mockRestore();
});
it('should continue processing even when all tool calls fail', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'nonexistent_tool_1', arguments: '{}' },
},
{
id: 'call_2',
type: 'function',
function: { name: 'nonexistent_tool_2', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Final response despite tool failures',
tool_calls: [],
});
// Mock tool executor to fail for all calls
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async () => {
throw new Error('Tool not found');
});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).not.toHaveBeenCalled(); // Should skip follow-up when no tool results
expect(errorHandlerSpy).toHaveBeenCalledTimes(2); // Should be called for each failed tool call
expect((view as any).messages.length).toBeGreaterThan(1);
errorHandlerSpy.mockRestore();
});
it('should skip follow-up when tool calls result in no successful results', async () => {
view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea');
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
(async function* () {
yield {
role: 'assistant',
content: 'Response with tools',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'nonexistent_tool', arguments: '{}' },
},
],
};
})()
);
const followUpSpy = jest.spyOn(view['ollamaClient'], 'chat').mockResolvedValue({
role: 'assistant',
content: 'Final response',
tool_calls: [],
});
// Mock tool executor to fail for the call
const toolExecutor = view['toolExecutor'];
jest.spyOn(toolExecutor, 'handleToolCall').mockImplementation(async () => {
throw new Error('Tool not found');
});
const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled();
expect(followUpSpy).not.toHaveBeenCalled(); // Should skip follow-up when no tool results
expect(errorHandlerSpy).toHaveBeenCalledTimes(1); // Should be called for each failed tool call
errorHandlerSpy.mockRestore();
});
it('should limit conversation history to maxMessageHistory', async () => { it('should limit conversation history to maxMessageHistory', async () => {
view['sendButton'] = document.createElement('button'); view['sendButton'] = document.createElement('button');
view['inputEl'] = document.createElement('textarea'); view['inputEl'] = document.createElement('textarea');
@@ -383,14 +587,16 @@ describe('ChatView', () => {
})() })()
); );
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorHandlerSpy = jest.spyOn(ErrorHandler, 'handleError').mockImplementation(() => {});
await (view as any).handleUserInput('test'); await (view as any).handleUserInput('test');
expect(chatSpy).toHaveBeenCalled(); expect(chatSpy).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith( expect(errorHandlerSpy).toHaveBeenCalledWith(
'Ollama Plugin Error [ChatView.handleUserInput]: Network error' expect.objectContaining({ message: 'Network error' }),
'ChatView.handleUserInput'
); );
errorHandlerSpy.mockRestore();
}); });
describe('event handlers', () => { describe('event handlers', () => {
+172 -16
View File
@@ -5,6 +5,22 @@ describe('OllamaClient', () => {
let client: OllamaClient; let client: OllamaClient;
let mockFetch: jest.Mock; let mockFetch: jest.Mock;
function createMockReader(data: string) {
const encoder = new TextEncoder();
const encoded = encoder.encode(data);
let called = false;
return {
read: () => {
if (!called) {
called = true;
return Promise.resolve({ done: false, value: encoded });
}
return Promise.resolve({ done: true, value: new Uint8Array(0) });
},
releaseLock: jest.fn(),
};
}
const mockMessages: OllamaMessage[] = [ const mockMessages: OllamaMessage[] = [
{ role: 'system', content: 'You are helpful.' }, { role: 'system', content: 'You are helpful.' },
{ role: 'user', content: 'Hello' }, { role: 'user', content: 'Hello' },
@@ -107,22 +123,6 @@ describe('OllamaClient', () => {
}); });
describe('streamChat', () => { describe('streamChat', () => {
function createMockReader(data: string) {
const encoder = new TextEncoder();
const encoded = encoder.encode(data);
let called = false;
return {
read: () => {
if (!called) {
called = true;
return Promise.resolve({ done: false, value: encoded });
}
return Promise.resolve({ done: true, value: new Uint8Array(0) });
},
releaseLock: jest.fn(),
};
}
it('should send a streaming request and yield chunks', async () => { it('should send a streaming request and yield chunks', async () => {
const streamData = [ const streamData = [
JSON.stringify({ message: { content: 'He' } }), JSON.stringify({ message: { content: 'He' } }),
@@ -357,6 +357,71 @@ describe('OllamaClient', () => {
}); });
}); });
describe('streamChat with retry logic', () => {
it('should retry on 5xx errors and eventually succeed', async () => {
let callCount = 0;
mockFetch.mockImplementation(async () => {
callCount++;
if (callCount === 1) {
return { ok: false, status: 500 };
}
if (callCount === 2) {
return { ok: false, status: 502 };
}
return {
ok: true,
body: {
getReader: () => ({
read: () => Promise.resolve({ done: true, value: new Uint8Array(0) }),
releaseLock: () => {},
}),
},
headers: {
get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null),
},
};
});
const stream = await client.streamChat(mockMessages, mockTools);
const chunks: string[] = [];
for await (const _ of stream) {
chunks.push('chunk');
}
expect(callCount).toBe(3);
expect(chunks.length).toBe(0);
});
it('should give up after maxRetries attempts', async () => {
mockFetch.mockResolvedValue({ ok: false, status: 500 });
const stream = await client.streamChat(mockMessages, mockTools);
await expect(
(async () => {
for await (const _ of stream) {
/* consume */
}
})()
).rejects.toThrow('Ollama API error: 500');
});
it('should not retry on 4xx errors', async () => {
mockFetch.mockResolvedValue({ ok: false, status: 404 });
const stream = await client.streamChat(mockMessages, mockTools);
await expect(
(async () => {
for await (const _ of stream) {
/* consume */
}
})()
).rejects.toThrow('Ollama API error: 404');
});
});
describe('cancelStream', () => { describe('cancelStream', () => {
it('should abort the current request', () => { it('should abort the current request', () => {
client.cancelStream(); client.cancelStream();
@@ -368,4 +433,95 @@ describe('OllamaClient', () => {
expect(client['abortController']).toBeNull(); expect(client['abortController']).toBeNull();
}); });
}); });
describe('streamChat final buffer parsing', () => {
it('should parse final buffer content when stream ends with partial line', async () => {
const streamData = [
JSON.stringify({ message: { content: 'First' } }),
JSON.stringify({ message: { content: 'Second' } }),
JSON.stringify({ message: { content: 'Third' } }),
'', // Final line should be empty to signal end
].join('\n');
const mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: () => mockReader },
headers: {
get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null),
},
});
const stream = await client.streamChat(mockMessages, mockTools);
const chunks: string[] = [];
for await (const chunk of stream) {
chunks.push(chunk.content);
}
expect(chunks).toEqual(['First', 'Second', 'Third']);
expect(mockReader.releaseLock).toHaveBeenCalled();
});
it('should handle malformed final buffer content gracefully', async () => {
const streamData = [
JSON.stringify({ message: { content: 'Valid' } }),
'malformed json',
'', // Final line should be empty to signal end
].join('\n');
const mockReader = createMockReader(streamData);
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: () => mockReader },
headers: {
get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null),
},
});
const stream = await client.streamChat(mockMessages, mockTools);
const chunks: string[] = [];
for await (const chunk of stream) {
chunks.push(chunk.content);
}
expect(chunks).toEqual(['Valid']);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('Skipped malformed chunk')
);
consoleWarnSpy.mockRestore();
});
it('should parse final buffer content even when it contains message data', async () => {
const streamData = [
JSON.stringify({ message: { content: 'First' } }),
'', // Final line should be empty to signal end
JSON.stringify({ message: { content: 'Final' } }),
].join('\n');
const mockReader = createMockReader(streamData);
mockFetch.mockResolvedValue({
ok: true,
body: { getReader: () => mockReader },
headers: {
get: (name: string) => (name === 'content-type' ? 'application/x-ndjson' : null),
},
});
const stream = await client.streamChat(mockMessages, mockTools);
const chunks: string[] = [];
for await (const chunk of stream) {
chunks.push(chunk.content);
}
expect(chunks).toEqual(['First', 'Final']);
expect(mockReader.releaseLock).toHaveBeenCalled();
});
});
}); });
+114
View File
@@ -1,6 +1,7 @@
/** /**
* Unit tests for utility functions, particularly validation functions * Unit tests for utility functions, particularly validation functions
*/ */
import { LogLevel, Logger } from '../src/utils';
import { import {
validateOllamaUrl, validateOllamaUrl,
validateModelName, validateModelName,
@@ -202,3 +203,116 @@ describe('Validation Functions', () => {
}); });
}); });
}); });
describe('Logger', () => {
beforeEach(() => {
// Reset logger level before each test
Logger.setLevel(LogLevel.DEBUG);
});
describe('setLevel', () => {
it('should set level using string values', () => {
Logger.setLevel('debug');
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
Logger.setLevel('info');
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
Logger.setLevel('warn');
expect((Logger as any).minLevel).toBe(LogLevel.WARN);
Logger.setLevel('error');
expect((Logger as any).minLevel).toBe(LogLevel.ERROR);
});
it('should set level using numeric values', () => {
Logger.setLevel(LogLevel.DEBUG);
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
Logger.setLevel(LogLevel.INFO);
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
Logger.setLevel(LogLevel.WARN);
expect((Logger as any).minLevel).toBe(LogLevel.WARN);
Logger.setLevel(LogLevel.ERROR);
expect((Logger as any).minLevel).toBe(LogLevel.ERROR);
});
it('should default to DEBUG for unknown string levels', () => {
Logger.setLevel('unknown-level');
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
});
it('should be case insensitive for string levels', () => {
Logger.setLevel('DEBUG');
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
Logger.setLevel('Info');
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
});
});
describe('logging methods with different levels', () => {
it('should log debug messages when level is DEBUG', () => {
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation();
Logger.setLevel('debug');
Logger.debug('test message', 'test-category');
Logger.info('test message', 'test-category');
Logger.warn('test message', 'test-category');
Logger.error('test message', 'test-category');
expect(consoleSpy).toHaveBeenCalledWith('[test-category] DEBUG: test message');
consoleSpy.mockRestore();
});
it('should not log debug messages when level is INFO', () => {
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation();
Logger.setLevel('info');
Logger.debug('test message', 'test-category');
Logger.info('test message', 'test-category');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should not log debug or info messages when level is WARN', () => {
const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
Logger.setLevel('warn');
Logger.debug('test message', 'test-category');
Logger.info('test message', 'test-category');
Logger.warn('test message', 'test-category');
expect(consoleDebugSpy).not.toHaveBeenCalled();
expect(consoleInfoSpy).not.toHaveBeenCalled();
consoleDebugSpy.mockRestore();
consoleInfoSpy.mockRestore();
});
it('should only log error messages when level is ERROR', () => {
const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
Logger.setLevel('error');
Logger.debug('test message', 'test-category');
Logger.info('test message', 'test-category');
Logger.warn('test message', 'test-category');
Logger.error('test message', 'test-category');
expect(consoleDebugSpy).not.toHaveBeenCalled();
expect(consoleInfoSpy).not.toHaveBeenCalled();
expect(consoleWarnSpy).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith('[test-category] ERROR: test message');
consoleDebugSpy.mockRestore();
consoleInfoSpy.mockRestore();
consoleWarnSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
});
});