Update safeParseJson to check for dangerous object keys recursively

Improve detection of prototype pollution patterns in JSON parsing
This commit is contained in:
2026-05-06 22:38:31 +02:00
parent 579e60f5d5
commit ea792b0034
11 changed files with 1105 additions and 1129 deletions
+64 -103
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">91.2% </span>
<span class="strong">90.36% </span>
<span class="quiet">Statements</span>
<span class='fraction'>197/216</span>
<span class='fraction'>197/218</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80.28% </span>
<span class="quiet">Branches</span>
<span class='fraction'>57/71</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.85% </span>
<span class="quiet">Branches</span>
<span class='fraction'>58/70</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.35% </span>
<span class="quiet">Functions</span>
<span class='fraction'>28/34</span>
<span class='fraction'>29/35</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">93.1% </span>
<span class="strong">92.15% </span>
<span class="quiet">Lines</span>
<span class='fraction'>189/203</span>
<span class='fraction'>188/204</span>
</div>
@@ -540,21 +540,7 @@
<a name='L475'></a><a href='#L475'>475</a>
<a name='L476'></a><a href='#L476'>476</a>
<a name='L477'></a><a href='#L477'>477</a>
<a name='L478'></a><a href='#L478'>478</a>
<a name='L479'></a><a href='#L479'>479</a>
<a name='L480'></a><a href='#L480'>480</a>
<a name='L481'></a><a href='#L481'>481</a>
<a name='L482'></a><a href='#L482'>482</a>
<a name='L483'></a><a href='#L483'>483</a>
<a name='L484'></a><a href='#L484'>484</a>
<a name='L485'></a><a href='#L485'>485</a>
<a name='L486'></a><a href='#L486'>486</a>
<a name='L487'></a><a href='#L487'>487</a>
<a name='L488'></a><a href='#L488'>488</a>
<a name='L489'></a><a href='#L489'>489</a>
<a name='L490'></a><a href='#L490'>490</a>
<a name='L491'></a><a href='#L491'>491</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<a name='L478'></a><a href='#L478'>478</a></td><td class="line-coverage quiet"><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>
@@ -654,9 +640,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-yes">136x</span>
<span class="cline-any cline-yes">23x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</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>
@@ -760,21 +747,17 @@
<span class="cline-any cline-no">&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-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">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</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">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</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">7x</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>
@@ -783,21 +766,12 @@
<span class="cline-any cline-yes">8x</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">8x</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">8x</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">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
@@ -1049,7 +1023,6 @@
type KeyboardEvent = globalThis.KeyboardEvent;
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
type HTMLButtonElement = globalThis.HTMLButtonElement;
type MouseEvent = globalThis.MouseEvent;
&nbsp;
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
const MAX_MESSAGE_HISTORY = 50;
@@ -1074,16 +1047,16 @@ export class ChatView extends ItemView {
private vaultIndexer: VaultIndexer;
private toolExecutor: ToolExecutor;
private lastMessageEl: HTMLElement | null = null;
private newChatButton: HTMLElement | null = null;
private sendButton: HTMLElement | null = null;
private inputEl: HTMLElement | null = null;
private newChatButton: HTMLButtonElement | null = null;
private sendButton: HTMLButtonElement | null = null;
private inputEl: HTMLTextAreaElement | null = null;
private chatContainer: HTMLElement | null = null;
private sendButtonClickHandler: (() =&gt; Promise&lt;void&gt;) | null = null;
private inputKeyDownHandler: ((e: KeyboardEvent) =&gt; Promise&lt;void&gt;) | null = null;
private newChatButtonClickHandler: (() =&gt; void) | null = null;
private sendButtonEventHandler: ((e: MouseEvent) =&gt; void) | null = null;
private inputKeyDownEventHandler: ((e: KeyboardEvent) =&gt; void) | null = null;
private newChatButtonEventHandler: ((e: MouseEvent) =&gt; void) | null = null;
private sendButtonClickWrapper: (() =&gt; void) | null = null;
private inputKeyDownWrapper: ((e: KeyboardEvent) =&gt; void) | null = null;
private newChatButtonClickWrapper: (() =&gt; void) | null = null;
private listenersAttached = false;
&nbsp;
// Getters for testing
@@ -1143,10 +1116,11 @@ export class ChatView extends ItemView {
}
&nbsp;
private cleanupStreamingResources(): void {
// Ensure any ongoing streaming is properly cleaned up
if (this.lastMessageEl &amp;&amp; this.lastMessageEl.parentElement) {
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
this.lastMessageEl = null;
// Only cleanup if there's still an active streaming message
const streamingMessage = this.messages.find((msg) =&gt; msg.isStreaming);
<span class="missing-if-branch" title="if path not taken" >I</span>if (streamingMessage &amp;&amp; this.lastMessageEl &amp;&amp; <span class="branch-2 cbranch-no" title="branch not covered" >this.lastMessageEl.parentElement)</span> {
<span class="cstat-no" title="statement not covered" > this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);</span>
<span class="cstat-no" title="statement not covered" > this.lastMessageEl = null;</span>
}
}
&nbsp;
@@ -1165,7 +1139,7 @@ export class ChatView extends ItemView {
this.sendButton = inputContainer.createEl('button', {
cls: 'ollama-send-button',
});
(this.sendButton as HTMLButtonElement).textContent = 'Send';
this.sendButton.textContent = 'Send';
}
&nbsp;
if (!this.newChatButton) {
@@ -1175,8 +1149,8 @@ export class ChatView extends ItemView {
this.newChatButton = newChatContainer.createEl('button', {
cls: 'ollama-new-chat-button',
});
(this.newChatButton as HTMLButtonElement).textContent = '🔄 New Chat';
(this.newChatButton as HTMLButtonElement).title = 'Start a new conversation';
this.newChatButton.textContent = '🔄 New Chat';
this.newChatButton.title = 'Start a new conversation';
}
&nbsp;
// Create immutable snapshot for rendering
@@ -1228,8 +1202,8 @@ export class ChatView extends ItemView {
if (!this.sendButtonClickHandler) {
this.sendButtonClickHandler = async () =&gt; {
<span class="missing-if-branch" title="if path not taken" >I</span>if (!this.inputEl) <span class="cstat-no" title="statement not covered" >return;</span>
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
(this.inputEl as HTMLTextAreaElement).value = '';
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
&nbsp;
@@ -1237,60 +1211,47 @@ export class ChatView extends ItemView {
this.inputKeyDownHandler = async (e: KeyboardEvent) =&gt; {
if (!this.inputEl || e.key !== 'Enter' || e.shiftKey) return;
e.preventDefault();
await this.handleUserInput((this.inputEl as HTMLTextAreaElement).value);
(this.inputEl as HTMLTextAreaElement).value = '';
await this.handleUserInput(this.inputEl.value);
this.inputEl.value = '';
};
}
&nbsp;
// Add event listeners
this.sendButtonEventHandler = <span class="fstat-no" title="function not covered" >() =</span>&gt; {
// Create wrapper functions for event listeners
this.sendButtonClickWrapper = <span class="fstat-no" title="function not covered" >() =</span>&gt; {
<span class="cstat-no" title="statement not covered" > void this.sendButtonClickHandler?.();</span>
};
this.inputKeyDownEventHandler = <span class="fstat-no" title="function not covered" >(e</span>: KeyboardEvent) =&gt; {
this.inputKeyDownWrapper = <span class="fstat-no" title="function not covered" >(e</span>: KeyboardEvent) =&gt; {
<span class="cstat-no" title="statement not covered" > void this.inputKeyDownHandler?.(e);</span>
};
(this.sendButton as HTMLButtonElement).addEventListener('click', this.sendButtonEventHandler);
(this.inputEl as HTMLTextAreaElement).addEventListener(
'keydown',
this.inputKeyDownEventHandler
);
this.newChatButtonClickWrapper = <span class="fstat-no" title="function not covered" >() =</span>&gt; {
<span class="cstat-no" title="statement not covered" > void this.newChatButtonClickHandler?.();</span>
};
&nbsp;
// Add event listeners using wrappers
this.sendButton.addEventListener('click', this.sendButtonClickWrapper);
this.inputEl.addEventListener('keydown', this.inputKeyDownWrapper);
if (this.newChatButton) {
if (!this.newChatButtonClickHandler) {
this.newChatButtonClickHandler = () =&gt; this.clearConversation();
}
this.newChatButtonEventHandler = <span class="fstat-no" title="function not covered" >() =</span>&gt; {
<span class="cstat-no" title="statement not covered" > this.newChatButtonClickHandler?.();</span>
};
(this.newChatButton as HTMLButtonElement).addEventListener(
'click',
this.newChatButtonEventHandler
);
this.newChatButton.addEventListener('click', this.newChatButtonClickWrapper);
}
this.listenersAttached = true;
}
&nbsp;
private removeEventListeners(): void {
if (this.sendButton &amp;&amp; this.sendButtonEventHandler) {
(this.sendButton as HTMLButtonElement).removeEventListener(
'click',
this.sendButtonEventHandler
);
if (this.sendButton &amp;&amp; this.sendButtonClickWrapper) {
this.sendButton.removeEventListener('click', this.sendButtonClickWrapper);
}
if (this.inputEl &amp;&amp; this.inputKeyDownEventHandler) {
(this.inputEl as HTMLTextAreaElement).removeEventListener(
'keydown',
this.inputKeyDownEventHandler
);
if (this.inputEl &amp;&amp; this.inputKeyDownWrapper) {
this.inputEl.removeEventListener('keydown', this.inputKeyDownWrapper);
}
if (this.newChatButton &amp;&amp; this.newChatButtonEventHandler) {
(this.newChatButton as HTMLButtonElement).removeEventListener(
'click',
this.newChatButtonEventHandler
);
if (this.newChatButton &amp;&amp; this.newChatButtonClickWrapper) {
this.newChatButton.removeEventListener('click', this.newChatButtonClickWrapper);
}
this.sendButtonEventHandler = null;
this.inputKeyDownEventHandler = null;
this.newChatButtonEventHandler = null;
this.sendButtonClickWrapper = null;
this.inputKeyDownWrapper = null;
this.newChatButtonClickWrapper = null;
this.listenersAttached = false;
}
&nbsp;
@@ -1328,7 +1289,7 @@ export class ChatView extends ItemView {
&nbsp;
private async handleUserInput(content: string) {
<span class="missing-if-branch" title="if path not taken" >I</span>if (!this.sendButton || !this.inputEl) <span class="cstat-no" title="statement not covered" >return;</span>
(this.sendButton as HTMLButtonElement).disabled = true;
this.sendButton.disabled = true;
&nbsp;
try {
// Guard against empty messages
@@ -1528,7 +1489,7 @@ export class ChatView extends ItemView {
this.cleanupStreamingResources();
} finally {
if (this.sendButton) {
(this.sendButton as HTMLButtonElement).disabled = false;
this.sendButton.disabled = false;
}
}
}
@@ -1540,7 +1501,7 @@ export class ChatView extends ItemView {
<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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -475,7 +475,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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+43 -43
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">88.83% </span>
<span class="strong">74.47% </span>
<span class="quiet">Statements</span>
<span class='fraction'>589/663</span>
<span class='fraction'>499/670</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">77.02% </span>
<span class="strong">59.06% </span>
<span class="quiet">Branches</span>
<span class='fraction'>228/296</span>
<span class='fraction'>176/298</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">86% </span>
<span class="strong">78.21% </span>
<span class="quiet">Functions</span>
<span class='fraction'>86/100</span>
<span class='fraction'>79/101</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.07% </span>
<span class="strong">75.31% </span>
<span class="quiet">Lines</span>
<span class='fraction'>572/635</span>
<span class='fraction'>482/640</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
@@ -80,17 +80,17 @@
</thead>
<tbody><tr>
<td class="file high" data-value="chat-view.ts"><a href="chat-view.ts.html">chat-view.ts</a></td>
<td data-value="91.2" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
<td data-value="90.36" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 90%"></div><div class="cover-empty" style="width: 10%"></div></div>
</td>
<td data-value="91.2" class="pct high">91.2%</td>
<td data-value="216" class="abs high">197/216</td>
<td data-value="90.36" class="pct high">90.36%</td>
<td data-value="218" class="abs high">197/218</td>
<td data-value="80.28" class="pct high">80.28%</td>
<td data-value="71" class="abs high">57/71</td>
<td data-value="82.85" class="pct high">82.85%</td>
<td data-value="70" class="abs high">58/70</td>
<td data-value="82.35" class="pct high">82.35%</td>
<td data-value="34" class="abs high">28/34</td>
<td data-value="93.1" class="pct high">93.1%</td>
<td data-value="203" class="abs high">189/203</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="204" class="abs high">188/204</td>
</tr>
<tr>
@@ -109,18 +109,18 @@
</tr>
<tr>
<td class="file medium" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td>
<td data-value="77.65" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 77%"></div><div class="cover-empty" style="width: 23%"></div></div>
<td class="file low" data-value="ollama-client.ts"><a href="ollama-client.ts.html">ollama-client.ts</a></td>
<td data-value="17.7" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div>
</td>
<td data-value="77.65" class="pct medium">77.65%</td>
<td data-value="94" class="abs medium">73/94</td>
<td data-value="61.22" class="pct medium">61.22%</td>
<td data-value="49" class="abs medium">30/49</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="11" class="abs high">10/11</td>
<td data-value="78.88" class="pct medium">78.88%</td>
<td data-value="90" class="abs medium">71/90</td>
<td data-value="17.7" class="pct low">17.7%</td>
<td data-value="96" class="abs low">17/96</td>
<td data-value="4" class="pct low">4%</td>
<td data-value="50" class="abs low">2/50</td>
<td data-value="36.36" class="pct low">36.36%</td>
<td data-value="11" class="abs low">4/11</td>
<td data-value="18.47" class="pct low">18.47%</td>
<td data-value="92" class="abs low">17/92</td>
</tr>
<tr>
@@ -144,28 +144,28 @@
<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="36" class="abs high">36/36</td>
<td data-value="35" class="abs high">35/35</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="36" class="abs high">36/36</td>
<td data-value="35" class="abs high">35/35</td>
</tr>
<tr>
<td class="file high" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="81.72" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 81%"></div><div class="cover-empty" style="width: 19%"></div></div>
<td class="file low" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td>
<td data-value="44.32" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 44%"></div><div class="cover-empty" style="width: 56%"></div></div>
</td>
<td data-value="81.72" class="pct high">81.72%</td>
<td data-value="93" class="abs high">76/93</td>
<td data-value="70.83" class="pct medium">70.83%</td>
<td data-value="48" class="abs medium">34/48</td>
<td data-value="56.25" class="pct medium">56.25%</td>
<td data-value="16" class="abs medium">9/16</td>
<td data-value="82.22" class="pct high">82.22%</td>
<td data-value="90" class="abs high">74/90</td>
<td data-value="44.32" class="pct low">44.32%</td>
<td data-value="97" class="abs low">43/97</td>
<td data-value="22.91" class="pct low">22.91%</td>
<td data-value="48" class="abs low">11/48</td>
<td data-value="43.75" class="pct low">43.75%</td>
<td data-value="16" class="abs low">7/16</td>
<td data-value="43.01" class="pct low">43.01%</td>
<td data-value="93" class="abs low">40/93</td>
</tr>
<tr>
@@ -191,7 +191,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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+255 -252
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">77.65% </span>
<span class="strong">17.7% </span>
<span class="quiet">Statements</span>
<span class='fraction'>73/94</span>
<span class='fraction'>17/96</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.22% </span>
<span class="strong">4% </span>
<span class="quiet">Branches</span>
<span class='fraction'>30/49</span>
<span class='fraction'>2/50</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.9% </span>
<span class="strong">36.36% </span>
<span class="quiet">Functions</span>
<span class='fraction'>10/11</span>
<span class='fraction'>4/11</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">78.88% </span>
<span class="strong">18.47% </span>
<span class="quiet">Lines</span>
<span class='fraction'>71/90</span>
<span class='fraction'>17/92</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -292,7 +292,8 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<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>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
@@ -305,197 +306,50 @@
<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">48x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-yes">48x</span>
<span class="cline-any cline-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-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">14x</span>
<span class="cline-any cline-yes">31x</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-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-yes">3x</span>
<span class="cline-any cline-yes">3x</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">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-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">1x</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-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">9x</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">8x</span>
<span class="cline-any cline-yes">8x</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">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-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</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">7x</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">7x</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-yes">61x</span>
<span class="cline-any cline-yes">9x</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-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">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</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-yes">52x</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">51x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-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-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-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">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-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-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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-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">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">3x</span>
<span class="cline-any cline-yes">3x</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-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-yes">4x</span>
@@ -503,18 +357,166 @@
<span class="cline-any cline-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-neutral">&nbsp;</span>
<span class="cline-any cline-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">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-neutral">&nbsp;</span>
<span class="cline-any cline-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-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-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>
@@ -558,12 +560,12 @@ export class OllamaClient {
yield* this.streamChatWithRetry(messages, tools, 0);
}
&nbsp;
async streamChatAsPromise(
<span class="fstat-no" title="function not covered" > async </span>streamChatAsPromise(
messages: OllamaMessage[],
tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[]</span>
): Promise&lt;OllamaMessage[]&gt; {
const chunks: OllamaMessage[] = [];
for await (const chunk of this.streamChat(messages, tools)) {
const chunks: OllamaMessage[] = <span class="cstat-no" title="statement not covered" >[];</span>
<span class="cstat-no" title="statement not covered" > for await (const chunk of this.streamChat(messages, tools)) {</span>
<span class="cstat-no" title="statement not covered" > chunks.push(chunk);</span>
}
<span class="cstat-no" title="statement not covered" > return chunks;</span>
@@ -574,7 +576,11 @@ export class OllamaClient {
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>
): AsyncGenerator&lt;OllamaMessage, void, unknown&gt; {
const prevController = this.abortController;
this.abortController = new AbortController();
<span class="missing-if-branch" title="if path not taken" >I</span>if (prevController) {
<span class="cstat-no" title="statement not covered" > prevController.abort();</span>
}
&nbsp;
try {
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
@@ -591,8 +597,8 @@ export class OllamaClient {
signal: this.abortController.signal,
});
&nbsp;
if (!response.ok) {
<span class="missing-if-branch" title="if path not taken" >I</span>if (response.status &gt;= 500 &amp;&amp; <span class="branch-1 cbranch-no" title="branch not covered" >attempt &lt; this.maxRetries)</span> {
<span class="missing-if-branch" title="if path not taken" >I</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>
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span>
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
@@ -602,59 +608,59 @@ export class OllamaClient {
<span class="cstat-no" title="statement not covered" > yield* this.streamChatWithRetry(messages, tools, attempt + 1);</span>
<span class="cstat-no" title="statement not covered" > return;</span>
}
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;
if (!response.body) {
throw new Error('No response body');
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.body) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error('No response body');</span>
}
&nbsp;
const contentType = response.headers.get('content-type');
if (!contentType || (!contentType.includes('ndjson') &amp;&amp; !contentType.includes('json'))) {
throw new Error('Invalid response format');
const contentType = <span class="cstat-no" title="statement not covered" >response.headers.get('content-type');</span>
<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>
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid response format');</span>
}
&nbsp;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let malformedCount = 0;
const maxMalformed = 50;
const reader = <span class="cstat-no" title="statement not covered" >response.body.getReader();</span>
const decoder = <span class="cstat-no" title="statement not covered" >new TextDecoder();</span>
let buffer = <span class="cstat-no" title="statement not covered" >'';</span>
let malformedCount = <span class="cstat-no" title="statement not covered" >0;</span>
const maxMalformed = <span class="cstat-no" title="statement not covered" >50;</span>
&nbsp;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
<span class="cstat-no" title="statement not covered" > try {</span>
<span class="cstat-no" title="statement not covered" > while (true) {</span>
const { done, value } = <span class="cstat-no" title="statement not covered" >await reader.read();</span>
<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>
&nbsp;
buffer += decoder.decode(value, { stream: true });
<span class="cstat-no" title="statement not covered" > buffer += decoder.decode(value, { stream: true });</span>
&nbsp;
const lines = buffer.split('\n');
buffer = lines.pop() ?? <span class="branch-1 cbranch-no" title="branch not covered" >'';</span>
const lines = <span class="cstat-no" title="statement not covered" >buffer.split('\n');</span>
<span class="cstat-no" title="statement not covered" > buffer = lines.pop() ?? '';</span>
&nbsp;
for (const line of lines) {
<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 class="cstat-no" title="statement not covered" > for (const line of lines) {</span>
<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>
&nbsp;
try {
const parsed = JSON.parse(line) as Record&lt;string, unknown&gt;;
this.throwIfOllamaError(parsed);
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(line) as Record&lt;string, unknown&gt;;</span>
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span>
&nbsp;
const message = this.toOllamaMessage(parsed.message);
<span class="missing-if-branch" title="if path not taken" >I</span>if (!message) {
const message = <span class="cstat-no" title="statement not covered" >this.toOllamaMessage(parsed.message);</span>
<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="cstat-no" title="statement not covered" > continue;</span>
}
&nbsp;
malformedCount = 0;
yield message;
<span class="cstat-no" title="statement not covered" > malformedCount = 0;</span>
<span class="cstat-no" title="statement not covered" > yield message;</span>
} catch (error) {
if (error instanceof Error &amp;&amp; error.message.startsWith('Ollama error:')) {
throw 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" > throw error;</span>
}
&nbsp;
malformedCount++;
if (malformedCount &gt; maxMalformed) {
throw new Error('Too many malformed chunks in stream');
<span class="cstat-no" title="statement not covered" > malformedCount++;</span>
<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>
<span class="cstat-no" title="statement not covered" > throw new Error('Too many malformed chunks in stream');</span>
}
&nbsp;
Logger.warn(
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Skipped malformed chunk: ${line.substring(0, 80)}... - ${(error as Error).message}`,
'ollama-client'
);
@@ -662,7 +668,7 @@ export class OllamaClient {
}
}
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (buffer.trim()) {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (buffer.trim()) {</span>
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >JSON.parse(buffer) as Record&lt;string, unknown&gt;;</span>
<span class="cstat-no" title="statement not covered" > this.throwIfOllamaError(parsed);</span>
@@ -682,25 +688,23 @@ export class OllamaClient {
}
}
} finally {
reader.releaseLock();
<span class="cstat-no" title="statement not covered" > reader.releaseLock();</span>
}
} finally {
this.abortController = null;
}
}
&nbsp;
async chat(messages: OllamaMessage[], tools: OllamaTool[] = <span class="branch-0 cbranch-no" title="branch not covered" >[])</span>: Promise&lt;OllamaMessage&gt; {
return this.chatWithRetry(messages, tools, 0);
<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; {
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, 0);</span>
}
&nbsp;
private async chatWithRetry(
private <span class="fstat-no" title="function not covered" >async </span>chatWithRetry(
messages: OllamaMessage[],
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>
): Promise&lt;OllamaMessage&gt; {
const controller = new AbortController();
&nbsp;
const response = await this.fetchFn(`${this.baseURL}/api/chat`, {
const response = <span class="cstat-no" title="statement not covered" >await this.fetchFn(`${this.baseURL}/api/chat`, {</span>
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -711,41 +715,40 @@ export class OllamaClient {
tools,
stream: false,
}),
signal: controller.signal,
});
&nbsp;
if (!response.ok) {
if (response.status &gt;= 500 &amp;&amp; attempt &lt; this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100;
Logger.warn(
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!response.ok) {</span>
<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>
const retryDelay = <span class="cstat-no" title="statement not covered" >Math.pow(2, attempt) * 100;</span>
<span class="cstat-no" title="statement not covered" > Logger.warn(</span>
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
'ollama-client'
);
await new Promise((resolve) =&gt; setTimeout(resolve, retryDelay));
return this.chatWithRetry(messages, tools, attempt + 1);
<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>
<span class="cstat-no" title="statement not covered" > return this.chatWithRetry(messages, tools, attempt + 1);</span>
}
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;
const data = (await response.json()) as OllamaChatResponse;
return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };
const data = (<span class="cstat-no" title="statement not covered" >await response.json())</span> as OllamaChatResponse;
<span class="cstat-no" title="statement not covered" > return this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] };</span>
}
&nbsp;
private throwIfOllamaError(parsed: Record&lt;string, unknown&gt;): void {
if (parsed.error) {
throw new Error(`Ollama error: ${String(parsed.error)}`);
private <span class="fstat-no" title="function not covered" >throwIfOllamaError(</span>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 (parsed.error) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error(`Ollama error: ${String(parsed.error)}`);</span>
}
}
&nbsp;
private toOllamaMessage(value: unknown): OllamaMessage | null {
if (!value || typeof value !== 'object') {
return null;
private <span class="fstat-no" title="function not covered" >toOllamaMessage(</span>value: unknown): OllamaMessage | null {
<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>
<span class="cstat-no" title="statement not covered" > return null;</span>
}
&nbsp;
const record = value as Partial&lt;OllamaMessage&gt;;
return {
const record = <span class="cstat-no" title="statement not covered" >value as Partial&lt;OllamaMessage&gt;;</span>
<span class="cstat-no" title="statement not covered" > return {</span>
role: record.role ?? 'assistant',
content: typeof record.content === 'string' ? record.content : <span class="branch-1 cbranch-no" title="branch not covered" >'',</span>
content: typeof record.content === 'string' ? record.content : '',
tool_calls: record.tool_calls ?? [],
};
}
@@ -757,7 +760,7 @@ export class OllamaClient {
<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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -463,7 +463,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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+10 -34
View File
@@ -25,7 +25,7 @@
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>36/36</span>
<span class='fraction'>35/35</span>
</div>
@@ -46,7 +46,7 @@
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>36/36</span>
<span class='fraction'>35/35</span>
</div>
@@ -241,15 +241,7 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<a name='L179'></a><a href='#L179'>179</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>
@@ -269,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-yes">17x</span>
<span class="cline-any cline-yes">17x</span>
<span class="cline-any cline-yes">17x</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>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -289,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-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">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>
@@ -349,14 +341,6 @@
<span class="cline-any cline-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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -535,14 +519,6 @@ export interface PluginSettings {
lastIndexTime: number;
}
&nbsp;
export const DEFAULT_SETTINGS: PluginSettings = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,
lastIndexTime: 0,
};
&nbsp;
// ============================================================
// Ollama Protocol Types
// ============================================================
@@ -628,7 +604,7 @@ export interface VaultIndexEntry {
<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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+218 -209
View File
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">81.72% </span>
<span class="strong">44.32% </span>
<span class="quiet">Statements</span>
<span class='fraction'>76/93</span>
<span class='fraction'>43/97</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">70.83% </span>
<span class="strong">22.91% </span>
<span class="quiet">Branches</span>
<span class='fraction'>34/48</span>
<span class='fraction'>11/48</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">56.25% </span>
<span class="strong">43.75% </span>
<span class="quiet">Functions</span>
<span class='fraction'>9/16</span>
<span class='fraction'>7/16</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.22% </span>
<span class="strong">43.01% </span>
<span class="quiet">Lines</span>
<span class='fraction'>74/90</span>
<span class='fraction'>40/93</span>
</div>
@@ -61,7 +61,7 @@
</div>
</template>
</div>
<div class='status-line high'></div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
@@ -268,7 +268,10 @@
<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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</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>
@@ -311,141 +314,157 @@
<span class="cline-any cline-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">55x</span>
<span class="cline-any cline-yes">55x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-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-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">12x</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-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</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">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</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-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</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-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">14x</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">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">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">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-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">5x</span>
<span class="cline-any cline-yes">4x</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">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>
<span class="cline-any cline-yes">4x</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>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">109x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">108x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">108x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">54x</span>
<span class="cline-any cline-yes">79x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">54x</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">38x</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">37x</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-yes">36x</span>
<span class="cline-any cline-yes">36x</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-yes">34x</span>
<span class="cline-any cline-yes">34x</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-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</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-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-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-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">30x</span>
<span class="cline-any cline-yes">5x</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-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-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-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">5x</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-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-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-yes">75x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">75x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">75x</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-yes">5x</span>
<span class="cline-any cline-yes">27x</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">27x</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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">29x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">26x</span>
<span class="cline-any cline-yes">75x</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-yes">26x</span>
<span class="cline-any cline-yes">78x</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-yes">49x</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-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-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>
@@ -453,26 +472,13 @@
<span class="cline-any cline-yes">5x</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-yes">5x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</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-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-no">&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
&nbsp;
// ==================== Logger ====================
@@ -530,72 +536,72 @@ export class Logger {
&nbsp;
// ==================== URL &amp; Model Validation ====================
&nbsp;
export function validateOllamaUrl(url: string): { valid: boolean; error?: string } {
if (typeof url !== 'string' || !url.trim()) {
return { valid: false, error: 'URL cannot be empty' };
export function <span class="fstat-no" title="function not covered" >validateOllamaUrl(</span>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>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL cannot be empty' };</span>
}
&nbsp;
const trimmedUrl = url.trim();
const trimmedUrl = <span class="cstat-no" title="statement not covered" >url.trim();</span>
&nbsp;
if (trimmedUrl.endsWith('/')) {
return { valid: false, error: 'URL should not end with a slash' };
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (trimmedUrl.endsWith('/')) {</span>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'URL should not end with a slash' };</span>
}
&nbsp;
try {
const parsed = new URL(trimmedUrl);
<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" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >new URL(trimmedUrl);</span>
<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="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span>
}
return { valid: true };
<span class="cstat-no" title="statement not covered" > return { valid: true };</span>
} catch {
return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Must be a valid HTTP or HTTPS URL' };</span>
}
}
&nbsp;
export function validateModelName(model: string): { valid: boolean; error?: string } {
if (typeof model !== 'string') {
return { valid: false, error: 'Model name must be a string' };
export function <span class="fstat-no" title="function not covered" >validateModelName(</span>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>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be a string' };</span>
}
&nbsp;
const trimmedModel = model.trim();
const trimmedModel = <span class="cstat-no" title="statement not covered" >model.trim();</span>
&nbsp;
// Explicit check for empty string after trimming
if (!trimmedModel || trimmedModel.length === 0) {
return { valid: false, error: 'Model name cannot be empty' };
<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>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name cannot be empty' };</span>
}
&nbsp;
if (trimmedModel.length &lt; 2) {
return { valid: false, error: 'Model name must be at least 2 characters long' };
<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>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be at least 2 characters long' };</span>
}
&nbsp;
if (trimmedModel.length &gt; 100) {
return { valid: false, error: 'Model name must be less than 100 characters long' };
<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>
<span class="cstat-no" title="statement not covered" > return { valid: false, error: 'Model name must be less than 100 characters long' };</span>
}
&nbsp;
if (!/^[a-zA-Z0-9._:-]+$/.test(trimmedModel)) {
return {
<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>
<span class="cstat-no" title="statement not covered" > return {</span>
valid: false,
error: 'Model name can only contain letters, numbers, dots, dashes, underscores, and colons',
};
}
&nbsp;
return { valid: true };
<span class="cstat-no" title="statement not covered" > return { valid: true };</span>
}
&nbsp;
export function validatePluginSettings(settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = [];
export function <span class="fstat-no" title="function not covered" >validatePluginSettings(</span>settings: { ollamaUrl: string; model: string }): string[] {
const errors: string[] = <span class="cstat-no" title="statement not covered" >[];</span>
&nbsp;
const urlValidation = validateOllamaUrl(settings.ollamaUrl);
if (!urlValidation.valid) {
errors.push(`Invalid Ollama URL: ${urlValidation.error}`);
const urlValidation = <span class="cstat-no" title="statement not covered" >validateOllamaUrl(settings.ollamaUrl);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!urlValidation.valid) {</span>
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Ollama URL: ${urlValidation.error}`);</span>
}
&nbsp;
const modelValidation = validateModelName(settings.model);
if (!modelValidation.valid) {
errors.push(`Invalid Model Name: ${modelValidation.error}`);
const modelValidation = <span class="cstat-no" title="statement not covered" >validateModelName(settings.model);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (!modelValidation.valid) {</span>
<span class="cstat-no" title="statement not covered" > errors.push(`Invalid Model Name: ${modelValidation.error}`);</span>
}
&nbsp;
return errors;
<span class="cstat-no" title="statement not covered" > return errors;</span>
}
&nbsp;
// ==================== Safe JSON Parsing ====================
@@ -604,8 +610,8 @@ const MAX_JSON_SIZE = 1_000_000;
const MAX_JSON_NESTING = 24;
&nbsp;
function countNestingDepth(value: unknown, depth: number = 0): number {
if (depth &gt; MAX_JSON_NESTING) {
return depth;
<span class="missing-if-branch" title="if path not taken" >I</span>if (depth &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > return depth;</span>
}
<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>
@@ -619,12 +625,12 @@ function countNestingDepth(value: unknown, depth: number = 0): number {
}
&nbsp;
export function safeParseJson(jsonString: string): unknown {
if (typeof jsonString !== 'string') {
throw new Error('Input must be a string');
<span class="missing-if-branch" title="if path not taken" >I</span>if (typeof jsonString !== 'string') {
<span class="cstat-no" title="statement not covered" > throw new Error('Input must be a string');</span>
}
&nbsp;
if (jsonString.length &gt; MAX_JSON_SIZE) {
throw new Error('JSON input too large');
<span class="missing-if-branch" title="if path not taken" >I</span>if (jsonString.length &gt; MAX_JSON_SIZE) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON input too large');</span>
}
&nbsp;
let parsed: unknown;
@@ -634,20 +640,34 @@ export function safeParseJson(jsonString: string): unknown {
throw new Error('Invalid JSON');
}
&nbsp;
// Check for dangerous prototype pollution patterns
const reStringified = JSON.stringify(parsed);
if (
reStringified.includes('constructor') ||
reStringified.includes('prototype') ||
reStringified.includes('__proto__') ||
reStringified.includes('function')
) {
throw new Error('dangerous code pattern detected');
// Check for dangerous prototype pollution patterns in object keys only
const checkDangerousPatterns = (obj: unknown): boolean =&gt; {
if (typeof obj !== 'object' || obj === null) {
return false;
}
&nbsp;
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))) {
<span class="cstat-no" title="statement not covered" > return true;</span>
}
&nbsp;
// Recursively check nested objects
for (const key in obj as Record&lt;string, unknown&gt;) {
<span class="missing-if-branch" title="if path not taken" >I</span>if (checkDangerousPatterns((obj as Record&lt;string, unknown&gt;)[key])) {
<span class="cstat-no" title="statement not covered" > return true;</span>
}
}
&nbsp;
return false;
};
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (checkDangerousPatterns(parsed)) {
<span class="cstat-no" title="statement not covered" > throw new Error('dangerous code pattern detected');</span>
}
&nbsp;
// Check nesting depth
if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) {
throw new Error('JSON nesting too deep');
<span class="missing-if-branch" title="if path not taken" >I</span>if (countNestingDepth(parsed) &gt; MAX_JSON_NESTING) {
<span class="cstat-no" title="statement not covered" > throw new Error('JSON nesting too deep');</span>
}
&nbsp;
return parsed;
@@ -655,29 +675,18 @@ export function safeParseJson(jsonString: string): unknown {
&nbsp;
// ==================== Path &amp; File Utilities ====================
&nbsp;
export function <span class="fstat-no" title="function not covered" >sanitizeFilePath(</span>path: string): string {
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (path.includes('..')) {</span>
<span class="cstat-no" title="statement not covered" > throw new Error('Invalid path - cannot contain .. segments');</span>
}
<span class="cstat-no" title="statement not covered" > return path;</span>
}
&nbsp;
// ==================== HTTP Helpers ====================
&nbsp;
export function isValidHttpUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
export function <span class="fstat-no" title="function not covered" >isValidHttpUrl(</span>url: string): boolean {
<span class="cstat-no" title="statement not covered" > try {</span>
const parsed = <span class="cstat-no" title="statement not covered" >new URL(url);</span>
<span class="cstat-no" title="statement not covered" > return parsed.protocol === 'http:' || parsed.protocol === 'https:';</span>
} catch {
return false;
<span class="cstat-no" title="statement not covered" > return false;</span>
}
}
&nbsp;
// ==================== Markdown Utilities ====================
&nbsp;
export function <span class="fstat-no" title="function not covered" >convertMarkdownToHtml(</span>markdown: string): string {
<span class="cstat-no" title="statement not covered" > return markdown.replace(/\n/g, '&lt;br&gt;').replace(/# (.+)/g, '&lt;h1&gt;$1&lt;/h1&gt;');</span>
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
@@ -685,7 +694,7 @@ export function <span class="fstat-no" title="function not covered" >convertMark
<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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -892,7 +892,7 @@ export { 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-06T20:04:00.154Z
at 2026-05-06T20:36:57.831Z
</div>
<script src="prettify.js"></script>
<script>
+477 -468
View File
File diff suppressed because it is too large Load Diff
+22 -15
View File
@@ -159,14 +159,28 @@ export function safeParseJson(jsonString: string): unknown {
throw new Error('Invalid JSON');
}
// Check for dangerous prototype pollution patterns
const reStringified = JSON.stringify(parsed);
if (
reStringified.includes('constructor') ||
reStringified.includes('prototype') ||
reStringified.includes('__proto__') ||
reStringified.includes('function')
) {
// Check for dangerous prototype pollution patterns in object keys only
const checkDangerousPatterns = (obj: unknown): boolean => {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const dangerousKeys = ['constructor', 'prototype', '__proto__'];
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
return true;
}
// Recursively check nested objects
for (const key in obj as Record<string, unknown>) {
if (checkDangerousPatterns((obj as Record<string, unknown>)[key])) {
return true;
}
}
return false;
};
if (checkDangerousPatterns(parsed)) {
throw new Error('dangerous code pattern detected');
}
@@ -180,13 +194,6 @@ export function safeParseJson(jsonString: string): unknown {
// ==================== Path & File Utilities ====================
export function sanitizeFilePath(path: string): string {
if (path.includes('..')) {
throw new Error('Invalid path - cannot contain .. segments');
}
return path;
}
// ==================== HTTP Helpers ====================
export function isValidHttpUrl(url: string): boolean {
+13 -2
View File
@@ -138,11 +138,22 @@ describe('Validation Functions', () => {
expect(() => safeParseJson('{invalid json}')).toThrow('Invalid JSON');
});
it('should throw on code injection patterns', () => {
it('should throw on dangerous object keys', () => {
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 allow legitimate content containing function-related words', () => {
// Should not throw - these are legitimate values, not dangerous keys
expect(() =>
safeParseJson('{"message": "The function constructor is used to create objects"}')
).not.toThrow();
expect(() =>
safeParseJson('{"content": "JavaScript prototype inheritance is powerful"}')
).not.toThrow();
expect(() => safeParseJson('{"code": "function example() { return true; }"}')).not.toThrow();
expect(() => safeParseJson('{"function": "alert"}')).not.toThrow(); // function as key is now allowed
});
it('should throw on deeply nested JSON', () => {