Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/model/AgentGUINodeViewModel.renderBudget.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

196 lines
5.9 KiB
TypeScript

import { renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import type { AgentGUINodeViewModel } from "./agentGuiNodeTypes";
import { useAgentGUIViewModel } from "./useAgentGUIViewModel";
import { buildAgentComposerDraft } from "./agentComposerDraft";
describe("useAgentGUIViewModel render budgets", () => {
it("keeps composer and rail references stable for a streaming detail update", () => {
assertIsolatedGroupUpdate("detail", ["composer", "rail"]);
});
it("keeps detail and composer references stable for a rail interaction", () => {
assertIsolatedGroupUpdate("rail", ["composer", "detail"]);
});
it("keeps rail and detail references stable while typing in the composer", () => {
assertIsolatedGroupUpdate("composer", ["detail", "rail"]);
});
it("publishes the connecting-to-ready Composer gate as one snapshot", () => {
const initial = createViewModel();
const connectingGate: AgentGUINodeViewModel["composer"]["gate"] = {
conversationBusy: false,
isAwaitingTurnStart: false,
runtime: {
status: "blocked",
reason: "target_connection",
sessionRuntimeReason: null
},
editor: { status: "blocked", reason: "runtime_blocked" },
submission: { status: "blocked", reason: "runtime_blocked" }
};
const readyGate: AgentGUINodeViewModel["composer"]["gate"] = {
conversationBusy: false,
isAwaitingTurnStart: false,
runtime: {
status: "ready",
reason: null,
sessionRuntimeReason: null
},
editor: { status: "editable", reason: null },
submission: { status: "ready", reason: null }
};
const rendered = renderHook(
({ candidate }) => useAgentGUIViewModel(candidate),
{
initialProps: {
candidate: {
...initial,
composer: { ...initial.composer, gate: connectingGate }
}
}
}
);
rendered.rerender({
candidate: {
...initial,
composer: { ...initial.composer, gate: readyGate }
}
});
expect(rendered.result.current.composer.gate).toBe(readyGate);
expect(rendered.result.current.composer.gate).toMatchObject({
runtime: { status: "ready" },
editor: { status: "editable" },
submission: { status: "ready" }
});
});
});
function assertIsolatedGroupUpdate(
group: "composer" | "detail" | "rail",
stableGroups: readonly ("composer" | "detail" | "rail")[]
): void {
const initial = createViewModel();
const rendered = renderHook(
({ candidate }) => useAgentGUIViewModel(candidate),
{ initialProps: { candidate: initial } }
);
const previous = rendered.result.current;
rendered.rerender({
candidate: {
...initial,
[group]: changedGroup(group, initial[group])
}
});
expect(rendered.result.current[group]).not.toBe(previous[group]);
for (const stableGroup of stableGroups) {
expect(rendered.result.current[stableGroup]).toBe(previous[stableGroup]);
}
}
function changedGroup<Group extends "composer" | "detail" | "rail">(
group: Group,
current: AgentGUINodeViewModel[Group]
): AgentGUINodeViewModel[Group] {
if (group === "detail") {
return {
...current,
hasSentUserMessage: !(current as AgentGUINodeViewModel["detail"])
.hasSentUserMessage
} as AgentGUINodeViewModel[Group];
}
if (group === "rail") {
return {
...current,
isLoadingConversations: !(current as AgentGUINodeViewModel["rail"])
.isLoadingConversations
} as AgentGUINodeViewModel[Group];
}
return {
...current,
draftPrompt: `${(current as AgentGUINodeViewModel["composer"]).draftPrompt}x`
} as AgentGUINodeViewModel[Group];
}
function createViewModel(): AgentGUINodeViewModel {
return {
shell: {
workspaceId: "workspace-1",
workspacePath: "/workspace",
currentUserId: "user-1",
data: {} as AgentGUINodeViewModel["shell"]["data"]
},
rail: {
selectedAgentTarget:
{} as AgentGUINodeViewModel["rail"]["selectedAgentTarget"],
agentTargets: [],
agentTargetsLoading: false,
providerRailMode: "catalog",
comingSoonProviders: [],
conversationFilter: { kind: "all" },
conversations: [],
userProjects: [],
activeConversation: null,
activeConversationId: null,
revealRequest: null,
isLoadingConversations: false,
listError: null
},
detail: {
availability: "ready",
isLoadingMessages: false,
isLoadingOlderMessages: false,
hasOlderMessages: false,
usage: null,
hasSentUserMessage: false,
avoidGroupingEdits: false,
conversation: null,
conversationDetail: null
},
composer: {
handoffAgentTargets: [],
availableCommands: [],
availableSkills: [],
draftPrompt: "",
draftContent: buildAgentComposerDraft({ prompt: "" }),
isCreatingConversation: false,
isSubmitting: false,
isInterrupting: false,
isCancelPending: false,
hasPendingSubmitStopTarget: false,
promptImagesSupported: false,
compactSupported: false,
goalPauseSupported: false,
gate: {
conversationBusy: false,
isAwaitingTurnStart: false,
runtime: {
status: "ready",
reason: null,
sessionRuntimeReason: null
},
editor: { status: "editable", reason: null },
submission: { status: "ready", reason: null }
},
isTuttiModeActive: false,
isTuttiModeUpdating: false,
tuttiModeEffect: 50,
tuttiModeSpeed: 50,
tuttiModeUpdateStatus: "idle",
composerSettings:
{} as AgentGUINodeViewModel["composer"]["composerSettings"],
queuedPrompts: [],
queueStatus: "active",
drainingQueuedPromptId: null
},
interaction: {} as AgentGUINodeViewModel["interaction"],
readiness: {} as AgentGUINodeViewModel["readiness"],
operations: {} as AgentGUINodeViewModel["operations"]
};
}