Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationRailQuery.activity.spec.tsx
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

466 lines
13 KiB
TypeScript

import {
normalizeAgentActivitySession,
type AgentActivityTurn
} from "@tutti-os/agent-activity-core";
import { act, renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
AgentGUIRuntimeProvider,
type AgentGUIRuntime
} from "../../../agentActivityRuntime";
import { createTestAgentSessionEngine } from "../../../shared/testing/createTestAgentSessionEngine";
import { useAgentGUIConversationRailQuery } from "./useAgentGUIConversationRailQuery";
describe("useAgentGUIConversationRailQuery Activity facts", () => {
it("keeps child lifecycle out of root status while preserving descendant attention", async () => {
const engine = createTestAgentSessionEngine("workspace-1");
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "root",
createdAtUnixMs: 1,
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
title: "Root",
updatedAtUnixMs: 1,
workspaceId: "workspace-1"
})
});
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: "child-turn",
agentSessionId: "child",
cwd: "/workspace",
kind: "child",
latestTurnInteractions: [],
parentAgentSessionId: "root",
parentToolCallId: "tool-1",
parentTurnId: "root-turn",
pendingInteractions: [],
provider: "codex",
rootAgentSessionId: "root",
rootTurnId: "root-turn",
title: "Child",
workspaceId: "workspace-1"
})
});
engine.dispatch({
live: true,
type: "turn/upserted",
turn: runningChildTurn()
});
const runtime = {
conversationActivityViewEnabled: true,
getSessionEngine: () => engine
} as unknown as AgentGUIRuntime;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AgentGUIRuntimeProvider runtime={runtime}>
{children}
</AgentGUIRuntimeProvider>
);
const rendered = renderHook(
() =>
useAgentGUIConversationRailQuery({
activeConversationId: null,
conversationFilter: { kind: "all" },
conversationQuery: "",
userProjects: [],
workspaceId: "workspace-1"
}),
{ wrapper }
);
await waitFor(() =>
expect(rendered.result.current.activityRootFacts.get("root")).toEqual({
needsUserAction: false,
status: "ready"
})
);
expect(rendered.result.current.activityRootFacts.has("child")).toBe(false);
expect(
rendered.result.current.activityConversations.map(
(conversation) => conversation.id
)
).toEqual(["root"]);
expect(rendered.result.current.activityConversations[0]).toMatchObject({
id: "root",
needsUserAction: false,
status: "ready"
});
await waitFor(() =>
expect(
rendered.result.current.activityController.getSnapshot().available
).toBe(true)
);
act(() => {
rendered.result.current.activityController.toggle();
});
expect(
rendered.result.current.activityController.getSnapshot().activation
?.priority
).toEqual([]);
const rootFactsBeforeChildLifecycleUpdate =
rendered.result.current.activityRootFacts;
const conversationsBeforeChildLifecycleUpdate =
rendered.result.current.activityConversations;
act(() => {
engine.dispatch({
live: true,
type: "turn/upserted",
turn: runningChildTurn(3)
});
});
expect(rendered.result.current.activityRootFacts).toBe(
rootFactsBeforeChildLifecycleUpdate
);
expect(rendered.result.current.activityConversations).toBe(
conversationsBeforeChildLifecycleUpdate
);
act(() => {
engine.dispatch({
type: "interaction/upserted",
interaction: {
agentSessionId: "child",
createdAtUnixMs: 3,
input: { question: "Approve child action?" },
kind: "approval",
requestId: "approval-1",
status: "pending",
turnId: "child-turn",
updatedAtUnixMs: 3
}
});
});
await waitFor(() =>
expect(rendered.result.current.activityRootFacts.get("root")).toEqual({
needsUserAction: true,
status: "ready"
})
);
expect(rendered.result.current.activityConversations[0]).toMatchObject({
id: "root",
needsUserAction: true,
status: "ready"
});
await waitFor(() =>
expect(
rendered.result.current.activityController.getSnapshot().activation
?.priority
).toMatchObject([
{
id: "root",
priorityReason: "waiting"
}
])
);
act(() => {
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: "root-turn",
agentSessionId: "root",
createdAtUnixMs: 1,
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
title: "Root",
updatedAtUnixMs: 4,
workspaceId: "workspace-1"
})
});
engine.dispatch({
live: true,
type: "turn/upserted",
turn: {
agentSessionId: "root",
origin: "user_prompt",
phase: "running",
startedAtUnixMs: 4,
turnId: "root-turn",
updatedAtUnixMs: 4
}
});
});
await waitFor(() =>
expect(rendered.result.current.activityRootFacts.get("root")).toEqual({
needsUserAction: true,
status: "working"
})
);
rendered.unmount();
engine.dispose();
});
it("projects Activity project identity from railSectionKey instead of cwd", async () => {
const engine = createTestAgentSessionEngine("workspace-1");
const runtime = {
conversationActivityViewEnabled: true,
getSessionEngine: () => engine
} as unknown as AgentGUIRuntime;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AgentGUIRuntimeProvider runtime={runtime}>
{children}
</AgentGUIRuntimeProvider>
);
const rendered = renderHook(
() =>
useAgentGUIConversationRailQuery({
activeConversationId: null,
conversationFilter: { kind: "all" },
conversationQuery: "",
userProjects: [
{
id: "project-1",
label: "Workspace",
path: "/workspace",
pinnedAtUnixMs: 0,
sectionKey: "project:/workspace"
}
],
workspaceId: "workspace-1"
}),
{ wrapper }
);
act(() => {
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "worktree-session",
cwd: "/Users/local/.tutti/agent/worktrees/worktree-session",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
railSectionKey: "project:/workspace",
title: "Worktree session",
workspaceId: "workspace-1"
})
});
});
await waitFor(() =>
expect(rendered.result.current.activityConversations[0]).toEqual(
expect.objectContaining({
id: "worktree-session",
project: expect.objectContaining({
id: "project-1",
sectionKey: "project:/workspace"
})
})
)
);
act(() => {
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "worktree-session",
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
railSectionKey: "conversations",
title: "Worktree session",
workspaceId: "workspace-1"
})
});
});
await waitFor(() =>
expect(
rendered.result.current.activityConversations[0]?.project
).toBeNull()
);
rendered.unmount();
engine.dispose();
});
it("keeps plan-implementation waiting on non-active conversations", async () => {
const engine = createTestAgentSessionEngine("workspace-1");
engine.dispatch({
type: "session/snapshotReceived",
sessions: [
normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "plan-session",
capabilities: {
imageInput: false,
modelImageInputRequired: false,
modelPlanBinding: false,
modelSwitch: false,
skills: false,
compact: false,
tokenUsage: false,
rateLimits: false,
planMode: true,
interrupt: false,
activeTurnGuidance: false,
browserUse: false,
computerUse: false,
goalPause: false,
planImplementation: true,
permissionModeChangeDuringTurn: false,
permissionModeChangeDeferred: false,
review: false,
resumeRunningTurn: false
},
cwd: "/workspace",
latestTurn: {
turnId: "turn-plan",
agentSessionId: "plan-session",
origin: "user_prompt",
phase: "settled",
outcome: "completed",
startedAtUnixMs: 10,
settledAtUnixMs: 20,
updatedAtUnixMs: 20
},
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
title: "Plan session",
workspaceId: "workspace-1"
}),
normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "other-session",
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
title: "Other session",
workspaceId: "workspace-1"
})
]
});
engine.dispatch({
type: "message/snapshotReceived",
messages: [
{
agentSessionId: "plan-session",
kind: "message",
messageId: "plan-message",
occurredAtUnixMs: 20,
payload: { messageKind: "plan" },
role: "agent",
turnId: "turn-plan",
version: 1,
workspaceId: "workspace-1"
}
]
});
const runtime = {
conversationActivityViewEnabled: true,
getSessionEngine: () => engine
} as unknown as AgentGUIRuntime;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AgentGUIRuntimeProvider runtime={runtime}>
{children}
</AgentGUIRuntimeProvider>
);
const rendered = renderHook(
() =>
useAgentGUIConversationRailQuery({
activeConversationId: "other-session",
conversationFilter: { kind: "all" },
conversationQuery: "",
userProjects: [],
workspaceId: "workspace-1"
}),
{ wrapper }
);
await waitFor(() =>
expect(
rendered.result.current.activityRootFacts.get("plan-session")
).toEqual({
needsUserAction: true,
status: "completed"
})
);
expect(
rendered.result.current.activityConversations.find(
(conversation) => conversation.id === "plan-session"
)
).toMatchObject({
id: "plan-session",
needsUserAction: true,
status: "completed"
});
expect(
rendered.result.current.activityRootFacts.get("other-session")
).toEqual({
needsUserAction: false,
status: "ready"
});
rendered.unmount();
engine.dispose();
});
it("returns the shared empty Activity facts when the host capability is disabled", () => {
const engine = createTestAgentSessionEngine("workspace-1");
engine.dispatch({
type: "session/upserted",
session: normalizeAgentActivitySession({
activeTurnId: "root-turn",
agentSessionId: "root",
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
title: "Root",
workspaceId: "workspace-1"
})
});
const runtime = {
conversationActivityViewEnabled: false,
getSessionEngine: () => engine
} as unknown as AgentGUIRuntime;
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AgentGUIRuntimeProvider runtime={runtime}>
{children}
</AgentGUIRuntimeProvider>
);
const rendered = renderHook(
() =>
useAgentGUIConversationRailQuery({
activeConversationId: null,
conversationFilter: { kind: "all" },
conversationQuery: "",
userProjects: [],
workspaceId: "workspace-1"
}),
{ wrapper }
);
expect(rendered.result.current.activityRootFacts).toEqual(new Map());
rendered.unmount();
engine.dispose();
});
});
function runningChildTurn(updatedAtUnixMs = 2): AgentActivityTurn {
return {
agentSessionId: "child",
origin: "provider_initiated",
phase: "running",
startedAtUnixMs: 2,
turnId: "child-turn",
updatedAtUnixMs
};
}