Add task UI delta metadata and resync regression coverage

This commit is contained in:
candieduniverse
2026-03-17 13:06:45 -07:00
parent eff6b337a8
commit f11f69771d
3 changed files with 198 additions and 37 deletions
@@ -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 Clines 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(...)`.
---
@@ -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()
})
})
@@ -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", () => {
</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")
})
})
})
it("requests a full-state resync when a delta sequence gap is detected", async () => {
render(
<ExtensionStateContextProvider>
<ContextProbe />
</ExtensionStateContextProvider>,
)
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(
<ExtensionStateContextProvider>
<ContextProbe />
</ExtensionStateContextProvider>,
)
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")
})
})
})