diff --git a/apps/web/app/components/chat-panel.stream-parser.test.ts b/apps/web/app/components/chat-panel.stream-parser.test.ts
new file mode 100644
index 00000000000..48cb9999aec
--- /dev/null
+++ b/apps/web/app/components/chat-panel.stream-parser.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, it } from "vitest";
+import { createStreamParser } from "./chat-panel";
+
+describe("createStreamParser", () => {
+ it("treats user-message as turn boundary and keeps user/assistant sequence stable", () => {
+ const parser = createStreamParser();
+
+ parser.processEvent({ type: "user-message", id: "u-1", text: "Draft an email" });
+ parser.processEvent({ type: "text-start" });
+ parser.processEvent({ type: "text-delta", delta: "Sure — drafting now." });
+ parser.processEvent({ type: "text-end" });
+ parser.processEvent({ type: "user-message", id: "u-2", text: "Also include follow-up" });
+ parser.processEvent({ type: "text-start" });
+ parser.processEvent({ type: "text-delta", delta: "Added a follow-up section." });
+ parser.processEvent({ type: "text-end" });
+
+ expect(parser.getParts()).toEqual([
+ { type: "user-message", id: "u-1", text: "Draft an email" },
+ { type: "text", text: "Sure — drafting now." },
+ { type: "user-message", id: "u-2", text: "Also include follow-up" },
+ { type: "text", text: "Added a follow-up section." },
+ ]);
+ });
+
+ it("accumulates tool input/output by toolCallId and preserves terminal status", () => {
+ const parser = createStreamParser();
+
+ parser.processEvent({
+ type: "tool-input-start",
+ toolCallId: "tool-1",
+ toolName: "searchDocs",
+ });
+ parser.processEvent({
+ type: "tool-input-available",
+ toolCallId: "tool-1",
+ input: { query: "workspace lock" },
+ });
+ parser.processEvent({
+ type: "tool-output-available",
+ toolCallId: "tool-1",
+ output: { hits: 3 },
+ });
+
+ parser.processEvent({
+ type: "tool-input-start",
+ toolCallId: "tool-2",
+ toolName: "writeFile",
+ });
+ parser.processEvent({
+ type: "tool-output-error",
+ toolCallId: "tool-2",
+ errorText: "permission denied",
+ });
+
+ expect(parser.getParts()).toEqual([
+ {
+ type: "dynamic-tool",
+ toolCallId: "tool-1",
+ toolName: "searchDocs",
+ state: "output-available",
+ input: { query: "workspace lock" },
+ output: { hits: 3 },
+ },
+ {
+ type: "dynamic-tool",
+ toolCallId: "tool-2",
+ toolName: "writeFile",
+ state: "error",
+ input: {},
+ output: { error: "permission denied" },
+ },
+ ]);
+ });
+
+ it("closes reasoning state on reasoning-end to prevent stuck streaming badges", () => {
+ const parser = createStreamParser();
+
+ parser.processEvent({ type: "reasoning-start" });
+ parser.processEvent({ type: "reasoning-delta", delta: "Planning edits..." });
+ parser.processEvent({ type: "reasoning-end" });
+
+ expect(parser.getParts()).toEqual([
+ { type: "reasoning", text: "Planning edits..." },
+ ]);
+ });
+
+ it("ignores unknown events without corrupting previously parsed parts", () => {
+ const parser = createStreamParser();
+
+ parser.processEvent({ type: "text-start" });
+ parser.processEvent({ type: "text-delta", delta: "Stable output" });
+ parser.processEvent({ type: "unrecognized-event", payload: "noise" });
+ parser.processEvent({ type: "text-end" });
+
+ expect(parser.getParts()).toEqual([{ type: "text", text: "Stable output" }]);
+ });
+});
diff --git a/apps/web/app/components/subagent-panel.messages.test.ts b/apps/web/app/components/subagent-panel.messages.test.ts
new file mode 100644
index 00000000000..40966fd6c05
--- /dev/null
+++ b/apps/web/app/components/subagent-panel.messages.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+import { buildMessagesFromParsed } from "./subagent-panel";
+
+describe("buildMessagesFromParsed", () => {
+ it("splits assistant output at user-message boundaries (prevents turn merging)", () => {
+ const messages = buildMessagesFromParsed("sub-1", "Initial task", [
+ { type: "text", text: "Working on it." },
+ { type: "reasoning", text: "Checking files", state: "streaming" },
+ { type: "user-message", id: "u-1", text: "Please include a summary" },
+ { type: "text", text: "Added a summary section." },
+ ]);
+
+ expect(messages).toHaveLength(4);
+ expect(messages[0]?.role).toBe("user");
+ expect(messages[1]?.role).toBe("assistant");
+ expect(messages[2]).toMatchObject({
+ id: "u-1",
+ role: "user",
+ parts: [{ type: "text", text: "Please include a summary" }],
+ });
+ expect(messages[3]).toMatchObject({
+ role: "assistant",
+ parts: [{ type: "text", text: "Added a summary section." }],
+ });
+ });
+
+ it("creates stable fallback user IDs when stream omits explicit user-message id", () => {
+ const messages = buildMessagesFromParsed("sub-2", "Task", [
+ { type: "user-message", text: "Follow-up without id" },
+ { type: "text", text: "Handled follow-up." },
+ ]);
+
+ expect(messages[1]?.id).toBe("user-sub-2-0");
+ expect(messages[1]?.role).toBe("user");
+ expect(messages[2]?.role).toBe("assistant");
+ });
+});
diff --git a/apps/web/app/components/workspace/object-filter-bar.test.tsx b/apps/web/app/components/workspace/object-filter-bar.test.tsx
new file mode 100644
index 00000000000..74f85fab7d3
--- /dev/null
+++ b/apps/web/app/components/workspace/object-filter-bar.test.tsx
@@ -0,0 +1,85 @@
+// @vitest-environment jsdom
+import React from "react";
+import { describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { emptyFilterGroup, type SavedView } from "@/lib/object-filters";
+import { ObjectFilterBar } from "./object-filter-bar";
+
+const fields = [
+ { id: "f-name", name: "Name", type: "text" },
+ { id: "f-status", name: "Status", type: "enum", enum_values: ["Important", "Backlog"] },
+];
+
+const importantView: SavedView = {
+ name: "Important",
+ filters: {
+ id: "root",
+ conjunction: "and",
+ rules: [{ id: "r-important", field: "Status", operator: "is", value: "Important" }],
+ },
+ columns: ["Name", "Status"],
+};
+
+const backlogView: SavedView = {
+ name: "Backlog",
+ filters: {
+ id: "root",
+ conjunction: "and",
+ rules: [{ id: "r-backlog", field: "Status", operator: "is", value: "Backlog" }],
+ },
+ columns: ["Name", "Status"],
+};
+
+describe("ObjectFilterBar views interaction", () => {
+ it("loads selected view from dropdown (keeps view label and table intent aligned)", async () => {
+ const user = userEvent.setup();
+ const onLoadView = vi.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /important/i }));
+ await user.click(screen.getByRole("button", { name: /backlog/i }));
+
+ expect(onLoadView).toHaveBeenCalledTimes(1);
+ expect(onLoadView).toHaveBeenCalledWith(backlogView);
+ });
+
+ it("clears active view from dropdown action (prevents sticky active-view state)", async () => {
+ const user = userEvent.setup();
+ const onFiltersChange = vi.fn();
+ const onSetActiveView = vi.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /important/i }));
+ await user.click(screen.getByRole("button", { name: /clear active view/i }));
+
+ expect(onSetActiveView).toHaveBeenCalledWith(undefined);
+ expect(onFiltersChange).toHaveBeenCalledWith(emptyFilterGroup());
+ });
+});
diff --git a/apps/web/app/components/workspace/profile-switcher.test.tsx b/apps/web/app/components/workspace/profile-switcher.test.tsx
new file mode 100644
index 00000000000..aee5bb36e58
--- /dev/null
+++ b/apps/web/app/components/workspace/profile-switcher.test.tsx
@@ -0,0 +1,91 @@
+// @vitest-environment jsdom
+import React from "react";
+import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { ProfileSwitcher } from "./profile-switcher";
+
+function jsonResponse(body: unknown, status = 200): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
+describe("ProfileSwitcher workspace delete action", () => {
+ const originalConfirm = window.confirm;
+
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ afterEach(() => {
+ window.confirm = originalConfirm;
+ });
+
+ it("deletes a profile workspace from the dropdown action", async () => {
+ const user = userEvent.setup();
+ const onWorkspaceDelete = vi.fn();
+ let profileFetchCount = 0;
+
+ global.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = typeof input === "string" ? input : (input as URL).href;
+ const method = init?.method ?? "GET";
+ if (url === "/api/profiles" && method === "GET") {
+ profileFetchCount += 1;
+ if (profileFetchCount === 1) {
+ return jsonResponse({
+ activeProfile: "work",
+ profiles: [
+ {
+ name: "work",
+ stateDir: "/home/testuser/.openclaw-work",
+ workspaceDir: "/home/testuser/.openclaw-work/workspace",
+ isActive: true,
+ hasConfig: true,
+ },
+ ],
+ });
+ }
+ return jsonResponse({
+ activeProfile: "work",
+ profiles: [
+ {
+ name: "work",
+ stateDir: "/home/testuser/.openclaw-work",
+ workspaceDir: null,
+ isActive: true,
+ hasConfig: true,
+ },
+ ],
+ });
+ }
+ if (url === "/api/workspace/delete" && method === "POST") {
+ return jsonResponse({ deleted: true, profile: "work" });
+ }
+ throw new Error(`Unexpected fetch call: ${method} ${url}`);
+ }) as typeof fetch;
+
+ window.confirm = vi.fn(() => true);
+
+ render();
+ await waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledWith("/api/profiles");
+ });
+
+ await user.click(screen.getByTitle("Switch workspace profile"));
+ await user.click(screen.getByTitle("Delete workspace for work"));
+
+ await waitFor(() => {
+ expect(onWorkspaceDelete).toHaveBeenCalledWith("work");
+ });
+
+ const deleteCall = vi
+ .mocked(global.fetch)
+ .mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : (call[0] as URL).href) === "/api/workspace/delete");
+ expect(deleteCall).toBeTruthy();
+ expect(deleteCall?.[1]).toMatchObject({
+ method: "POST",
+ });
+ });
+});