Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6414978
feat(task-persistence): add TaskOrganizationStore with atomic persist…
Aug 1, 2026
2926587
feat(task-org-ipc): add task organization IPC message handler and pro…
Aug 2, 2026
e139962
feat(task-organization): add DnD folder management and task grouping
k1yt Jul 25, 2026
33c76d7
fix(history): prevent workspace cross-contamination of tasks, pins, a…
Jul 27, 2026
f6cefb1
fix(history): hide workspace-specific folders when no workspace is open
Jul 27, 2026
963495c
fix: resolve TaskOrganizationStore test failures
Jul 31, 2026
e40f92d
fix(knip): ignore B10 unused file TaskStatusBadge and dnd-kit depende…
Aug 2, 2026
03d93b3
fix(history): align DraggableTaskEntry tests with role-stripping, add…
Aug 2, 2026
d8e7d6e
fix(task-organization): resolve lock bypass, root-task orphaning, sta…
Aug 3, 2026
733ec57
fix(task-organization): resolve lock bypass, root-task orphaning, sta…
Aug 3, 2026
fb167e2
fix(task-organization): reject same-revision writes and harden watche…
Aug 3, 2026
345f92b
fix(task-organization): guard reconcile against constructor-order race
Aug 3, 2026
33e19a8
fix(task-organization): guard taskOrganization revision in full-state…
Aug 3, 2026
df8ecea
fix(history): scope folder pins to the workspace filter and align emp…
Aug 3, 2026
3388c90
feat(history): add pin toggles to grouped-mode task rows for Welcome/…
Aug 3, 2026
1c07838
feat(history): show pinned shortcuts at the top of Welcome Recent Tasks
Aug 4, 2026
f4b6b94
ci: retrigger workflow after transient infra outage
Aug 6, 2026
73be783
fix: prune stale eslint-suppressions.json entries
Aug 6, 2026
a64cacd
chore: remove temp file progress.txt
Aug 6, 2026
837fdc7
feat(ci-precheck): upgrade from 7 to 12 checks with conditional execu…
Aug 6, 2026
5a5b996
fix(ci): strip BOM in ExtensionStateContext and boost test coverage t…
Aug 7, 2026
4c0abc2
test(e2e): add task organization IPC suite
Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 435 additions & 0 deletions .roo/skills/local-ci-precheck/SKILL.md

Large diffs are not rendered by default.

248 changes: 248 additions & 0 deletions apps/vscode-e2e/src/suite/task-org-ipc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import * as assert from "assert"
import * as vscode from "vscode"

import type { TaskOrganizationMutationResultV1, TaskOrganizationStateV1 } from "@roo-code/types"

import { setDefaultSuiteTimeout } from "./test-utils"
import { waitFor } from "./utils"

/**
* E2E tests for the Task Organization IPC bridge.
*
* The bridge lives in `src/core/webview/taskOrganizationMessageHandler.ts` and
* `src/core/task-persistence/TaskOrganizationStore.ts`. It receives
* `taskOrganizationMutation` webview messages, validates them with Zod, applies
* them through the `TaskOrganizationStore`, and posts a typed
* `taskOrganizationMutationResult` back to the webview correlated by `requestId`.
*
* These tests exercise the full round-trip: message dispatch → store mutation →
* result posting → state broadcast (`taskOrganizationUpdated` / `state`).
*
* Because e2e tests run inside the extension host, we can access the real
* `ClineProvider` instance and its `TaskOrganizationStore` directly. We also
* listen on the provider's `postMessageToWebview` to capture the IPC result
* messages that would normally be sent to the webview.
*/
suite("Task Organization IPC Bridge", function () {
setDefaultSuiteTimeout(this)

/**
* Get the visible ClineProvider instance. The extension host should already
* have focused the sidebar during suite setup.
*
* Uses `require()` to bypass TypeScript cross-package module resolution
* limitations in the e2e test project.
*/
function getProvider() {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { ClineProvider } = require("../../../src/core/webview/ClineProvider")
const provider = ClineProvider.getVisibleInstance()
assert.ok(provider, "ClineProvider visible instance should be available")
return provider
}

/**
* Send a mutation through the real handler and capture the result message
* that would be posted to the webview.
*/
async function sendMutationAndWaitForResult(
provider: ReturnType<typeof getProvider>,
request: {
requestId: string
baseRevision: number
mutation: Record<string, unknown>
},
): Promise<TaskOrganizationMutationResultV1> {
const results: TaskOrganizationMutationResultV1[] = []

// Spy on postMessageToWebview to capture the result.
const originalPostMessage = provider.postMessageToWebview.bind(provider)
provider.postMessageToWebview = async (message: unknown) => {
const msg = message as { type?: string; requestId?: string; taskOrganizationMutationResult?: TaskOrganizationMutationResultV1 }
if (msg.type === "taskOrganizationMutationResult" && msg.requestId === request.requestId) {
results.push(msg.taskOrganizationMutationResult!)
}
return originalPostMessage(message)
}

try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { handleTaskOrganizationMessage } = require("../../../src/core/webview/taskOrganizationMessageHandler")

await handleTaskOrganizationMessage(provider, {
type: "taskOrganizationMutation",
taskOrganizationMutation: request,
})

await waitFor(() => results.length > 0, { timeout: 10_000, interval: 100 })
return results[0]!
} finally {
provider.postMessageToWebview = originalPostMessage
}
}

/**
* Read the current task organization state from the provider's store.
*/
async function getTaskOrganizationState(
provider: ReturnType<typeof getProvider>,
): Promise<TaskOrganizationStateV1> {
const store = provider.getTaskOrganizationStore()
await store.waitForInitialized()
return store.getState()
}

test("createFolder mutation round-trips and updates state", async () => {
const provider = getProvider()

const stateBefore = await getTaskOrganizationState(provider)
const baseRevision = stateBefore.revision

const result = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-create-folder-001",
baseRevision,
mutation: {
kind: "createFolder",
folderId: "e2e-folder-001",
name: "E2E Test Folder",
source: { kind: "task", taskId: "e2e-task-001" },
destination: { kind: "folder", folderId: "e2e-folder-001" },
},
})

assert.strictEqual(result.success, true, `Expected success but got error: ${result.error?.message}`)
assert.strictEqual(result.requestId, "e2e-create-folder-001")
assert.ok(result.committedRevision > baseRevision, "revision should increment after mutation")

// Verify the store state reflects the new folder.
const stateAfter = await getTaskOrganizationState(provider)
const folder = stateAfter.folders.find((f) => f.folderId === "e2e-folder-001")
assert.ok(folder, "created folder should appear in state")
assert.strictEqual(folder.name, "E2E Test Folder")
})

test("moveToFolder mutation adds task to existing folder", async () => {
const provider = getProvider()

// Ensure the folder from the previous test exists (or create it here).
let state = await getTaskOrganizationState(provider)
if (!state.folders.some((f) => f.folderId === "e2e-folder-001")) {
const createResult = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-create-folder-002",
baseRevision: state.revision,
mutation: {
kind: "createFolder",
folderId: "e2e-folder-001",
name: "E2E Test Folder",
source: { kind: "task", taskId: "e2e-task-001" },
destination: { kind: "folder", folderId: "e2e-folder-001" },
},
})
assert.strictEqual(createResult.success, true)
state = await getTaskOrganizationState(provider)
}

const baseRevision = state.revision

const moveResult = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-move-task-001",
baseRevision,
mutation: {
kind: "moveToFolder",
source: { kind: "task", taskId: "e2e-task-002" },
folderId: "e2e-folder-001",
},
})

assert.strictEqual(moveResult.success, true, `Expected success but got error: ${moveResult.error?.message}`)
assert.strictEqual(moveResult.requestId, "e2e-move-task-001")

const stateAfter = await getTaskOrganizationState(provider)
const folder = stateAfter.folders.find((f) => f.folderId === "e2e-folder-001")
assert.ok(folder?.taskIds.includes("e2e-task-002"), "task should be present in folder")
})

test("setPinned mutation toggles pin state and respects pin limit", async () => {
const provider = getProvider()

// Pin a task.
let state = await getTaskOrganizationState(provider)
let baseRevision = state.revision

const pinResult = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-pin-001",
baseRevision,
mutation: {
kind: "setPinned",
target: { kind: "task", taskId: "e2e-task-001" },
pinned: true,
},
})

assert.strictEqual(pinResult.success, true, `Expected success but got error: ${pinResult.error?.message}`)

state = await getTaskOrganizationState(provider)
const pinned = state.pins.find((p) => p.target.kind === "task" && p.target.taskId === "e2e-task-001")
assert.ok(pinned, "task should be pinned")

// Unpin the same task.
baseRevision = state.revision

const unpinResult = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-unpin-001",
baseRevision,
mutation: {
kind: "setPinned",
target: { kind: "task", taskId: "e2e-task-001" },
pinned: false,
},
})

assert.strictEqual(unpinResult.success, true)

state = await getTaskOrganizationState(provider)
const stillPinned = state.pins.find((p) => p.target.kind === "task" && p.target.taskId === "e2e-task-001")
assert.strictEqual(stillPinned, undefined, "task should be unpinned")
})

test("invalid mutation payload returns validation error", async () => {
const provider = getProvider()

const result = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-invalid-001",
baseRevision: 0,
mutation: {
kind: "unknownKind",
foo: "bar",
},
})

assert.strictEqual(result.success, false)
assert.strictEqual(result.error?.code, "TASK_ORG/VALIDATION/001")
assert.ok(result.error?.message.includes("Invalid mutation request"))
})

test("stale baseRevision returns conflict error", async () => {
const provider = getProvider()

const state = await getTaskOrganizationState(provider)
const currentRevision = state.revision

// Send a mutation with a deliberately stale revision.
const result = await sendMutationAndWaitForResult(provider, {
requestId: "e2e-stale-001",
baseRevision: currentRevision - 1,
mutation: {
kind: "createFolder",
folderId: "e2e-folder-stale",
name: "Stale Folder",
source: { kind: "task", taskId: "e2e-task-stale" },
destination: { kind: "folder", folderId: "e2e-folder-stale" },
},
})

assert.strictEqual(result.success, false)
assert.strictEqual(result.error?.code, "TASK_ORG/CONFLICT/002")
assert.ok(result.error?.message.includes("Organization state has changed"))
})
})
7 changes: 6 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"webview-ui": {
"entry": ["src/index.tsx"],
"project": ["src/**/*.{ts,tsx}", "../src/shared/*.ts"],
"ignore": [
"src/components/history/TaskStatusBadge.tsx"
],
"ignoreDependencies": [
"@roo-code/config-typescript",
"@types/katex",
Expand All @@ -32,7 +35,9 @@
"source-map",
"tailwindcss",
"tailwindcss-animate",
"monocart-reporter"
"monocart-reporter",
"@dnd-kit/sortable",
"@dnd-kit/utilities"
]
},
"apps/cli": {
Expand Down
Loading
Loading