Files
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

347 lines
10 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import type { AgentSessionEngine } from "@tutti-os/agent-activity-core";
import { describe, expect, it, vi } from "vitest";
import type { AgentGUINodeData, AgentGUIAgentTarget } from "../../../types";
import type { AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import { useAgentGUIProviderHome } from "./useAgentGUIProviderHome";
describe("useAgentGUIProviderHome", () => {
it("syncs a scoped rail filter to an externally changed home composer target", () => {
const codexTarget = target("codex", "agent:codex", "provider-target:codex");
const claudeTarget = target(
"claude-code",
"agent:claude-code",
"provider-target:claude-code"
);
const fixture = renderProviderHome({
activeConversationId: null,
conversationFilter: {
kind: "agentTarget",
agentTargetId: "agent:codex"
},
conversations: [],
data: {
agentTargetId: "agent:claude-code",
lastActiveAgentSessionId: null,
provider: "claude-code"
},
selectedTarget: claudeTarget,
targets: [codexTarget, claudeTarget]
});
expect(fixture.setConversationFilter).toHaveBeenCalledWith({
kind: "agentTarget",
agentTargetId: "agent:claude-code"
});
});
it("does not narrow the all-agents rail filter when the home composer target changes", () => {
const claudeTarget = target(
"claude-code",
"agent:claude-code",
"provider-target:claude-code"
);
const fixture = renderProviderHome({
activeConversationId: null,
conversationFilter: { kind: "all" },
conversations: [],
data: {
agentTargetId: "agent:claude-code",
lastActiveAgentSessionId: null,
provider: "claude-code"
},
selectedTarget: claudeTarget,
targets: [claudeTarget]
});
expect(fixture.setConversationFilter).not.toHaveBeenCalled();
});
it("does not change the rail filter for an open conversation", () => {
const codexTarget = target("codex", "agent:codex", "provider-target:codex");
const claudeTarget = target(
"claude-code",
"agent:claude-code",
"provider-target:claude-code"
);
const fixture = renderProviderHome({
activeConversationId: "codex-session",
conversationFilter: {
kind: "agentTarget",
agentTargetId: "agent:codex"
},
conversations: [conversation("codex-session", "codex", "agent:codex")],
data: {
agentTargetId: "agent:claude-code",
lastActiveAgentSessionId: null,
provider: "claude-code"
},
selectedTarget: claudeTarget,
targets: [codexTarget, claudeTarget]
});
expect(fixture.setConversationFilter).not.toHaveBeenCalled();
});
it("does not change the rail filter while the home composer target is unresolved", () => {
const loadingTarget: AgentGUIAgentTarget = {
disabled: true,
label: "Loading",
provider: "claude-code",
ref: { kind: "loading", provider: "claude-code" },
targetId: "__loading__"
};
const fixture = renderProviderHome({
activeConversationId: null,
conversationFilter: {
kind: "agentTarget",
agentTargetId: "agent:codex"
},
conversations: [],
data: {
agentTargetId: "agent:claude-code",
lastActiveAgentSessionId: null,
provider: "claude-code"
},
selectedTarget: loadingTarget,
targets: []
});
expect(fixture.setConversationFilter).not.toHaveBeenCalled();
});
it("restores the selected target's last session in the current node", () => {
const codexTarget = target("codex", "agent:codex", "provider-target:codex");
const claudeTarget = target(
"claude-code",
"agent:claude-code",
"provider-target:claude-code"
);
const conversations = [
conversation("codex-session", "codex", "agent:codex"),
conversation("claude-session", "claude-code", "agent:claude-code")
];
const fixture = renderProviderHome({
activeConversationId: "codex-session",
conversations,
data: {
agentTargetId: "agent:codex",
lastActiveAgentSessionId: "codex-session",
lastActiveAgentSessionIdByAgentTargetId: {
"agent:claude-code": "claude-session",
"agent:codex": "codex-session"
},
provider: "codex"
},
selectedTarget: codexTarget,
targets: [codexTarget, claudeTarget]
});
act(() =>
fixture.result.current.selectConversationFilterTarget({
agentTargetId: " agent:claude-code ",
provider: "claude-code"
})
);
expect(fixture.unactivate).toHaveBeenCalledWith("codex-session");
expect(fixture.setConversationFilter).toHaveBeenCalledWith({
kind: "agentTarget",
agentTargetId: "agent:claude-code"
});
expect(fixture.selectConversation).toHaveBeenCalledWith("claude-session", {
reloadConversations: false
});
expect(fixture.getData().lastActiveAgentSessionIdByAgentTargetId).toEqual({
"agent:claude-code": "claude-session",
"agent:codex": "codex-session"
});
const filterCallCount = fixture.setConversationFilter.mock.calls.length;
const selectionCallCount = fixture.selectConversation.mock.calls.length;
const unactivateCallCount = fixture.unactivate.mock.calls.length;
act(() =>
fixture.result.current.selectConversationFilterTarget({
agentTargetId: " ",
provider: "claude-code"
})
);
expect(fixture.setConversationFilter).toHaveBeenCalledTimes(
filterCallCount
);
expect(fixture.selectConversation).toHaveBeenCalledTimes(
selectionCallCount
);
expect(fixture.unactivate).toHaveBeenCalledTimes(unactivateCallCount);
});
it("keeps a restored active session when the bounded rail page is empty", () => {
const codexTarget = target("codex", "agent:codex", "provider-target:codex");
const fixture = renderProviderHome({
activeConversationId: "persisted-session",
conversations: [],
data: {
agentTargetId: "agent:codex",
lastActiveAgentSessionId: "persisted-session",
provider: "codex"
},
selectedTarget: codexTarget,
targets: [codexTarget]
});
expect(fixture.unactivate).not.toHaveBeenCalled();
expect(fixture.setActiveConversationId).not.toHaveBeenCalled();
expect(fixture.persistActiveConversation).not.toHaveBeenCalled();
expect(fixture.onDataChange).not.toHaveBeenCalled();
expect(fixture.getData().lastActiveAgentSessionId).toBe(
"persisted-session"
);
});
});
function renderProviderHome(input: {
activeConversationId: string | null;
conversationFilter?:
| {
kind: "all";
}
| {
kind: "agentTarget";
agentTargetId: string;
};
conversations: readonly AgentGUIConversationSummary[];
data: AgentGUINodeData;
selectedTarget: AgentGUIAgentTarget;
targets: readonly AgentGUIAgentTarget[];
}) {
let data = input.data;
const dataRef = { current: data };
const activeConversationIdRef = {
current: input.activeConversationId
};
const selectConversation = vi.fn();
const unactivate = vi.fn().mockResolvedValue(undefined);
const setConversationFilter = vi.fn();
const setActiveConversationId = vi.fn();
const persistActiveConversation = vi.fn();
const onDataChange = vi.fn(
(updater: (current: AgentGUINodeData) => AgentGUINodeData) => {
data = updater(data);
dataRef.current = data;
}
);
const agentTargetId = input.selectedTarget.agentTargetId!;
const conversationFilter = input.conversationFilter ?? {
kind: "agentTarget" as const,
agentTargetId
};
const { result } = renderHook(() =>
useAgentGUIProviderHome({
activeConversationId: activeConversationIdRef.current,
activeConversationIdRef,
activePendingActivation: null,
agentActivityRuntime: {} as never,
clearRailRevealRequest: vi.fn(),
conversationFilter,
conversationFilterRef: { current: conversationFilter },
conversationsRef: { current: input.conversations },
data,
dataRef,
defaultAgentTargetId: agentTargetId,
effectiveSelectedProviderTarget: input.selectedTarget,
firstReadyHomeComposerProviderTarget: input.selectedTarget,
homeComposerTargetOverride: null,
isComposerHomeRef: { current: false },
isLoadingConversations: false,
normalizedExplicitProviderTargets: input.targets,
normalizedProviderTargets: input.targets,
onDataChangeRef: { current: onDataChange },
persistActiveConversation,
providerReadinessGates: null,
selectedComposerTargetDataRef: {
current: {
agentTargetId,
data,
provider: input.selectedTarget.provider,
targetId: input.selectedTarget.targetId
}
},
selectConversation,
sessionEngine: engine(input.conversations),
setActiveConversationId,
setConversationFilter,
setDetailError: vi.fn(),
setHomeComposerTargetOverride: vi.fn(),
setIntent: vi.fn(),
setIsComposerHome: vi.fn(),
setIsLoadingMessages: vi.fn(),
shouldUseStaticProviderTargets: false,
transientConversation: null,
unactivate,
workspaceId: "workspace-1"
})
);
return {
getData: () => data,
onDataChange,
persistActiveConversation,
result,
selectConversation,
setActiveConversationId,
setConversationFilter,
unactivate
};
}
function target(
provider: string,
agentTargetId: string,
targetId: string
): AgentGUIAgentTarget {
return {
agentTargetId,
label: agentTargetId,
provider,
ref: { kind: "local", provider },
targetId
};
}
function conversation(
id: string,
provider: string,
agentTargetId: string
): AgentGUIConversationSummary {
return {
agentTargetId,
cwd: "/repo",
id,
provider,
status: "completed",
title: id,
updatedAtUnixMs: 1
};
}
function engine(
conversations: readonly AgentGUIConversationSummary[]
): AgentSessionEngine {
return {
getSnapshot: () => ({
sessionLifecycle: {
deletedSessionIds: {},
sessionsById: Object.fromEntries(
conversations.map((item) => [
item.id,
{
agentSessionId: item.id,
agentTargetId: item.agentTargetId
}
])
)
}
})
} as unknown as AgentSessionEngine;
}