<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>State-Management on Tarragon</title><link>https://tarrragon.github.io/blog/tags/state-management/</link><description>Recent content in State-Management on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 25 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/state-management/index.xml" rel="self" type="application/rss+xml"/><item><title>新增欄位忘記同步 reset — 跨測試狀態洩漏的系統性根因</title><link>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：&lt;code>flushing&lt;/code>（flush 併發 guard）、&lt;code>retryCount&lt;/code>（重試計數）、&lt;code>lastHeartbeat&lt;/code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 &lt;code>__reset()&lt;/code> 方法。&lt;/p>
&lt;p>測試框架在每個 test case 之間呼叫 &lt;code>__reset()&lt;/code> 清理狀態。因為 &lt;code>retryCount&lt;/code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。&lt;/p>
&lt;p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。&lt;/p>
&lt;h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化&lt;/h2>
&lt;p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。&lt;/p>
&lt;p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。&lt;/p>
&lt;p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 &lt;a href="https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict&lt;/a>。&lt;/p>
&lt;h2 id="防護state-registry-pattern">防護：State Registry Pattern&lt;/h2>
&lt;p>將所有 private 欄位的初始值集中宣告一次：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">initialState() {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">config&lt;/span>: &lt;span class="kt">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">buffer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">flushing&lt;/span>: &lt;span class="kt">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">retryCount&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">lastHeartbeat&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>reset 改用 &lt;code>Object.assign(this, initialState())&lt;/code>。新增欄位只改一處，init 和 reset 自動同步。&lt;/p>
&lt;p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：<code>flushing</code>（flush 併發 guard）、<code>retryCount</code>（重試計數）、<code>lastHeartbeat</code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 <code>__reset()</code> 方法。</p>
<p>測試框架在每個 test case 之間呼叫 <code>__reset()</code> 清理狀態。因為 <code>retryCount</code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。</p>
<p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。</p>
<h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化</h2>
<p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。</p>
<p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。</p>
<p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 <a href="/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict</a>。</p>
<h2 id="防護state-registry-pattern">防護：State Registry Pattern</h2>
<p>將所有 private 欄位的初始值集中宣告一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">initialState() {</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">config</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">buffer</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">flushing</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">retryCount</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">lastHeartbeat</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="p">};</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>reset 改用 <code>Object.assign(this, initialState())</code>。新增欄位只改一處，init 和 reset 自動同步。</p>
<p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。</p>
<h2 id="適用場景">適用場景</h2>
<p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。</p>
]]></content:encoded></item></channel></rss>