From f11f69771d1b44dae95668720cb7860000671a14 Mon Sep 17 00:00:00 2001 From: candieduniverse <132302818+candieduniverse@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:50:28 -0700 Subject: [PATCH] Add task UI delta metadata and resync regression coverage --- ...cy-technique-04-task-ui-delta-sync-plan.md | 13 +- src/test/controller-task-ui-metadata.test.ts | 67 ++++++++ .../context/ExtensionStateContext.test.tsx | 155 ++++++++++++++---- 3 files changed, 198 insertions(+), 37 deletions(-) create mode 100644 src/test/controller-task-ui-metadata.test.ts diff --git a/docs/remote-workspace-latency-technique-04-task-ui-delta-sync-plan.md b/docs/remote-workspace-latency-technique-04-task-ui-delta-sync-plan.md index fe6527342a..1de170f883 100644 --- a/docs/remote-workspace-latency-technique-04-task-ui-delta-sync-plan.md +++ b/docs/remote-workspace-latency-technique-04-task-ui-delta-sync-plan.md @@ -272,8 +272,8 @@ Keep the fallback path boring and reliable. Smart engineering here means preferr ### Tests -- [ ] Unit test: metadata delta publishes for current active task. -- [ ] Unit test: mismatched/non-active task falls back to snapshot path. +- [x] Unit test: metadata delta publishes for current active task. +- [x] Unit test: mismatched/non-active task falls back to snapshot path. --- @@ -351,8 +351,8 @@ This step is essential to keeping the rest of Cline’s product surfaces healthy ### Tests - [x] Regression test: initial load hydrates correctly without prior deltas. -- [ ] Regression test: reopening or task switching still works. -- [ ] Regression test: full snapshot repairs intentionally diverged delta state. +- [x] Regression test: reopening or task switching still works. +- [x] Regression test: full snapshot repairs intentionally diverged delta state. --- @@ -471,12 +471,13 @@ That is why this technique still matters for large-file-write scenarios, even th - Added extraction-branch follow-up coverage in commit `8839a5bf6` (`Add task UI delta sync test coverage and env flag helper`), including backend delta broadcaster tests, latency/env-flag helper coverage, reducer sequencing tests, and a webview context delta hydration test. - Added latency-analysis helpers and validation scripts for comparing delta-enabled vs delta-disabled runs (`src/services/telemetry/taskLatencySummary.ts`, `scripts/validate-latency-scenarios.ts`, `scripts/analyze-task-latency-metrics.mjs`, and `scripts/compare-task-latency-metrics.mjs`). - Added message-state regression coverage in commit `cdee38396` (`Add message-state task UI delta regression tests`) and fixed verification follow-up issues in commit `5548080d9`. +- Added controller metadata delta coverage (`src/test/controller-task-ui-metadata.test.ts`) plus webview resync/task-switch regression coverage in `webview-ui/src/context/ExtensionStateContext.test.tsx`. - Wired focus-chain metadata and background-command metadata through task-specific delta publication, with snapshot fallback when task identity is ambiguous. - Preserved snapshot hydration/resync semantics alongside delta application and added frontend debug counters for snapshot, partial-message, delta, and resync activity. - Installed dependencies, regenerated protos, and verified the focused backend and webview coverage locally. Successful verification included: - - `npm run test:unit -- src/core/controller/ui/subscribeToTaskUiDeltas.test.ts src/test/message-state-handler.test.ts src/core/task/__tests__/latency.test.ts src/services/telemetry/__tests__/taskLatencySummary.test.ts` + - `npm run test:unit -- src/test/controller-task-ui-metadata.test.ts src/core/controller/ui/subscribeToTaskUiDeltas.test.ts src/test/message-state-handler.test.ts src/core/task/__tests__/latency.test.ts src/services/telemetry/__tests__/taskLatencySummary.test.ts` - `cd webview-ui && npm run test -- src/context/taskUiDeltaState.test.ts src/context/ExtensionStateContext.test.tsx` -- The webview verification passes, but still emits React `act(...)` warnings in `ExtensionStateContext.test.tsx`; these are test-harness hygiene issues rather than functional failures. +- The webview verification now passes without the earlier React `act(...)` warning noise after wrapping streamed state updates in `act(...)`. --- diff --git a/src/test/controller-task-ui-metadata.test.ts b/src/test/controller-task-ui-metadata.test.ts new file mode 100644 index 0000000000..efc2d340ed --- /dev/null +++ b/src/test/controller-task-ui-metadata.test.ts @@ -0,0 +1,67 @@ +import { describe, it } from "mocha" +import "should" +import * as sinon from "sinon" +import { Controller } from "../core/controller" +import * as taskUiDeltaModule from "../core/controller/ui/subscribeToTaskUiDeltas" + +describe("Controller.postTaskMetadataDelta", () => { + it("publishes metadata deltas for the current active task", async () => { + const sendTaskUiDeltaStub = sinon.stub(taskUiDeltaModule, "sendTaskUiDelta").resolves(undefined) + const postStateToWebview = sinon.stub().resolves() + + const fakeController = { + task: { + taskId: "task-1", + taskState: { + taskUiDeltaSequence: 0, + }, + }, + postStateToWebview, + } + + await Controller.prototype.postTaskMetadataDelta.call(fakeController as any, { + backgroundCommandRunning: true, + backgroundCommandTaskId: "task-1", + }) + + sinon.assert.calledOnce(sendTaskUiDeltaStub) + sinon.assert.calledWithExactly(sendTaskUiDeltaStub, { + type: "task_metadata_updated", + taskId: "task-1", + sequence: 1, + metadata: { + backgroundCommandRunning: true, + backgroundCommandTaskId: "task-1", + }, + }) + sinon.assert.notCalled(postStateToWebview) + + sendTaskUiDeltaStub.restore() + }) + + it("falls back to posting full state when task identity is missing or mismatched", async () => { + const sendTaskUiDeltaStub = sinon.stub(taskUiDeltaModule, "sendTaskUiDelta").resolves(undefined) + const postStateToWebview = sinon.stub().resolves() + + const fakeController = { + task: { + taskId: "task-1", + taskState: { + taskUiDeltaSequence: 4, + }, + }, + postStateToWebview, + } + + await Controller.prototype.postTaskMetadataDelta.call( + fakeController as any, + { currentFocusChainChecklist: "- [x] one" }, + "task-2", + ) + + sinon.assert.notCalled(sendTaskUiDeltaStub) + sinon.assert.calledOnce(postStateToWebview) + + sendTaskUiDeltaStub.restore() + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.test.tsx b/webview-ui/src/context/ExtensionStateContext.test.tsx index 8c721acaac..56c85e58ae 100644 --- a/webview-ui/src/context/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/ExtensionStateContext.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from "@testing-library/react" +import { act, render, screen, waitFor } from "@testing-library/react" import { describe, expect, it, vi } from "vitest" import { ExtensionStateContextProvider, useExtensionState } from "./ExtensionStateContext" @@ -90,26 +90,30 @@ describe("ExtensionStateContextProvider", () => { , ) - subscriptions.state?.onResponse?.({ - stateJson: JSON.stringify({ - version: "initial", - clineMessages: [], - currentTaskItem: { id: "task-1" }, - backgroundCommandRunning: false, - }), + await act(async () => { + subscriptions.state?.onResponse?.({ + stateJson: JSON.stringify({ + version: "initial", + clineMessages: [], + currentTaskItem: { id: "task-1" }, + backgroundCommandRunning: false, + }), + }) }) await waitFor(() => { expect(screen.getByTestId("version").textContent).toBe("initial") }) - subscriptions.delta?.onResponse?.({ - deltaJson: JSON.stringify({ - type: "message_added", - taskId: "task-1", - sequence: 1, - message: { ts: 1, type: "say", say: "text", text: "hello" }, - }), + await act(async () => { + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "message_added", + taskId: "task-1", + sequence: 1, + message: { ts: 1, type: "say", say: "text", text: "hello" }, + }), + }) }) await waitFor(() => { @@ -117,21 +121,23 @@ describe("ExtensionStateContextProvider", () => { expect(screen.getByTestId("latest-message").textContent).toBe("hello") }) - subscriptions.delta?.onResponse?.({ - deltaJson: JSON.stringify({ - type: "message_updated", - taskId: "task-1", - sequence: 2, - message: { ts: 1, type: "say", say: "text", text: "hello world" }, - }), - }) - subscriptions.delta?.onResponse?.({ - deltaJson: JSON.stringify({ - type: "task_metadata_updated", - taskId: "task-1", - sequence: 3, - metadata: { backgroundCommandRunning: true, backgroundCommandTaskId: "task-1" }, - }), + await act(async () => { + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "message_updated", + taskId: "task-1", + sequence: 2, + message: { ts: 1, type: "say", say: "text", text: "hello world" }, + }), + }) + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "task_metadata_updated", + taskId: "task-1", + sequence: 3, + metadata: { backgroundCommandRunning: true, backgroundCommandTaskId: "task-1" }, + }), + }) }) await waitFor(() => { @@ -139,4 +145,91 @@ describe("ExtensionStateContextProvider", () => { expect(screen.getByTestId("background-command").textContent).toBe("true") }) }) -}) \ No newline at end of file + + it("requests a full-state resync when a delta sequence gap is detected", async () => { + render( + + + , + ) + + await act(async () => { + subscriptions.state?.onResponse?.({ + stateJson: JSON.stringify({ + version: "initial", + clineMessages: [], + currentTaskItem: { id: "task-1" }, + }), + }) + }) + + await act(async () => { + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "message_added", + taskId: "task-1", + sequence: 2, + message: { ts: 2, type: "say", say: "text", text: "should trigger resync" }, + }), + }) + }) + + await waitFor(() => { + expect(screen.getByTestId("version").textContent).toBe("resynced") + expect(screen.getByTestId("latest-message").textContent).toBe("resynced") + }) + }) + + it("resets delta sequencing when a full snapshot switches to a different task", async () => { + render( + + + , + ) + + await act(async () => { + subscriptions.state?.onResponse?.({ + stateJson: JSON.stringify({ + version: "task-1-state", + clineMessages: [], + currentTaskItem: { id: "task-1" }, + }), + }) + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "message_added", + taskId: "task-1", + sequence: 1, + message: { ts: 1, type: "say", say: "text", text: "task one" }, + }), + }) + }) + + await waitFor(() => { + expect(screen.getByTestId("latest-message").textContent).toBe("task one") + }) + + await act(async () => { + subscriptions.state?.onResponse?.({ + stateJson: JSON.stringify({ + version: "task-2-state", + clineMessages: [], + currentTaskItem: { id: "task-2" }, + }), + }) + subscriptions.delta?.onResponse?.({ + deltaJson: JSON.stringify({ + type: "message_added", + taskId: "task-2", + sequence: 1, + message: { ts: 2, type: "say", say: "text", text: "task two" }, + }), + }) + }) + + await waitFor(() => { + expect(screen.getByTestId("version").textContent).toBe("task-2-state") + expect(screen.getByTestId("latest-message").textContent).toBe("task two") + }) + }) +})