Skip to content

Commit c3dbed4

Browse files
fix(write-to-file): keep per-task stream state and release it once per failure
1 parent acd836f commit c3dbed4

10 files changed

Lines changed: 2853 additions & 90 deletions

‎src/__tests__/removeClineFromStack-delegation.spec.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ClineProvider } from "../core/webview/ClineProvider"
55
import { TaskRegistry } from "../core/task/TaskRegistry"
66
import { PendingActionSettlementError, type Task } from "../core/task/Task"
77
import { makeProviderStub } from "./helpers/provider-stub"
8+
import { writeToFileTool } from "../core/tools/WriteToFileTool"
89

910
type PrivateClineProviderMethods = {
1011
removeClineFromStack: (this: ClineProvider) => ReturnType<ClineProvider["removeClineFromStack"]>
@@ -202,6 +203,47 @@ describe("ClineProvider failed history restoration cleanup", () => {
202203
expect(task.dispose).toHaveBeenCalledOnce()
203204
})
204205

206+
it("releases the tool's per-task state before a directly disposed task loses its listeners", async () => {
207+
const key = "failed-history-task.inst-1"
208+
const order: string[] = []
209+
const task = {
210+
taskId: "failed-history-task",
211+
instanceId: "inst-1",
212+
emit: vi.fn(),
213+
once: vi.fn(),
214+
off: vi.fn(),
215+
dispose: vi.fn().mockImplementation(() => {
216+
// Dispose removes every listener, so this is the last moment the abort
217+
// cleanup could still have run; the entry must already be gone here.
218+
order.push(writeToFileTool["taskPartialStreamState"].has(key) ? "state-retained" : "state-cleared")
219+
order.push("dispose")
220+
return Promise.resolve()
221+
}),
222+
} as unknown as Task
223+
// Fixture: the state entry the tool would have created during a partial stream.
224+
writeToFileTool["taskPartialStreamState"].set(key, {
225+
lastSeenPartialPath: undefined,
226+
streamFailed: false,
227+
streamError: undefined,
228+
rollbackFailure: undefined,
229+
task,
230+
abortCleanup: () => {},
231+
})
232+
const taskEventListeners = new Map([[task, [vi.fn()]]])
233+
const taskRegistry = new TaskRegistry()
234+
taskRegistry.push(task)
235+
const provider = { taskRegistry, taskEventListeners, log: vi.fn() } as unknown as ClineProvider
236+
237+
await privateClineProvider.cleanupFailedHistoryTask.call(
238+
provider,
239+
task,
240+
new PendingActionSettlementError("settlement failed"),
241+
)
242+
243+
expect(order).toEqual(["state-cleared", "dispose"])
244+
expect(writeToFileTool["taskPartialStreamState"].size).toBe(0)
245+
})
246+
205247
it("keeps the task active after an unrelated history resume failure", async () => {
206248
const cleanupListener = vi.fn()
207249
const task = {

0 commit comments

Comments
 (0)