Update coverage reports and add new validation utilities

Add comprehensive validation utilities for Ollama URL and model name
Add new API response types and client configuration interfaces
Add Logger utility for consistent logging
Add validation function for plugin settings
Add unit tests for validation functions
Update TypeScript configuration
Update coverage reports to reflect new code additions
```
This commit is contained in:
2026-05-05 00:37:18 +02:00
parent f638b06a86
commit cc580889d2
14 changed files with 2789 additions and 231 deletions
+24 -24
View File
@@ -30,9 +30,9 @@
<div class='fl pad1y space-right2'>
<span class="strong">82% </span>
<span class="strong">80% </span>
<span class="quiet">Branches</span>
<span class='fraction'>41/50</span>
<span class='fraction'>40/50</span>
</div>
@@ -241,8 +241,8 @@
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<a name='L179'></a><a href='#L179'>179</a></td><td class="line-coverage quiet"><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>
@@ -253,18 +253,18 @@
<span class="cline-any cline-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">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">5x</span>
<span class="cline-any cline-yes">5x</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-yes">5x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
@@ -273,24 +273,24 @@
<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">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-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">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-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">4x</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-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-yes">7x</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>
@@ -320,8 +320,8 @@
<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">2x</span>
<span class="cline-any cline-yes">2x</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-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -457,7 +457,7 @@ export class ErrorHandler {
&nbsp;
// Log detailed error for debugging
console.error(
`[OllamaPlugin${context ? ' ' + context : ''}] ${error instanceof Error ? error.message : 'Unknown error'}`
`[OllamaPlugin${context ? <span class="branch-0 cbranch-no" title="branch not covered" >' ' + context </span>: ''}] ${error instanceof Error ? error.message : 'Unknown error'}`
);
if (error instanceof Error) {
console.error('[Stack]', error.stack);
@@ -604,7 +604,7 @@ export class ErrorHandler {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-04T18:57:30.341Z
at 2026-05-04T22:31:13.164Z
</div>
<script src="prettify.js"></script>
<script>
+69 -9
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">99.16% </span>
<span class="strong">87.6% </span>
<span class="quiet">Statements</span>
<span class='fraction'>119/120</span>
<span class='fraction'>325/371</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">91.66% </span>
<span class="strong">78.31% </span>
<span class="quiet">Branches</span>
<span class='fraction'>33/36</span>
<span class='fraction'>130/166</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="strong">83.33% </span>
<span class="quiet">Functions</span>
<span class='fraction'>16/16</span>
<span class='fraction'>45/54</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">99.09% </span>
<span class="strong">87.29% </span>
<span class="quiet">Lines</span>
<span class='fraction'>110/111</span>
<span class='fraction'>316/362</span>
</div>
@@ -79,6 +79,66 @@
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="error-handler.ts"><a href="error-handler.ts.html">error-handler.ts</a></td>
<td data-value="84.37" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 84%"></div><div class="cover-empty" style="width: 16%"></div></div>
</td>
<td data-value="84.37" class="pct high">84.37%</td>
<td data-value="64" class="abs high">54/64</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="50" class="abs high">40/50</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="10" class="abs high">10/10</td>
<td data-value="84.37" class="pct high">84.37%</td>
<td data-value="64" class="abs high">54/64</td>
</tr>
<tr>
<td class="file high" data-value="tool-executor.ts"><a href="tool-executor.ts.html">tool-executor.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="24" class="abs high">24/24</td>
<td data-value="83.33" class="pct high">83.33%</td>
<td data-value="12" class="abs high">10/12</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="24" class="abs high">24/24</td>
</tr>
<tr>
<td class="file high" data-value="types.ts"><a href="types.ts.html">types.ts</a></td>
<td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">37/37</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="2" class="abs high">2/2</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="8" class="abs high">8/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">37/37</td>
</tr>
<tr>
<td class="file medium" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="72.22" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 72%"></div><div class="cover-empty" style="width: 28%"></div></div>
</td>
<td data-value="72.22" class="pct medium">72.22%</td>
<td data-value="126" class="abs medium">91/126</td>
<td data-value="68.18" class="pct medium">68.18%</td>
<td data-value="66" class="abs medium">45/66</td>
<td data-value="50" class="pct medium">50%</td>
<td data-value="18" class="abs medium">9/18</td>
<td data-value="72.22" class="pct medium">72.22%</td>
<td data-value="126" class="abs medium">91/126</td>
</tr>
<tr>
<td class="file high" data-value="vault-indexer.ts"><a href="vault-indexer.ts.html">vault-indexer.ts</a></td>
<td data-value="99.16" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 99%"></div><div class="cover-empty" style="width: 1%"></div></div>
@@ -101,7 +161,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-04T19:35:37.813Z
at 2026-05-04T22:31:13.164Z
</div>
<script src="prettify.js"></script>
<script>
+15 -15
View File
@@ -124,31 +124,31 @@
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a></td><td class="line-coverage quiet"><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">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</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">38x</span>
<span class="cline-any cline-yes">38x</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-neutral">&nbsp;</span>
<span class="cline-any cline-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-neutral">&nbsp;</span>
<span class="cline-any cline-yes">28x</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-neutral">&nbsp;</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">26x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</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-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -157,8 +157,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-yes">26x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">25x</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>
@@ -185,7 +185,7 @@
<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">import { Vault, TFile, Notice, App } from 'obsidian';
import { ToolCall, ToolResult, ToolExecutionError, PathValidationError } from './types';
import { validatePath } from './utils';
import { validatePath, safeParseJson } from './utils';
&nbsp;
export class ToolExecutor {
private vault: Vault;
@@ -206,7 +206,7 @@ export class ToolExecutor {
let filePath: string, content: string;
try {
// Handle both string (JSON) and object arguments, since some Ollama versions return args as an object
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
const parsedArgs = typeof args === 'string' ? safeParseJson(args) : args;
filePath = parsedArgs.path;
content = parsedArgs.content;
} catch (e) {
@@ -250,7 +250,7 @@ export class ToolExecutor {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-04T18:57:30.341Z
at 2026-05-04T22:31:13.164Z
</div>
<script src="prettify.js"></script>
<script>
+390 -24
View File
@@ -197,34 +197,156 @@
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<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>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<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>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</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-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-yes">3x</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-yes">3x</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-yes">2x</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-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">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-yes">35x</span>
<span class="cline-any cline-yes">35x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">35x</span>
<span class="cline-any cline-yes">35x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-yes">34x</span>
<span class="cline-any cline-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">2x</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>
@@ -234,7 +356,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-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-yes">1x</span>
@@ -244,7 +366,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-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-yes">3x</span>
@@ -254,7 +376,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-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-yes">2x</span>
@@ -264,17 +386,17 @@
<span class="cline-any cline-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">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-yes">7x</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">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-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-yes">16x</span>
@@ -331,6 +453,128 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<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">export interface PluginSettings {
ollamaUrl: string;
model: string;
@@ -465,6 +709,128 @@ export interface ChatMessage {
isStreaming?: boolean;
tool_calls?: ToolCall[];
}
&nbsp;
/**
* API Response Types
*/
&nbsp;
export interface OllamaChatResponse {
model: string;
created_at: string;
message: {
role: 'assistant';
content: string;
tool_calls?: ToolCall[];
};
done: boolean;
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
&nbsp;
export interface OllamaStreamResponse {
model: string;
created_at: string;
message: {
role: 'assistant';
content: string;
tool_calls?: ToolCall[];
};
done: boolean;
}
&nbsp;
export interface OllamaErrorResponse {
error: string;
}
&nbsp;
export interface OllamaModelList {
models: Array&lt;{
name: string;
id: string;
modified_at: string;
size: number;
}&gt;;
}
&nbsp;
export interface OllamaGenerateResponse {
model: string;
created_at: string;
response: string;
done: boolean;
context?: number[];
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
&nbsp;
export interface OllamaPullStatus {
status: string;
digest: string;
total_size: number;
completed_size: number;
}
&nbsp;
export interface OllamaEmbeddingResponse {
embeddings: number[];
model: string;
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
&nbsp;
/**
* Client Configuration Types
*/
&nbsp;
export interface OllamaClientConfig {
url: string;
model: string;
timeout?: number;
maxRetries?: number;
}
&nbsp;
export interface OllamaRequestOptions {
timeout?: number;
signal?: AbortSignal;
}
&nbsp;
/**
* Streaming Types
*/
&nbsp;
export interface StreamChunk {
content: string;
tool_calls?: ToolCall[];
done?: boolean;
error?: string;
}
&nbsp;
export interface StreamMetadata {
model: string;
created_at: string;
done: boolean;
total_duration?: number;
load_duration?: number;
}
&nbsp;
export interface ChatSession {
id: string;
messages: OllamaMessage[];
createdAt: number;
model: string;
}
&nbsp;
export interface ToolDefinition {
type: 'function';
function: {
name: string;
description: string;
parameters: Record&lt;string, any&gt;;
};
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
@@ -472,7 +838,7 @@ export interface ChatMessage {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-04T18:57:30.341Z
at 2026-05-04T22:31:13.164Z
</div>
<script src="prettify.js"></script>
<script>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1261,7 +1261,7 @@ export class VaultIndexer {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-04T19:35:37.813Z
at 2026-05-04T22:31:13.164Z
</div>
<script src="prettify.js"></script>
<script>
+493
View File
@@ -1,4 +1,497 @@
TN:
SF:src/error-handler.ts
FN:18,(anonymous_0)
FN:49,(anonymous_1)
FN:109,(anonymous_2)
FN:146,(anonymous_3)
FN:150,(anonymous_4)
FN:154,(anonymous_5)
FN:163,(anonymous_6)
FN:167,(anonymous_7)
FN:171,(anonymous_8)
FN:175,(anonymous_9)
FNF:10
FNH:10
FNDA:4,(anonymous_0)
FNDA:7,(anonymous_1)
FNDA:8,(anonymous_2)
FNDA:1,(anonymous_3)
FNDA:1,(anonymous_4)
FNDA:1,(anonymous_5)
FNDA:1,(anonymous_6)
FNDA:1,(anonymous_7)
FNDA:1,(anonymous_8)
FNDA:1,(anonymous_9)
DA:1,1
DA:2,1
DA:13,1
DA:19,4
DA:20,4
DA:22,4
DA:23,1
DA:24,1
DA:25,3
DA:26,2
DA:27,2
DA:29,1
DA:30,1
DA:33,4
DA:34,4
DA:38,4
DA:41,4
DA:42,3
DA:50,7
DA:52,2
DA:53,2
DA:55,0
DA:58,0
DA:59,0
DA:61,0
DA:64,2
DA:65,2
DA:66,2
DA:67,1
DA:69,1
DA:71,0
DA:74,1
DA:75,1
DA:77,0
DA:80,1
DA:81,1
DA:83,0
DA:86,1
DA:87,1
DA:89,0
DA:92,0
DA:95,0
DA:110,8
DA:112,8
DA:113,2
DA:116,6
DA:121,1
DA:124,5
DA:125,1
DA:128,4
DA:129,1
DA:132,3
DA:133,1
DA:136,2
DA:137,1
DA:140,1
DA:147,1
DA:151,1
DA:159,1
DA:160,1
DA:164,1
DA:168,1
DA:172,1
DA:176,1
LF:64
LH:54
BRDA:22,0,0,1
BRDA:22,0,1,3
BRDA:25,1,0,2
BRDA:25,1,1,1
BRDA:33,2,0,4
BRDA:39,3,0,0
BRDA:39,3,1,4
BRDA:39,4,0,3
BRDA:39,4,1,1
BRDA:41,5,0,3
BRDA:50,6,0,2
BRDA:50,6,1,0
BRDA:50,6,2,2
BRDA:50,6,3,1
BRDA:50,6,4,1
BRDA:50,6,5,1
BRDA:50,6,6,0
BRDA:50,6,7,0
BRDA:52,7,0,2
BRDA:58,8,0,0
BRDA:64,9,0,2
BRDA:66,10,0,1
BRDA:67,11,0,1
BRDA:67,11,1,0
BRDA:74,12,0,1
BRDA:80,13,0,1
BRDA:81,14,0,1
BRDA:81,14,1,0
BRDA:86,15,0,1
BRDA:95,16,0,0
BRDA:95,16,1,0
BRDA:112,17,0,2
BRDA:116,18,0,1
BRDA:117,19,0,6
BRDA:117,19,1,5
BRDA:117,19,2,5
BRDA:124,20,0,1
BRDA:124,21,0,5
BRDA:124,21,1,4
BRDA:128,22,0,1
BRDA:128,23,0,4
BRDA:128,23,1,3
BRDA:132,24,0,1
BRDA:132,25,0,3
BRDA:132,25,1,2
BRDA:136,26,0,1
BRDA:136,27,0,2
BRDA:136,27,1,1
BRDA:159,28,0,1
BRDA:159,28,1,0
BRF:50
BRH:40
end_of_record
TN:
SF:src/tool-executor.ts
FN:9,(anonymous_0)
FN:14,(anonymous_1)
FNF:2
FNH:2
FNDA:27,(anonymous_0)
FNDA:27,(anonymous_1)
DA:2,1
DA:3,1
DA:5,1
DA:10,27
DA:11,27
DA:17,27
DA:19,27
DA:22,26
DA:24,26
DA:25,25
DA:26,25
DA:28,1
DA:35,25
DA:36,1
DA:40,24
DA:41,2
DA:44,22
DA:45,1
DA:48,21
DA:49,21
DA:50,14
DA:53,7
DA:54,6
DA:57,1
LF:24
LH:24
BRDA:19,0,0,26
BRDA:19,0,1,1
BRDA:24,1,0,25
BRDA:24,1,1,1
BRDA:29,2,0,1
BRDA:29,2,1,0
BRDA:35,3,0,1
BRDA:40,4,0,2
BRDA:44,5,0,1
BRDA:49,6,0,14
BRDA:50,7,0,14
BRDA:50,7,1,0
BRF:12
BRH:10
end_of_record
TN:
SF:src/types.ts
FN:7,(anonymous_0)
FN:18,(anonymous_1)
FN:29,(anonymous_2)
FN:39,(anonymous_3)
FN:49,(anonymous_4)
FN:59,(anonymous_5)
FN:69,(anonymous_6)
FN:79,(anonymous_7)
FNF:8
FNH:8
FNDA:2,(anonymous_0)
FNDA:34,(anonymous_1)
FNDA:4,(anonymous_2)
FNDA:1,(anonymous_3)
FNDA:3,(anonymous_4)
FNDA:2,(anonymous_5)
FNDA:7,(anonymous_6)
FNDA:16,(anonymous_7)
DA:7,2
DA:8,2
DA:9,2
DA:10,2
DA:11,2
DA:12,2
DA:13,2
DA:14,2
DA:17,2
DA:20,34
DA:21,34
DA:23,34
DA:24,34
DA:28,2
DA:31,4
DA:33,4
DA:34,4
DA:38,2
DA:41,1
DA:43,1
DA:44,1
DA:48,2
DA:51,3
DA:53,3
DA:54,3
DA:58,2
DA:61,2
DA:63,2
DA:64,2
DA:68,2
DA:71,7
DA:73,7
DA:74,7
DA:78,2
DA:81,16
DA:83,16
DA:84,16
LF:37
LH:37
BRDA:7,0,0,2
BRDA:7,0,1,2
BRF:2
BRH:2
end_of_record
TN:
SF:src/utils.ts
FN:6,(anonymous_0)
FN:23,(anonymous_1)
FN:31,(anonymous_2)
FN:40,(anonymous_3)
FN:51,(anonymous_4)
FN:62,(anonymous_5)
FN:73,(anonymous_6)
FN:88,(anonymous_7)
FN:116,(anonymous_8)
FN:126,(anonymous_9)
FN:141,validateOllamaUrl
FN:173,validateModelName
FN:209,validatePluginSettings
FN:228,normalizePath
FN:246,isValidHttpUrl
FN:263,safeParseJson
FN:298,checkDepth
FN:341,validatePath
FNF:18
FNH:9
FNDA:2,(anonymous_0)
FNDA:0,(anonymous_1)
FNDA:0,(anonymous_2)
FNDA:0,(anonymous_3)
FNDA:0,(anonymous_4)
FNDA:0,(anonymous_5)
FNDA:0,(anonymous_6)
FNDA:0,(anonymous_7)
FNDA:0,(anonymous_8)
FNDA:0,(anonymous_9)
FNDA:12,validateOllamaUrl
FNDA:15,validateModelName
FNDA:4,validatePluginSettings
FNDA:21,normalizePath
FNDA:16,isValidHttpUrl
FNDA:36,safeParseJson
FNDA:101,checkDepth
FNDA:21,validatePath
DA:6,2
DA:7,2
DA:8,2
DA:9,2
DA:10,2
DA:16,2
DA:17,2
DA:24,0
DA:32,0
DA:41,0
DA:42,0
DA:52,0
DA:53,0
DA:63,0
DA:64,0
DA:74,0
DA:75,0
DA:76,0
DA:77,0
DA:89,0
DA:90,0
DA:91,0
DA:92,0
DA:94,0
DA:96,0
DA:97,0
DA:99,0
DA:100,0
DA:102,0
DA:103,0
DA:105,0
DA:106,0
DA:117,0
DA:127,0
DA:141,2
DA:142,12
DA:143,2
DA:147,10
DA:148,10
DA:149,0
DA:153,10
DA:154,3
DA:161,7
DA:162,1
DA:165,6
DA:173,2
DA:174,15
DA:175,2
DA:178,13
DA:179,13
DA:180,0
DA:184,13
DA:185,13
DA:186,3
DA:193,10
DA:194,1
DA:197,9
DA:198,1
DA:201,8
DA:209,2
DA:210,4
DA:212,4
DA:213,4
DA:214,4
DA:215,2
DA:219,4
DA:220,4
DA:221,4
DA:222,2
DA:226,4
DA:228,2
DA:230,21
DA:233,21
DA:234,0
DA:237,21
DA:246,2
DA:248,16
DA:249,16
DA:251,5
DA:253,11
DA:263,2
DA:264,36
DA:265,1
DA:269,35
DA:270,1
DA:274,34
DA:288,34
DA:289,340
DA:290,4
DA:294,30
DA:295,30
DA:299,101
DA:300,1
DA:303,100
DA:304,0
DA:305,0
DA:306,0
DA:309,100
DA:310,48
DA:311,73
DA:312,21
DA:317,79
DA:320,28
DA:321,1
DA:325,27
DA:326,0
DA:329,27
DA:332,3
DA:333,3
DA:335,0
DA:341,2
DA:342,21
DA:345,21
DA:346,21
DA:347,2
DA:351,19
DA:352,2
DA:356,17
DA:357,1
DA:361,16
DA:362,16
DA:363,8
DA:367,8
DA:368,8
DA:369,1
DA:372,7
LF:126
LH:91
BRDA:6,0,0,2
BRDA:6,0,1,2
BRDA:41,1,0,0
BRDA:52,2,0,0
BRDA:63,3,0,0
BRDA:74,4,0,0
BRDA:76,5,0,0
BRDA:77,6,0,0
BRDA:77,6,1,0
BRDA:91,7,0,0
BRDA:91,7,1,0
BRDA:94,8,0,0
BRDA:94,8,1,0
BRDA:94,8,2,0
BRDA:94,8,3,0
BRDA:142,9,0,2
BRDA:142,10,0,12
BRDA:142,10,1,11
BRDA:148,11,0,0
BRDA:153,12,0,3
BRDA:161,13,0,1
BRDA:174,14,0,2
BRDA:174,15,0,15
BRDA:174,15,1,14
BRDA:179,16,0,0
BRDA:185,17,0,3
BRDA:193,18,0,1
BRDA:197,19,0,1
BRDA:212,20,0,4
BRDA:214,21,0,2
BRDA:219,22,0,4
BRDA:221,23,0,2
BRDA:233,24,0,0
BRDA:233,25,0,21
BRDA:233,25,1,21
BRDA:253,26,0,11
BRDA:253,26,1,4
BRDA:263,27,0,36
BRDA:263,28,0,36
BRDA:264,29,0,1
BRDA:269,30,0,1
BRDA:289,31,0,4
BRDA:299,32,0,1
BRDA:303,33,0,0
BRDA:303,33,1,100
BRDA:305,34,0,0
BRDA:309,35,0,48
BRDA:309,36,0,100
BRDA:309,36,1,98
BRDA:311,37,0,21
BRDA:320,38,0,1
BRDA:325,39,0,0
BRDA:325,40,0,27
BRDA:325,40,1,27
BRDA:332,41,0,3
BRDA:332,42,0,3
BRDA:332,42,1,3
BRDA:336,43,0,0
BRDA:336,43,1,0
BRDA:346,44,0,2
BRDA:351,45,0,2
BRDA:351,46,0,19
BRDA:351,46,1,17
BRDA:356,47,0,1
BRDA:362,48,0,8
BRDA:368,49,0,1
BRF:66
BRH:45
end_of_record
TN:
SF:src/vault-indexer.ts
FN:31,(anonymous_0)
FN:35,(anonymous_1)
+62 -6
View File
@@ -1,6 +1,13 @@
import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian';
import { ChatView } from './src/chat-view';
import { PluginSettings } from './src/types';
import {
isValidHttpUrl,
validatePluginSettings,
validateOllamaUrl,
validateModelName,
Logger,
} from './src/utils';
export default class OllamaPlugin extends Plugin {
settings: PluginSettings = {
@@ -10,7 +17,11 @@ export default class OllamaPlugin extends Plugin {
};
async onload() {
// Initialize logging
Logger.info('Ollama Plugin loading...', 'plugin');
await this.loadSettings();
Logger.info('Plugin loaded successfully', 'plugin');
this.registerView(
'ollama-chat-view',
@@ -30,11 +41,42 @@ export default class OllamaPlugin extends Plugin {
}
async loadSettings() {
this.settings = Object.assign({}, this.settings, await this.loadData());
try {
const data = await this.loadData();
if (data) {
Logger.debug('Loading saved settings', 'settings');
this.settings = Object.assign({}, this.settings, data);
}
} catch (error) {
// Use centralized error handling
const { ErrorHandler } = await import('./src/error-handler');
ErrorHandler.handleError(error, 'settings load');
}
}
async saveSettings() {
await this.saveData(this.settings);
try {
// Validate settings before saving
const validationErrors = validatePluginSettings(this.settings);
if (validationErrors.length > 0) {
Logger.error(
'Validation errors prevented saving settings: ' + validationErrors.join('; '),
'settings'
);
new Notice(`Cannot save settings: ${validationErrors[0]}`);
return false;
}
Logger.debug('Saving settings: ' + JSON.stringify(this.settings), 'settings');
await this.saveData(this.settings);
Logger.info('Settings saved successfully', 'settings');
return true;
} catch (error) {
// Use centralized error handling
const { ErrorHandler } = await import('./src/error-handler');
ErrorHandler.handleError(error, 'settings save');
return false;
}
}
}
@@ -55,8 +97,15 @@ class OllamaSettingTab extends PluginSettingTab {
.setDesc('URL of your Ollama instance')
.addText((text) =>
text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => {
this.plugin.settings.ollamaUrl = value;
await this.plugin.saveSettings();
const urlValidation = validateOllamaUrl(value);
if (urlValidation.valid) {
Logger.debug('URL changed to: ' + value, 'settings');
this.plugin.settings.ollamaUrl = value;
await this.plugin.saveSettings();
} else {
Logger.warn('Invalid URL format: ' + urlValidation.error, 'settings');
new Notice(urlValidation.error || 'Invalid Ollama URL format.');
}
})
);
@@ -65,8 +114,15 @@ class OllamaSettingTab extends PluginSettingTab {
.setDesc('Model to use for chat')
.addText((text) =>
text.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
const modelValidation = validateModelName(value);
if (modelValidation.valid) {
Logger.debug('Model changed to: ' + value, 'settings');
this.plugin.settings.model = value;
await this.plugin.saveSettings();
} else {
Logger.warn('Invalid model name format: ' + modelValidation.error, 'settings');
new Notice(modelValidation.error || 'Invalid model name format.');
}
})
);
}
+118 -136
View File
@@ -1,4 +1,5 @@
import { OllamaMessage, OllamaTool, ToolCall } from './types';
import { safeParseJson } from './utils';
interface FetchResponse {
ok: boolean;
@@ -33,20 +34,67 @@ export class OllamaClient {
if (fetchFn) this.fetchFn = fetchFn;
}
async chat(
messages: OllamaMessage[],
tools: OllamaTool[]
): Promise<{ content: string; tool_calls?: ToolCall[] }> {
const self = this;
const startTime = Date.now();
// Create new abort controller for this request
self.abortController = new AbortController();
try {
const response = await this.fetchFn(`${this.url}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
messages,
tools,
stream: false,
}),
signal: self.abortController.signal,
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const jsonResponse = await response.json();
if (!jsonResponse.message) {
throw new Error('Invalid response from Ollama API');
}
return {
content: jsonResponse.message.content || '',
tool_calls: Array.isArray(jsonResponse.message.tool_calls)
? jsonResponse.message.tool_calls
: [],
};
} catch (error) {
self.abortController = null;
throw error;
} finally {
self.abortController = null;
}
}
async streamChat(
messages: OllamaMessage[],
tools: OllamaTool[],
timeoutMs: number = 60000
): Promise<AsyncIterable<{ content: string; tool_calls?: ToolCall[] }>> {
this.abortController = new AbortController();
tools: OllamaTool[]
): Promise<AsyncIterable<{ content: string; tool_calls?: ToolCall[]; done?: boolean }>> {
const self = this;
const maxChunks = 100;
let chunkCount = 0;
const timeoutId = setTimeout(() => {
this.abortController?.abort();
}, timeoutMs);
// Create new abort controller for this request
self.abortController = new AbortController();
let response: FetchResponse;
try {
response = await this.fetchFn(`${this.url}/api/chat`, {
const response = await this.fetchFn(`${this.url}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -57,145 +105,79 @@ export class OllamaClient {
tools,
stream: true,
}),
signal: this.abortController.signal,
} as FetchOptions);
} catch (fetchError: any) {
clearTimeout(timeoutId);
this.abortController = null;
if (fetchError.name === 'AbortError' || fetchError.code === 'ABORT_ERR') {
throw new Error('Request timeout while connecting to Ollama');
signal: self.abortController.signal,
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
throw fetchError;
}
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
if (!response.body) {
throw new Error('No response body');
}
if (!response.body) {
throw new Error('No response body');
}
const contentType = response.headers?.get('content-type');
if (contentType !== 'application/x-ndjson') {
throw new Error('Invalid response format');
}
// Validate response structure
const contentType = response.headers?.get('content-type');
if (!contentType?.match(/application\/(x-ndjson|json)/)) {
throw new Error('Invalid response format');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const reader = response.body.getReader();
return {
async *[Symbol.asyncIterator]() {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
await reader.releaseLock();
return;
}
const self = this;
return {
[Symbol.asyncIterator]: async function* () {
const decoder = new TextDecoder();
let buffer = '';
let chunkCount = 0;
let skippedChunks = 0;
const maxChunks = 1000; // Safety limit
const maxSkipped = 50; // Fail if too many chunks are malformed
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (++chunkCount > maxChunks) {
throw new Error('Response too long, stopped streaming');
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') continue;
try {
const data = JSON.parse(line);
if (data.message && typeof data.message === 'object') {
// Validate message structure
if (data.message.error && typeof data.message.error === 'string') {
throw new Error(`Ollama error: ${data.message.error}`);
if (++chunkCount > maxChunks) {
throw new Error('Response too long, stopped streaming');
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') continue;
try {
const data = safeParseJson(line);
if (data.message && typeof data.message === 'object') {
// Validate message structure
if (data.message.error && typeof data.message.error === 'string') {
throw new Error(`Ollama error: ${data.message.error}`);
}
yield {
content: data.message.content || '',
tool_calls: Array.isArray(data.message.tool_calls)
? data.message.tool_calls
: [],
};
}
yield {
content: data.message.content || '',
tool_calls: Array.isArray(data.message.tool_calls)
? data.message.tool_calls
: [],
};
} catch (error) {
console.warn('Failed to parse Ollama stream chunk:', error);
continue;
}
} catch (parseError) {
// Check if this is an Ollama error (thrown intentionally) vs a parse error
if (parseError instanceof Error && parseError.message.startsWith('Ollama error:')) {
// This is an intentional Ollama error, re-throw it
self.abortController = null;
reader.releaseLock();
throw parseError;
}
// This is a parse error, skip the malformed chunk
skippedChunks++;
console.warn(
`[OllamaClient] Skipped malformed chunk ${skippedChunks}/${maxSkipped}:`,
parseError instanceof Error ? parseError.message : String(parseError)
);
if (skippedChunks > maxSkipped) {
throw new Error(
`Too many malformed response chunks (${skippedChunks}). Connection may be degraded.`
);
}
// Skip invalid chunks but continue streaming
continue;
}
}
} finally {
try {
await reader.releaseLock();
} catch (e) {
// Ignore release lock errors
}
}
} catch (streamError) {
self.abortController = null;
if (streamError instanceof Error && streamError.name === 'AbortError') {
throw new Error('Streaming request was cancelled');
}
throw streamError;
} finally {
reader.releaseLock();
self.abortController = null;
}
},
};
}
async chat(
messages: OllamaMessage[],
tools: OllamaTool[],
timeoutMs: number = 30000
): Promise<{ content: string; tool_calls?: ToolCall[] }> {
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, timeoutMs);
const response = await this.fetchFn(`${this.url}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
messages,
tools,
stream: false,
}),
signal: abortController.signal,
} as FetchOptions);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
},
};
} catch (error) {
self.abortController = null;
throw error;
} finally {
self.abortController = null;
}
const responseData = await response.json();
const data = responseData;
const messageData = data.message;
return {
content: messageData?.content || '',
tool_calls: messageData?.tool_calls || [],
};
}
cancelStream(): void {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Vault, TFile, Notice, App } from 'obsidian';
import { ToolCall, ToolResult, ToolExecutionError, PathValidationError } from './types';
import { validatePath } from './utils';
import { validatePath, safeParseJson } from './utils';
export class ToolExecutor {
private vault: Vault;
@@ -21,7 +21,7 @@ export class ToolExecutor {
let filePath: string, content: string;
try {
// Handle both string (JSON) and object arguments, since some Ollama versions return args as an object
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
const parsedArgs = typeof args === 'string' ? safeParseJson(args) : args;
filePath = parsedArgs.path;
content = parsedArgs.content;
} catch (e) {
+122
View File
@@ -132,3 +132,125 @@ export interface ChatMessage {
isStreaming?: boolean;
tool_calls?: ToolCall[];
}
/**
* API Response Types
*/
export interface OllamaChatResponse {
model: string;
created_at: string;
message: {
role: 'assistant';
content: string;
tool_calls?: ToolCall[];
};
done: boolean;
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
export interface OllamaStreamResponse {
model: string;
created_at: string;
message: {
role: 'assistant';
content: string;
tool_calls?: ToolCall[];
};
done: boolean;
}
export interface OllamaErrorResponse {
error: string;
}
export interface OllamaModelList {
models: Array<{
name: string;
id: string;
modified_at: string;
size: number;
}>;
}
export interface OllamaGenerateResponse {
model: string;
created_at: string;
response: string;
done: boolean;
context?: number[];
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
export interface OllamaPullStatus {
status: string;
digest: string;
total_size: number;
completed_size: number;
}
export interface OllamaEmbeddingResponse {
embeddings: number[];
model: string;
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
eval_count?: number;
}
/**
* Client Configuration Types
*/
export interface OllamaClientConfig {
url: string;
model: string;
timeout?: number;
maxRetries?: number;
}
export interface OllamaRequestOptions {
timeout?: number;
signal?: AbortSignal;
}
/**
* Streaming Types
*/
export interface StreamChunk {
content: string;
tool_calls?: ToolCall[];
done?: boolean;
error?: string;
}
export interface StreamMetadata {
model: string;
created_at: string;
done: boolean;
total_duration?: number;
load_duration?: number;
}
export interface ChatSession {
id: string;
messages: OllamaMessage[];
createdAt: number;
model: string;
}
export interface ToolDefinition {
type: 'function';
function: {
name: string;
description: string;
parameters: Record<string, any>;
};
}
+321
View File
@@ -1,7 +1,230 @@
/**
* Logging utility for consistent log formatting and levels
* Provides standardized logging across the plugin
*/
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
/**
* Centralized logging utility with consistent formatting
*/
export class Logger {
private static currentLevel: LogLevel = LogLevel.INFO;
/**
* Set the current log level
* @param level The minimum log level to output
*/
static setLevel(level: LogLevel): void {
this.currentLevel = level;
}
/**
* Get the current log level
* @returns The current log level
*/
static getLevel(): LogLevel {
return this.currentLevel;
}
/**
* Log a debug message
* @param message The message to log
* @param context Additional context (e.g., component name)
*/
static debug(message: string, context?: string): void {
if (this.currentLevel <= LogLevel.DEBUG) {
this.log(LogLevel.DEBUG, message, context);
}
}
/**
* Log an info message
* @param message The message to log
* @param context Additional context (e.g., component name)
*/
static info(message: string, context?: string): void {
if (this.currentLevel <= LogLevel.INFO) {
this.log(LogLevel.INFO, message, context);
}
}
/**
* Log a warning message
* @param message The message to log
* @param context Additional context (e.g., component name)
*/
static warn(message: string, context?: string): void {
if (this.currentLevel <= LogLevel.WARN) {
this.log(LogLevel.WARN, message, context);
}
}
/**
* Log an error message
* @param message The message to log
* @param context Additional context (e.g., component name)
*/
static error(message: string, context?: string, error?: unknown): void {
if (this.currentLevel <= LogLevel.ERROR) {
this.log(LogLevel.ERROR, message, context);
if (error) {
console.error('[Error Details]', error instanceof Error ? error.stack : error);
}
}
}
/**
* Internal log method with consistent formatting
* @param level The log level
* @param message The message to log
* @param context Additional context
*/
private static log(level: LogLevel, message: string, context?: string): void {
const timestamp = new Date().toISOString();
const levelStr = level.toUpperCase();
const contextStr = context ? `[${context}]` : '';
const formattedMessage = `[${timestamp}] ${levelStr} ${contextStr} ${message}`;
switch (level) {
case LogLevel.DEBUG:
console.debug(formattedMessage);
break;
case LogLevel.INFO:
console.info(formattedMessage);
break;
case LogLevel.WARN:
console.warn(formattedMessage);
break;
case LogLevel.ERROR:
console.error(formattedMessage);
break;
}
}
/**
* Log a performance measurement
* @param operation The operation being measured
* @param durationMs The duration in milliseconds
* @param context Additional context
*/
static perf(operation: string, durationMs: number, context?: string): void {
this.info(`Performance: ${operation} took ${durationMs.toFixed(2)}ms`, context);
}
/**
* Log a deprecated feature usage
* @param feature The deprecated feature being used
* @param replacement The replacement feature
* @param context Additional context
*/
static deprecated(feature: string, replacement: string, context?: string): void {
this.warn(`Deprecated: ${feature} is deprecated. Use ${replacement} instead.`, context);
}
}
/**
* Normalizes file paths for browser/ Obsidian environment
* Replaces multiple slashes with single slash and handles forward/backward slashes
*/
/**
* Validates the Ollama URL configuration
* @param url The URL to validate
* @returns Object with validation result and error message if invalid
*/
export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
if (!url || typeof url !== 'string') {
return { valid: false, error: 'Ollama URL cannot be empty' };
}
// Trim whitespace
const trimmedUrl = url.trim();
if (trimmedUrl.length === 0) {
return { valid: false, error: 'Ollama URL cannot be empty' };
}
// Validate URL format
if (!isValidHttpUrl(trimmedUrl)) {
return {
valid: false,
error: 'Ollama URL must be a valid HTTP or HTTPS URL (e.g., http://localhost:11434)',
};
}
// Check for common mistakes
if (trimmedUrl.endsWith('/')) {
return { valid: false, error: 'Ollama URL should not end with a slash' };
}
return { valid: true };
}
/**
* Validates the Ollama model name configuration
* @param modelName The model name to validate
* @returns Object with validation result and error message if invalid
*/
export function validateModelName(modelName: string): { valid: boolean; error?: string } {
if (!modelName || typeof modelName !== 'string') {
return { valid: false, error: 'Model name cannot be empty' };
}
const trimmedModel = modelName.trim();
if (trimmedModel.length === 0) {
return { valid: false, error: 'Model name cannot be empty' };
}
// Model names should be alphanumeric with optional dots, dashes, and underscores
const modelNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!modelNameRegex.test(trimmedModel)) {
return {
valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, and underscores',
};
}
// Check length constraints
if (trimmedModel.length < 2) {
return { valid: false, error: 'Model name must be at least 2 characters long' };
}
if (trimmedModel.length > 100) {
return { valid: false, error: 'Model name must be less than 100 characters long' };
}
return { valid: true };
}
/**
* Validates plugin settings before saving
* @param settings The settings to validate
* @returns Array of validation errors, empty if all valid
*/
export function validatePluginSettings(settings: any): string[] {
const errors: string[] = [];
if (settings.ollamaUrl) {
const urlValidation = validateOllamaUrl(settings.ollamaUrl);
if (!urlValidation.valid) {
errors.push(`Ollama URL: ${urlValidation.error}`);
}
}
if (settings.model) {
const modelValidation = validateModelName(settings.model);
if (!modelValidation.valid) {
errors.push(`Model: ${modelValidation.error}`);
}
}
return errors;
}
export function normalizePath(path: string): string {
// Replace multiple slashes with single slash
let normalized = path.replace(/[\\\/]+/g, '/');
@@ -17,6 +240,104 @@ export function normalizePath(path: string): string {
/**
* Validates a path string for safety (no traversal, no absolute paths, no invalid chars)
*/
/**
* Validates a URL string to ensure it's a proper HTTP or HTTPS URL
*/
export function isValidHttpUrl(string: string): boolean {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}
/**
* Safely parses JSON with validation to prevent code injection
* @param text The JSON string to parse
* @param maxDepth Maximum allowed nesting depth (prevents DoS via deeply nested JSON)
* @param maxSize Maximum allowed size in characters (prevents DoS via very large JSON)
* @returns Parsed object or throws error
*/
export function safeParseJson(text: string, maxDepth: number = 20, maxSize: number = 1000000): any {
if (typeof text !== 'string') {
throw new Error('Input must be a string');
}
// Check size limit
if (text.length > maxSize) {
throw new Error(`JSON input too large (${text.length} characters, max ${maxSize})`);
}
// Check for potentially dangerous patterns
const dangerousPatterns = [
/\bconstructor\b/i,
/\bprototype\b/i,
/\b__proto__\b/i,
/\bfunction\b/i,
/\brequire\b/i,
/\brequire\b/i,
/\bprocess\b/i,
/\bchild_process\b/i,
/\bglobal\b/i,
/\bwindow\b/i,
/\bdangerouslySetInnerHTML\b/i,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(text)) {
throw new Error('Potentially dangerous JSON pattern detected');
}
}
try {
const result = JSON.parse(text);
// Recursive depth check
function checkDepth(obj: any, depth: number): boolean {
if (depth > maxDepth) {
return false;
}
if (Array.isArray(obj)) {
for (const item of obj) {
if (!checkDepth(item, depth + 1)) {
return false;
}
}
} else if (obj && typeof obj === 'object') {
for (const key in obj) {
if (!checkDepth(obj[key], depth + 1)) {
return false;
}
}
}
return true;
}
if (!checkDepth(result, 0)) {
throw new Error(`JSON nesting too deep (max ${maxDepth} levels)`);
}
// Validate that the result is a safe object (not a function, etc.)
if (typeof result === 'function' || result instanceof Function) {
throw new Error('JSON parsing resulted in a function - potential code injection');
}
return result;
} catch (error) {
// Re-throw JSON parse errors with additional context
if (error instanceof Error && error.message.includes('JSON')) {
throw new Error(`Invalid JSON: ${error.message}`);
}
throw new Error(
`Failed to parse JSON: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export function validatePath(path: string): { valid: boolean; error?: string } {
const normalized = normalizePath(path);
+197
View File
@@ -0,0 +1,197 @@
/**
* Unit tests for utility functions, particularly validation functions
*/
import {
validateOllamaUrl,
validateModelName,
validatePluginSettings,
safeParseJson,
isValidHttpUrl,
} from '../src/utils';
describe('Validation Functions', () => {
describe('validateOllamaUrl', () => {
it('should accept valid HTTP URLs', () => {
expect(validateOllamaUrl('http://localhost:11434').valid).toBe(true);
expect(validateOllamaUrl('https://ollama.example.com').valid).toBe(true);
expect(validateOllamaUrl('http://192.168.1.100:8080').valid).toBe(true);
});
it('should reject invalid URLs', () => {
const invalidUrl = validateOllamaUrl('not-a-url');
expect(invalidUrl.valid).toBe(false);
expect(invalidUrl.error).toContain('valid HTTP or HTTPS URL');
});
it('should reject URLs ending with slash', () => {
const urlWithSlash = validateOllamaUrl('http://localhost:11434/');
expect(urlWithSlash.valid).toBe(false);
expect(urlWithSlash.error).toContain('should not end with a slash');
});
it('should reject empty URLs', () => {
const emptyUrl = validateOllamaUrl('');
expect(emptyUrl.valid).toBe(false);
expect(emptyUrl.error).toContain('cannot be empty');
});
it('should reject non-string inputs', () => {
const nonString = validateOllamaUrl(123 as any);
expect(nonString.valid).toBe(false);
});
it('should trim whitespace from URLs', () => {
expect(validateOllamaUrl(' http://localhost:11434 ').valid).toBe(true);
});
});
describe('validateModelName', () => {
it('should accept valid model names', () => {
expect(validateModelName('llama3').valid).toBe(true);
expect(validateModelName('llama-2').valid).toBe(true);
expect(validateModelName('llama.2').valid).toBe(true);
expect(validateModelName('llama_2').valid).toBe(true);
expect(validateModelName('llama-2-7b-instruct').valid).toBe(true);
});
it('should reject invalid model names', () => {
const invalidModel = validateModelName('llama@2');
expect(invalidModel.valid).toBe(false);
expect(invalidModel.error).toContain(
'only contain letters, numbers, dots, dashes, and underscores'
);
});
it('should reject empty model names', () => {
const emptyModel = validateModelName('');
expect(emptyModel.valid).toBe(false);
expect(emptyModel.error).toContain('cannot be empty');
});
it('should reject model names that are too short', () => {
const shortModel = validateModelName('a');
expect(shortModel.valid).toBe(false);
expect(shortModel.error).toContain('at least 2 characters long');
});
it('should reject model names that are too long', () => {
const longModel = validateModelName('a'.repeat(101));
expect(longModel.valid).toBe(false);
expect(longModel.error).toContain('less than 100 characters long');
});
it('should reject non-string inputs', () => {
const nonString = validateModelName(123 as any);
expect(nonString.valid).toBe(false);
});
it('should trim whitespace from model names', () => {
expect(validateModelName(' llama3 ').valid).toBe(true);
});
});
describe('validatePluginSettings', () => {
it('should return empty array for valid settings', () => {
const errors = validatePluginSettings({
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
lastIndexTime: 0,
});
expect(errors).toEqual([]);
});
it('should report URL validation errors', () => {
const errors = validatePluginSettings({
ollamaUrl: 'invalid-url',
model: 'llama3',
lastIndexTime: 0,
});
expect(errors.length).toBe(1);
expect(errors[0]).toContain('Ollama URL');
});
it('should report model validation errors', () => {
const errors = validatePluginSettings({
ollamaUrl: 'http://localhost:11434',
model: 'invalid@model',
lastIndexTime: 0,
});
expect(errors.length).toBe(1);
expect(errors[0]).toContain('Model');
});
it('should report multiple validation errors', () => {
const errors = validatePluginSettings({
ollamaUrl: 'invalid-url',
model: 'invalid@model',
lastIndexTime: 0,
});
expect(errors.length).toBe(2);
expect(errors[0]).toContain('Ollama URL');
expect(errors[1]).toContain('Model');
});
});
describe('safeParseJson', () => {
it('should parse valid JSON', () => {
const result = safeParseJson('{"key": "value"}');
expect(result).toEqual({ key: 'value' });
});
it('should throw on invalid JSON', () => {
expect(() => safeParseJson('{invalid json}')).toThrow('Invalid JSON');
});
it('should throw on code injection patterns', () => {
expect(() => safeParseJson('{"constructor": "function(){}"}')).toThrow('dangerous');
expect(() => safeParseJson('{"prototype": "something"}')).toThrow('dangerous');
expect(() => safeParseJson('{"__proto__": "something"}')).toThrow('dangerous');
expect(() => safeParseJson('{"function": "alert"}')).toThrow('dangerous');
});
it('should throw on deeply nested JSON', () => {
const deeplyNested = JSON.stringify({ value: 1 });
let current = deeplyNested;
for (let i = 0; i < 25; i++) {
current = `{ "nested": ${current} }`;
}
expect(() => safeParseJson(current)).toThrow('nesting too deep');
});
it('should throw on very large JSON', () => {
const largeJson = '"' + 'x'.repeat(1000001) + '"'; // 1,000,001 characters
expect(() => safeParseJson(largeJson)).toThrow('input too large');
});
it('should handle string vs object arguments correctly', () => {
const stringArgs = '{"path": "test.md", "content": "test"}';
const objectArgs = { path: 'test.md', content: 'test' };
const stringResult = safeParseJson(stringArgs);
const objectResult = safeParseJson(JSON.stringify(objectArgs));
expect(stringResult).toEqual(objectResult);
});
it('should throw on non-string input', () => {
expect(() => safeParseJson(123 as any)).toThrow('Input must be a string');
});
});
describe('isValidHttpUrl', () => {
it('should accept valid HTTP URLs', () => {
expect(isValidHttpUrl('http://localhost:11434')).toBe(true);
expect(isValidHttpUrl('https://example.com')).toBe(true);
});
it('should reject non-HTTP URLs', () => {
expect(isValidHttpUrl('ftp://example.com')).toBe(false);
expect(isValidHttpUrl('file:///path')).toBe(false);
});
it('should reject invalid URLs', () => {
expect(isValidHttpUrl('not-a-url')).toBe(false);
expect(isValidHttpUrl('')).toBe(false);
});
});
});
+1 -3
View File
@@ -10,11 +10,9 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext"],
"moduleResolution": "node",
"types": ["node", "jest"]
},
"include": ["src/**/*", "main.ts"],
"typeRoots": ["node_modules/@types", "./src"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}