mirror of
https://github.com/cline/cline.git
synced 2026-09-21 05:10:09 +08:00
Add task UI delta metadata and resync regression coverage
This commit is contained in:
@@ -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(...)`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user