FN-6750: harden task chat steering coverage

Verify task chat timestamps and immediate steering delivery across UI, route, and executor surfaces.

- Assert inline and expanded TaskChatTab timestamp parity for agent output and user steering comments.
- Cover steering route wake payloads so assigned agents receive the newest steering comment id immediately.
- Harden executor real-time steering tests for seen-before-inject ordering, queued prompt delivery, duplicate suppression, and empty-comment no-ops.

Files changed:
 .../app/components/__tests__/TaskChatTab.test.tsx  |  79 +++++++++++-
 .../src/__tests__/routes-tasks-ops.test.ts         | 136 +++++++++++++++++++++
 .../src/__tests__/executor-step-session.test.ts    | 116 ++++++++++++++++--
 3 files changed, 316 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-6750

Fusion-Task-Lineage: 48e1d57d-fd37-4d45-9b98-766c4a2f704d
This commit is contained in:
gsxdsm
2026-06-19 22:24:18 -07:00
parent d4d7623ee0
commit a63cf1c911
3 changed files with 316 additions and 15 deletions

View File

@@ -332,9 +332,14 @@ describe("TaskChatTab", () => {
expect(mockedUseAgentLogs).toHaveBeenCalledWith("FN-001", false, "project-1");
});
it("renders empty state when no agent output exists", () => {
it("renders empty state without timestamp shells when no transcript messages exist", () => {
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
expect(screen.getByText(/No agent output yet/)).toBeTruthy();
const transcript = screen.getByTestId("task-chat-transcript");
expect(within(transcript).getByText(/No agent output yet/)).toBeTruthy();
expect(within(transcript).queryByTestId("task-chat-group-time")).not.toBeInTheDocument();
expect(within(transcript).queryByTestId("task-chat-user-time")).not.toBeInTheDocument();
expect(transcript).not.toHaveTextContent(/NaN|Invalid Date/);
});
it("renders the collapsed icon-only expand toggle inside the chat view and calls the toggle handler", () => {
@@ -461,6 +466,65 @@ describe("TaskChatTab", () => {
expect(within(groupMeta as HTMLElement).getByTestId("task-chat-group-time")).toHaveTextContent("2m ago");
});
it("keeps agent and user timestamp parity in the inline chat surface", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-17T15:00:00.000Z"));
mockLogs([
makeEntry({ agent: "executor", text: "inline agent response", timestamp: "2026-06-17T14:58:30.000Z" }),
]);
render(
<TaskChatTab
task={makeTask({
steeringComments: [makeSteeringComment({ id: "inline-user", text: "inline user guidance", createdAt: "2026-06-17T14:57:00.000Z" })],
})}
active
expanded={false}
addToast={vi.fn()}
/>,
);
const transcript = screen.getByTestId("task-chat-transcript");
expect(within(transcript).getByText("inline agent response")).toBeVisible();
expect(within(transcript).getByText("inline user guidance")).toBeVisible();
expect(within(transcript).getByTestId("task-chat-group-time")).toHaveTextContent("1m ago");
expect(within(transcript).getByTestId("task-chat-user-time")).toHaveTextContent("3m ago");
});
it("keeps agent and user timestamp parity in the expanded chat surface", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-17T15:00:00.000Z"));
mockLogs([
makeEntry({ agent: "executor", text: "expanded single entry", timestamp: "2026-06-17T14:59:30.000Z" }),
makeEntry({ agent: "reviewer", text: "expanded older reviewer entry", timestamp: "2026-06-17T14:53:00.000Z" }),
makeEntry({ agent: "reviewer", text: "expanded latest reviewer entry", timestamp: "2026-06-17T14:55:00.000Z" }),
]);
render(
<TaskChatTab
task={makeTask({
steeringComments: [makeSteeringComment({ id: "expanded-user", text: "expanded user guidance", createdAt: "2026-06-17T14:57:00.000Z" })],
})}
active
expanded
onToggleExpanded={vi.fn()}
addToast={vi.fn()}
/>,
);
const executorMeta = screen.getByLabelText("Executor messages").querySelector(".task-chat-group-meta");
const reviewerMeta = screen.getByLabelText("Reviewer messages").querySelector(".task-chat-group-meta");
const userHeader = screen.getByText("You").closest(".task-chat-user-header");
expect(executorMeta).not.toBeNull();
expect(reviewerMeta).not.toBeNull();
expect(userHeader).not.toBeNull();
expect(within(executorMeta as HTMLElement).getByText("1 entry")).toBeVisible();
expect(within(executorMeta as HTMLElement).getByTestId("task-chat-group-time")).toHaveTextContent("just now");
expect(within(reviewerMeta as HTMLElement).getByText("2 entries")).toBeVisible();
expect(within(reviewerMeta as HTMLElement).getByTestId("task-chat-group-time")).toHaveTextContent("5m ago");
expect(within(userHeader as HTMLElement).getByTestId("task-chat-user-time")).toHaveTextContent("3m ago");
});
it("renders a single text entry as one text bubble", () => {
mockLogs([
makeEntry({ agent: "executor", text: "single response" }),
@@ -2100,6 +2164,17 @@ describe("TaskChatTab", () => {
expect(mobileInputRule).toContain("min-height: calc(var(--space-2xl) + var(--space-lg))");
});
it("keeps TaskDetailModal inline and expanded chat on the canonical TaskChatTab renderer", () => {
const source = readFileSync(resolve(__dirname, "../TaskDetailModal.tsx"), "utf8");
const taskChatMounts = source.match(/<TaskChatTab\b/g) ?? [];
expect(source).toContain('import { TaskChatTab } from "./TaskChatTab"');
expect(taskChatMounts).toHaveLength(1);
expect(source).toContain("const isChatExpanded = chatExpanded && activeTab === \"chat\" && !isEditing");
expect(source).toContain("task-detail-content--chat-expanded");
expect(source).toContain("expanded={chatExpanded}");
});
it("keeps task chat timestamp styling tokenized and mobile-safe", () => {
const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8");
const groupMetaRule = getCssRuleBlock(css, ".task-chat-group-meta");

View File

@@ -10,6 +10,7 @@ import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { createHmac } from "node:crypto";
import { createApiRoutes } from "../routes.js";
import { registerTaskWorkflowRoutes } from "../routes/register-task-workflow-routes.js";
import {
getProjectIdFromRequest as getProjectIdFromRouteRequest,
getProjectContext as resolveRouteProjectContext,
@@ -328,6 +329,141 @@ describe("POST /tasks/:id/steer", () => {
return app;
}
it("passes the new steering comment id to the task-workflow wake dependency", async () => {
const scopedStore = createMockStore();
const updatedTask = {
...FAKE_TASK_DETAIL,
id: "FN-001",
column: "in-progress" as const,
assignedAgentId: "agent-1",
steeringComments: [{ id: "steer-route-1", text: "Please continue", author: "user" as const, createdAt: "2026-06-12T00:00:00.000Z" }],
};
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
const router = express.Router();
registerTaskWorkflowRoutes({
router,
store: scopedStore,
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
planningLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
chatLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
getProjectIdFromRequest: () => undefined,
getScopedStore: async () => scopedStore,
getProjectContext: async () => ({ store: scopedStore, projectId: undefined }),
prioritizeProjectsForCurrentDirectory: (projects) => projects,
emitRemoteRouteDiagnostic: vi.fn(),
emitAuthSyncAuditLog: vi.fn(),
parseScopeParam: () => undefined,
resolveAutomationStore: vi.fn() as any,
resolveRoutineStore: vi.fn() as any,
resolveRoutineRunner: vi.fn() as any,
registerDispose: vi.fn(),
dispose: vi.fn(),
rethrowAsApiError: (error) => { throw error; },
}, {
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
upload: { single: vi.fn(() => (_req: unknown, _res: unknown, next: () => void) => next()) },
taskDetailActivityLogLimit: 100,
validateOptionalModelField: () => undefined,
normalizeModelSelectionPair: (provider, modelId) => ({ provider, modelId }),
runGitCommand: vi.fn(),
isGitRepo: vi.fn(),
resolveIntegrationBranch: vi.fn(),
trimTaskDetailActivityLog: (task) => task,
triggerCommentWakeForAssignedAgent,
resolveSelfHealingManager: () => undefined,
});
const app = express();
app.use(express.json());
app.use("/api", router);
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Please continue" }), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(scopedStore.addSteeringComment).toHaveBeenCalledWith("FN-001", "Please continue", "user");
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledOnce();
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledWith(scopedStore, updatedTask, {
triggeringCommentType: "steering",
triggeringCommentIds: ["steer-route-1"],
triggerDetail: "steering-comment",
});
});
it("uses the newest steering comment id when waking assigned agents", async () => {
const scopedStore = createMockStore();
const updatedTask = {
...FAKE_TASK_DETAIL,
id: "FN-001",
column: "in-progress" as const,
assignedAgentId: "agent-1",
steeringComments: [
{
id: "older-steer",
text: "Earlier guidance",
author: "user" as const,
createdAt: "2026-06-12T00:00:00.000Z",
},
{
id: "newest-steer",
text: "Newest guidance",
author: "user" as const,
createdAt: "2026-06-12T00:01:00.000Z",
},
],
};
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
const router = express.Router();
registerTaskWorkflowRoutes({
router,
store: scopedStore,
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
planningLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
chatLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
getProjectIdFromRequest: () => undefined,
getScopedStore: async () => scopedStore,
getProjectContext: async () => ({ store: scopedStore, projectId: undefined }),
prioritizeProjectsForCurrentDirectory: (projects) => projects,
emitRemoteRouteDiagnostic: vi.fn(),
emitAuthSyncAuditLog: vi.fn(),
parseScopeParam: () => undefined,
resolveAutomationStore: vi.fn() as any,
resolveRoutineStore: vi.fn() as any,
resolveRoutineRunner: vi.fn() as any,
registerDispose: vi.fn(),
dispose: vi.fn(),
rethrowAsApiError: (error) => { throw error; },
}, {
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
upload: { single: vi.fn(() => (_req: unknown, _res: unknown, next: () => void) => next()) },
taskDetailActivityLogLimit: 100,
validateOptionalModelField: () => undefined,
normalizeModelSelectionPair: (provider, modelId) => ({ provider, modelId }),
runGitCommand: vi.fn(),
isGitRepo: vi.fn(),
resolveIntegrationBranch: vi.fn(),
trimTaskDetailActivityLog: (task) => task,
triggerCommentWakeForAssignedAgent,
resolveSelfHealingManager: () => undefined,
});
const app = express();
app.use(express.json());
app.use("/api", router);
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Newest guidance" }), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledWith(scopedStore, updatedTask, {
triggeringCommentType: "steering",
triggeringCommentIds: ["newest-steer"],
triggerDetail: "steering-comment",
});
});
it("records user steering comments and wakes the assigned immediate-response agent", async () => {
const updatedTask = {
...FAKE_TASK_DETAIL,

View File

@@ -3090,7 +3090,13 @@ describe("Real-time steering injection", () => {
it("injects new steering comments via active StepSessionExecutor on task:updated", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const steerActiveSessions = vi.fn().mockResolvedValue(1);
const seenIds = new Set<string>();
const updateSteeringComments = vi.fn();
const steerActiveSessions = vi.fn().mockImplementation(async () => {
expect(updateSteeringComments).toHaveBeenCalledWith([newComment]);
expect(seenIds.has(newComment.id)).toBe(true);
return 1;
});
const markSteeringCommentsDelivered = vi.fn();
const newComment = {
id: "step-session-comment",
@@ -3099,8 +3105,12 @@ describe("Real-time steering injection", () => {
author: "user" as const,
};
(executor as any).activeStepExecutors.set("FN-001", { steerActiveSessions, markSteeringCommentsDelivered });
(executor as any).activeStepExecutorSeenSteeringIds.set("FN-001", new Set());
(executor as any).activeStepExecutors.set("FN-001", {
steerActiveSessions,
markSteeringCommentsDelivered,
updateSteeringComments,
});
(executor as any).activeStepExecutorSeenSteeringIds.set("FN-001", seenIds);
await (store as any)._triggerAsync("task:updated", {
id: "FN-001",
@@ -3116,6 +3126,7 @@ describe("Real-time steering injection", () => {
updatedAt: new Date().toISOString(),
});
expect(updateSteeringComments).toHaveBeenCalledOnce();
expect(steerActiveSessions).toHaveBeenCalledOnce();
expect(steerActiveSessions.mock.calls[0][0]).toContain("📣 **New feedback**");
expect(steerActiveSessions.mock.calls[0][0]).toContain("Please adjust the active step");
@@ -3130,33 +3141,37 @@ describe("Real-time steering injection", () => {
it("queues step-session steering comments for the next prompt when no step session is active", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const steerActiveSessions = vi.fn().mockResolvedValue(0);
const updateSteeringComments = vi.fn();
const markSteeringCommentsDelivered = vi.fn();
const newComment = {
id: "step-session-queued-comment",
text: "Please apply this in the next step prompt",
createdAt: new Date().toISOString(),
author: "user" as const,
};
(executor as any).activeStepExecutors.set("FN-001", {
steerActiveSessions,
updateSteeringComments,
markSteeringCommentsDelivered,
const { StepSessionExecutor: ActualStepSessionExecutor } = await vi.importActual<typeof import("../step-session-executor.js")>("../step-session-executor.js");
const stepExecutor = new ActualStepSessionExecutor({
taskDetail: makeSteeringTask() as any,
worktreePath: "/tmp/test",
rootDir: "/tmp",
settings: {} as any,
});
const markSteeringCommentsDelivered = vi.spyOn(stepExecutor, "markSteeringCommentsDelivered");
(executor as any).activeStepExecutors.set("FN-001", stepExecutor);
(executor as any).activeStepExecutorSeenSteeringIds.set("FN-001", new Set());
await (store as any)._triggerAsync("task:updated", makeSteeringTask([newComment]));
expect(steerActiveSessions).toHaveBeenCalledOnce();
expect(updateSteeringComments).toHaveBeenCalledWith([newComment]);
expect(markSteeringCommentsDelivered).not.toHaveBeenCalled();
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("Comment received mid-execution"),
"by user",
);
const nextPromptTask = (stepExecutor as any).consumeTaskDetailForStepPrompt();
expect(nextPromptTask.steeringComments).toEqual([newComment]);
const laterPromptTask = (stepExecutor as any).consumeTaskDetailForStepPrompt();
expect(laterPromptTask.steeringComments).toBeUndefined();
});
it("injects new steering comments via active workflow step session on task:updated", async () => {
@@ -3197,6 +3212,52 @@ describe("Real-time steering injection", () => {
);
});
it("marks new comments seen before injecting and logs once across simultaneous surfaces", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const newComment = {
id: "shared-surface-comment",
text: "Please reach every live surface once",
createdAt: new Date().toISOString(),
author: "user" as const,
};
const legacySeen = new Set<string>();
const stepSeen = new Set<string>();
const workflowSeen = new Set<string>();
const legacySteer = vi.fn().mockImplementation(async () => {
expect(legacySeen.has(newComment.id)).toBe(true);
});
const stepSteerActiveSessions = vi.fn().mockImplementation(async () => {
expect(stepSeen.has(newComment.id)).toBe(true);
return 1;
});
const workflowSteer = vi.fn().mockImplementation(async () => {
expect(workflowSeen.has(newComment.id)).toBe(true);
});
const markSteeringCommentsDelivered = vi.fn();
setLegacyActiveSession(executor, legacySteer, legacySeen);
(executor as any).activeStepExecutors.set("FN-001", { steerActiveSessions: stepSteerActiveSessions, markSteeringCommentsDelivered });
(executor as any).activeStepExecutorSeenSteeringIds.set("FN-001", stepSeen);
(executor as any).activeWorkflowStepSessions.set("FN-001", { steer: workflowSteer });
(executor as any).activeWorkflowStepSessionSeenSteeringIds.set("FN-001", workflowSeen);
await (store as any)._triggerAsync("task:updated", makeSteeringTask([newComment]));
await (store as any)._triggerAsync("task:updated", makeSteeringTask([newComment]));
expect(legacySteer).toHaveBeenCalledOnce();
expect(stepSteerActiveSessions).toHaveBeenCalledOnce();
expect(workflowSteer).toHaveBeenCalledOnce();
expect(markSteeringCommentsDelivered).toHaveBeenCalledOnce();
expect(markSteeringCommentsDelivered).toHaveBeenCalledWith([newComment.id]);
expect(store.logEntry).toHaveBeenCalledTimes(1);
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("Comment received mid-execution"),
"by user",
);
});
it("does not re-inject an already seen active StepSessionExecutor steering comment", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
@@ -3263,6 +3324,35 @@ describe("Real-time steering injection", () => {
expect(steerFn).toHaveBeenCalledTimes(1);
});
it("does not inject or log when active surfaces receive empty or undefined steering comments", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const legacySteer = vi.fn().mockResolvedValue(undefined);
const stepSteerActiveSessions = vi.fn().mockResolvedValue(1);
const workflowSteer = vi.fn().mockResolvedValue(undefined);
setLegacyActiveSession(executor, legacySteer);
(executor as any).activeStepExecutors.set("FN-001", {
steerActiveSessions: stepSteerActiveSessions,
updateSteeringComments: vi.fn(),
});
(executor as any).activeStepExecutorSeenSteeringIds.set("FN-001", new Set());
(executor as any).activeWorkflowStepSessions.set("FN-001", { steer: workflowSteer });
(executor as any).activeWorkflowStepSessionSeenSteeringIds.set("FN-001", new Set());
await (store as any)._triggerAsync("task:updated", { ...makeSteeringTask(), steeringComments: undefined });
await (store as any)._triggerAsync("task:updated", makeSteeringTask([]));
expect(legacySteer).not.toHaveBeenCalled();
expect(stepSteerActiveSessions).not.toHaveBeenCalled();
expect(workflowSteer).not.toHaveBeenCalled();
expect(store.logEntry).not.toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("Comment received mid-execution"),
expect.anything(),
);
});
it("does not inject steering comments for tasks without an active injection target", async () => {
const store = createMockStore();
new TaskExecutor(store, "/tmp/test");