fix: unblock v0.93.0 release build

This commit is contained in:
coso
2026-03-22 00:34:16 +08:00
parent dc37baff36
commit 7765a3b029
9 changed files with 122 additions and 86 deletions
@@ -2,6 +2,8 @@ import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ConfiguredProvider } from "@/hooks/useConfiguredProviders";
import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry";
import { AgentChatHomeShell } from "./AgentChatHomeShell";
import { SettingsTabs } from "@/types/settings";
@@ -67,8 +69,12 @@ const {
mockSetProviderType: vi.fn(),
mockSetModel: vi.fn(),
mockSetExecutionStrategy: vi.fn(),
mockLoadConfiguredProviders: vi.fn(async () => []),
mockLoadProviderModels: vi.fn(async () => []),
mockLoadConfiguredProviders: vi.fn(
async (): Promise<ConfiguredProvider[]> => [],
),
mockLoadProviderModels: vi.fn(
async (): Promise<EnhancedModelMetadata[]> => [],
),
mockFilterModelsByTheme: vi.fn(
(_theme: string | undefined, models: unknown[]) => ({
models,
@@ -5398,8 +5398,8 @@ export function AgentChatWorkspace({
dismissThemeWorkbenchEntryPrompt,
} = useThemeWorkbenchEntryPrompt({
activeTheme,
contentId,
sessionId,
contentId: contentId ?? undefined,
sessionId: sessionId ?? undefined,
isThemeWorkbench,
shouldUseCompactThemeWorkbench,
messagesCount: messages.length,
@@ -40,19 +40,23 @@ function renderHook(props?: Partial<HookProps>) {
selectedTeam: createSelectedTeam(),
subagentEnabled: true,
hasRealTeamGraph: false,
generateRuntimeTeam: vi.fn(async () => ({
id: "ephemeral-1",
source: "ephemeral",
label: "临时 Team",
description: "自动生成",
roles: [
{
id: "member-1",
label: "执行者",
summary: "执行当前任务",
},
],
})),
generateRuntimeTeam: vi.fn(
async (
_options: Parameters<NonNullable<HookProps["generateRuntimeTeam"]>>[0],
): Promise<TeamDefinition> => ({
id: "ephemeral-1",
source: "ephemeral",
label: "临时 Team",
description: "自动生成",
roles: [
{
id: "member-1",
label: "执行者",
summary: "执行当前任务",
},
],
}),
),
createRequestId: () => "request-1",
now: () => 1_710_000_000_000,
};
@@ -140,7 +140,7 @@ describe("useSelectedTeamPreference", () => {
agentTeam: {
selectedTeam: {
id: engineeringTeam.id,
source: engineeringTeam.source,
source: "builtin",
},
},
};
@@ -228,7 +228,7 @@ describe("useSelectedTeamPreference", () => {
agentTeam: {
selectedTeam: {
id: engineeringTeam.id,
source: engineeringTeam.source,
source: "builtin",
},
},
},
@@ -1,6 +1,8 @@
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { WorkflowState as ContentWorkflowState } from "@/lib/api/content-workflow";
import type { ThemeWorkbenchRunState as BackendThemeWorkbenchRunState } from "@/lib/api/executionRun";
import { useThemeWorkbenchEntryPrompt } from "./useThemeWorkbenchEntryPrompt";
interface HookHarness {
@@ -29,8 +31,13 @@ function mountHook(initialProps?: Partial<HookProps>): HookHarness {
const root = createRoot(container);
const onHydrateInitialPrompt = vi.fn();
const loadWorkflow = vi.fn(async () => null);
const loadRunState = vi.fn(async () => null);
const loadWorkflow = vi.fn(
async (_contentId: string): Promise<ContentWorkflowState | null> => null,
);
const loadRunState = vi.fn(
async (_sessionId: string): Promise<BackendThemeWorkbenchRunState | null> =>
null,
);
let hookValue: ReturnType<typeof useThemeWorkbenchEntryPrompt> | null = null;
let currentProps: HookProps = {
@@ -135,32 +142,51 @@ describe("useThemeWorkbenchEntryPrompt", () => {
document.body.appendChild(container);
const root = createRoot(container);
const onHydrateInitialPrompt = vi.fn();
const loadWorkflow = vi.fn(async () => ({
id: "wf-1",
content_id: "content-1",
theme: "social-media",
mode: "guided",
current_step_index: 1,
created_at: Date.now(),
updated_at: Date.now(),
steps: [
{
id: "step-1",
title: "撰写主稿",
status: "completed",
},
{
id: "step-2",
title: "润色结尾",
status: "pending",
},
],
}));
const loadRunState = vi.fn(async () => null);
let hookValue: ReturnType<typeof useThemeWorkbenchEntryPrompt> | null = null;
const loadWorkflow = vi.fn(
async (_contentId: string): Promise<ContentWorkflowState | null> => ({
id: "wf-1",
content_id: "content-1",
theme: "social-media",
mode: "guided",
current_step_index: 1,
created_at: Date.now(),
updated_at: Date.now(),
steps: [
{
id: "step-1",
type: "write",
title: "撰写主稿",
behavior: {
skippable: false,
redoable: true,
auto_advance: false,
},
status: "completed",
},
{
id: "step-2",
type: "polish",
title: "润色结尾",
behavior: {
skippable: false,
redoable: true,
auto_advance: false,
},
status: "pending",
},
],
}),
);
const loadRunState = vi.fn(
async (_sessionId: string): Promise<BackendThemeWorkbenchRunState | null> =>
null,
);
const hookValueRef: {
current: ReturnType<typeof useThemeWorkbenchEntryPrompt> | null;
} = { current: null };
function TestComponent() {
hookValue = useThemeWorkbenchEntryPrompt({
hookValueRef.current = useThemeWorkbenchEntryPrompt({
activeTheme: "social-media",
contentId: "content-1",
sessionId: "session-1",
@@ -185,11 +211,11 @@ describe("useThemeWorkbenchEntryPrompt", () => {
try {
await flushEffects();
expect(loadWorkflow).toHaveBeenCalledWith("content-1");
expect(hookValue?.themeWorkbenchEntryPrompt).toMatchObject({
expect(hookValueRef.current?.themeWorkbenchEntryPrompt).toMatchObject({
kind: "resume",
title: "发现上次未完成任务",
});
expect(hookValue?.themeWorkbenchEntryCheckPending).toBe(false);
expect(hookValueRef.current?.themeWorkbenchEntryCheckPending).toBe(false);
} finally {
act(() => {
root.unmount();
@@ -54,7 +54,6 @@ function resolveThemeWorkbenchGateLabel(
return "写作推进";
case "publish_confirm":
return "发布确认";
case "idle":
case null:
case undefined:
default:
+23 -33
View File
@@ -640,6 +640,21 @@ interface MountedHarness {
rerender: (props?: Partial<ComponentProps<typeof AgentChatPage>>) => void;
}
type MockInputbarSendProps = {
onToolStatesChange?: (
next:
| Record<string, unknown>
| ((prev: Record<string, unknown>) => Record<string, unknown>),
) => void;
onSend?: (
images?: unknown[],
webSearch?: boolean,
thinking?: boolean,
textOverride?: string,
executionStrategy?: "react" | "code_orchestrated" | "auto",
) => void | Promise<void> | Promise<boolean | void> | boolean;
};
const mountedRoots: MountedHarness[] = [];
const observedWorkspaceIds: string[] = [];
let sharedSwitchTopicMock: ReturnType<typeof vi.fn>;
@@ -1582,13 +1597,7 @@ describe("AgentChatPage 通用工作台", () => {
await flushEffects(10);
let latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as
| {
onToolStatesChange?: (
next:
| Record<string, unknown>
| ((prev: Record<string, unknown>) => Record<string, unknown>),
) => void;
}
| MockInputbarSendProps
| undefined;
act(() => {
@@ -1600,15 +1609,7 @@ describe("AgentChatPage 通用工作台", () => {
await flushEffects(8);
latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as
| {
onSend?: (
images?: unknown[],
webSearch?: boolean,
thinking?: boolean,
textOverride?: string,
executionStrategy?: "react" | "code_orchestrated" | "auto",
) => Promise<void>;
}
| MockInputbarSendProps
| undefined;
await act(async () => {
@@ -1720,13 +1721,7 @@ describe("AgentChatPage 通用工作台", () => {
await flushEffects(10);
let latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as
| {
onToolStatesChange?: (
next:
| Record<string, unknown>
| ((prev: Record<string, unknown>) => Record<string, unknown>),
) => void;
}
| MockInputbarSendProps
| undefined;
act(() => {
@@ -1738,15 +1733,7 @@ describe("AgentChatPage 通用工作台", () => {
await flushEffects(8);
latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as
| {
onSend?: (
images?: unknown[],
webSearch?: boolean,
thinking?: boolean,
textOverride?: string,
executionStrategy?: "react" | "code_orchestrated" | "auto",
) => Promise<void>;
}
| MockInputbarSendProps
| undefined;
await act(async () => {
@@ -3156,9 +3143,12 @@ describe("AgentChatPage 自动引导", () => {
const mounted = mountedRoots.at(-1);
expect(mounted).toBeTruthy();
if (!mounted) {
throw new Error("未找到挂载页面");
}
act(() => {
mounted?.root.render(
mounted.root.render(
<AgentChatPage
projectId="project-theme-delayed-intent"
contentId="content-theme-delayed-intent"
@@ -118,7 +118,7 @@ export function sanitizeContentPartsForDisplay(
return parts;
}
const sanitizedParts = parts.flatMap((part) => {
const sanitizedParts = parts.flatMap<ContentPart>((part) => {
if (part.type !== "text") {
return [part];
}
@@ -13,6 +13,7 @@ import {
createTeamDefinitionId,
normalizeTeamDefinition,
type TeamDefinition,
type TeamRoleDefinition,
} from "./teamDefinitions";
interface GenerateTeamWithModelOptions {
@@ -136,13 +137,23 @@ function parseGeneratedTeam(
const json = extractJsonObject(raw);
const parsed = JSON.parse(json) as GeneratedTeamPayload;
const payload = parsed.team && typeof parsed.team === "object" ? parsed.team : parsed;
const normalizedRoles: TeamRoleDefinition[] | undefined = payload.roles?.map(
(role) => ({
id: role.id?.trim() || "",
label: role.label?.trim() || "",
summary: role.summary?.trim() || "",
profileId: role.profileId?.trim() || undefined,
roleKey: role.roleKey?.trim() || undefined,
skillIds: role.skillIds?.map((skillId) => skillId.trim()).filter(Boolean) || [],
}),
);
const normalized = normalizeTeamDefinition({
id: createTeamDefinitionId("ephemeral-team"),
source: "ephemeral",
label: payload.label,
description: payload.description,
theme: activeTheme?.trim() || undefined,
roles: payload.roles,
roles: normalizedRoles,
});
if (!normalized) {
@@ -188,7 +199,7 @@ export async function generateEphemeralTeamWithModel(
resolvedExecutionStrategy,
);
const eventName = `agent_team_draft:${sessionId}:${Date.now()}`;
let unlisten: (() => void) | null = null;
const unlistenRef: { current: (() => void) | null } = { current: null };
try {
const completion = new Promise<TeamDefinition>((resolve, reject) => {
@@ -207,7 +218,7 @@ export async function generateEphemeralTeamWithModel(
void (async () => {
try {
unlisten = await safeListen(eventName, async (event) => {
unlistenRef.current = await safeListen(eventName, async (event) => {
const parsed = parseStreamEvent(event.payload);
if (!parsed) {
return;
@@ -292,7 +303,7 @@ export async function generateEphemeralTeamWithModel(
return await completion;
} finally {
try {
unlisten?.();
unlistenRef.current?.();
} catch {
// ignore cleanup failure
}