Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 162 additions & 26 deletions apps/aevatar-console-web/src/pages/studio/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3201,14 +3201,57 @@ describe("StudioPage", () => {
});
});

it("strips legacy label params while preserving stable scope and member ids", async () => {
it("canonicalizes legacy service member params to real member ids", async () => {
mockStudioMembers = [
...mockStudioMembers,
{
memberId: "member-alpha",
scopeId: "scope-a",
displayName: "Member Alpha",
description: "Legacy service-backed member",
implementationKind: "workflow",
lifecycleStage: "bind_ready",
publishedServiceId: "service-alpha",
lastBoundRevisionId: "rev-alpha",
createdAt: "2026-04-27T08:00:00Z",
updatedAt: "2026-04-27T08:05:00Z",
},
];
mockScopeRuntimeApi.listServices.mockResolvedValue([
{
serviceId: "service-alpha",
displayName: "Member Alpha",
deploymentStatus: "Active",
primaryActorId: "actor-alpha",
endpoints: [
{
endpointId: "chat",
displayName: "Chat",
kind: "chat",
description: "Chat with Alpha.",
requestTypeUrl: "",
responseTypeUrl: "",
},
],
},
]);
mockScopeRuntimeApi.getServiceRevisions.mockImplementation(
async () =>
mockBuildServiceRevisionCatalog({
serviceId: "service-alpha",
displayName: "Member Alpha",
workflowName: "workspace-demo",
})
);

renderStudioPage(
"/studio?scopeId=scope-a&scopeLabel=%E5%9B%A2%E9%98%9F+A&memberId=service-alpha&memberLabel=%E6%88%90%E5%91%98+Alpha&focus=workflow%3Aworkflow-1&tab=studio"
);

expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy();
await waitFor(() => {
expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-alpha");
expect(window.location.search).toContain("member=member%3Amember-alpha");
});
expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("团队 A");
expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("成员 Alpha");
Expand All @@ -3220,31 +3263,105 @@ describe("StudioPage", () => {

const searchParams = new URLSearchParams(window.location.search);
expect(searchParams.get("scopeId")).toBe("scope-a");
expect(searchParams.get("member")).toBe("member:service-alpha");
expect(searchParams.get("member")).toBe("member:member-alpha");
expect(searchParams.get("memberId")).toBeNull();
expect(searchParams.get("scopeLabel")).toBeNull();
expect(searchParams.get("memberLabel")).toBeNull();
expect(searchParams.get("focus")).toBe("workflow:workflow-1");
expect(searchParams.get("tab")).toBe("studio");
expect(studioApi.getMember).toHaveBeenCalledWith("scope-a", "member-alpha");
expect(studioApi.getMember).not.toHaveBeenCalledWith("scope-a", "service-alpha");
});

it("resyncs the Studio state from stable scope and member ids when the route changes after mount", async () => {
it("keeps direct member route keys as canonical member ids", async () => {
renderStudioPage(
"/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"
);

expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy();
await waitFor(() => {
expect(window.location.search).toContain("member=member%3Aworkspace-demo");
});

const searchParams = new URLSearchParams(window.location.search);
expect(searchParams.get("member")).toBe("member:workspace-demo");
expect(searchParams.get("memberId")).toBeNull();
expect(studioApi.getMember).toHaveBeenCalledWith("scope-1", "workspace-demo");
});

it("resyncs the Studio state from legacy service params when the route changes after mount", async () => {
mockStudioMembers = [
...mockStudioMembers,
{
memberId: "member-alpha",
scopeId: "scope-a",
displayName: "Member Alpha",
description: "Legacy service-backed member",
implementationKind: "workflow",
lifecycleStage: "bind_ready",
publishedServiceId: "service-alpha",
lastBoundRevisionId: "rev-alpha",
createdAt: "2026-04-27T08:00:00Z",
updatedAt: "2026-04-27T08:05:00Z",
},
{
memberId: "member-beta",
scopeId: "scope-b",
displayName: "Member Beta",
description: "Legacy service-backed member",
implementationKind: "workflow",
lifecycleStage: "bind_ready",
publishedServiceId: "service-beta",
lastBoundRevisionId: "rev-beta",
createdAt: "2026-04-27T08:00:00Z",
updatedAt: "2026-04-27T08:05:00Z",
},
];
mockScopeRuntimeApi.listServices.mockImplementation(async (scopeId: string) => [
{
serviceId: scopeId === "scope-b" ? "service-beta" : "service-alpha",
displayName: scopeId === "scope-b" ? "Member Beta" : "Member Alpha",
deploymentStatus: "Active",
primaryActorId: scopeId === "scope-b" ? "actor-beta" : "actor-alpha",
endpoints: [
{
endpointId: "chat",
displayName: "Chat",
kind: "chat",
description: "Chat with the member.",
requestTypeUrl: "",
responseTypeUrl: "",
},
],
},
]);
mockScopeRuntimeApi.getServiceRevisions.mockImplementation(
async (_scopeId: string, serviceId: string) =>
mockBuildServiceRevisionCatalog({
serviceId,
displayName: serviceId === "service-beta" ? "Member Beta" : "Member Alpha",
workflowName: "workspace-demo",
})
);

renderStudioPage(
"/studio?scopeId=scope-a&scopeLabel=%E5%9B%A2%E9%98%9F+A&memberId=service-alpha&memberLabel=%E6%88%90%E5%91%98+Alpha&focus=workflow%3Aworkflow-1&tab=studio"
);

expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy();
expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-alpha");
await waitFor(() => {
expect(window.location.search).toContain("member=member%3Amember-alpha");
});

await replaceStudioRoute(
"/studio?scopeId=scope-b&scopeLabel=%E5%9B%A2%E9%98%9F+B&memberId=service-beta&memberLabel=%E6%88%90%E5%91%98+Beta&tab=workflows"
);

expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy();
expect(screen.getByTestId("studio-context-title")).toHaveTextContent(
"workspace-demo"
);
expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-beta");
await waitFor(() => {
expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-beta");
expect(window.location.search).toContain("member=member%3Amember-beta");
});
expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("团队 B");
expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("成员 Beta");
expect(screen.getByTestId("studio-workflow-build-panel")).toBeTruthy();
Expand All @@ -3260,12 +3377,14 @@ describe("StudioPage", () => {

const searchParams = new URLSearchParams(window.location.search);
expect(searchParams.get("scopeId")).toBe("scope-b");
expect(searchParams.get("member")).toBe("member:service-beta");
expect(searchParams.get("member")).toBe("member:member-beta");
expect(searchParams.get("memberId")).toBeNull();
expect(searchParams.get("scopeLabel")).toBeNull();
expect(searchParams.get("memberLabel")).toBeNull();
expect(searchParams.get("focus")).toBe("workflow:workflow-1");
expect(searchParams.get("tab")).toBe("studio");
expect(studioApi.getMember).toHaveBeenCalledWith("scope-b", "member-beta");
expect(studioApi.getMember).not.toHaveBeenCalledWith("scope-b", "service-beta");
});

it("ignores removed create-team route params and falls back to the explicit member-selection empty state", async () => {
Expand Down Expand Up @@ -3659,7 +3778,7 @@ describe("StudioPage", () => {
});

it("returns to canonical Team detail when Studio has Team context", async () => {
renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

fireEvent.click(await screen.findByRole("button", { name: "返回团队" }));

Expand Down Expand Up @@ -4031,7 +4150,7 @@ describe("StudioPage", () => {
});

it("carries the selected bind contract into invoke after continuing from build", async () => {
renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy();

Expand Down Expand Up @@ -4115,6 +4234,21 @@ describe("StudioPage", () => {
});

it("shows an invoke empty state when a bound member has no endpoint data", async () => {
mockStudioMembers = [
...mockStudioMembers,
{
memberId: "script-member",
scopeId: "scope-1",
displayName: "script-alpha",
description: "Script member with no endpoints",
implementationKind: "script",
lifecycleStage: "bind_ready",
publishedServiceId: "script-alpha",
lastBoundRevisionId: "rev-script-alpha",
createdAt: "2026-04-27T08:00:00Z",
updatedAt: "2026-04-27T08:05:00Z",
},
];
mockScopeRuntimeApi.listServices.mockResolvedValueOnce([
{
serviceId: "script-alpha",
Expand All @@ -4126,15 +4260,17 @@ describe("StudioPage", () => {
]);

renderStudioPage(
"/studio?scopeId=scope-1&memberId=script-alpha&step=invoke&tab=invoke"
"/studio?scopeId=scope-1&member=member%3Ascript-member&step=invoke&tab=invoke"
);

expect(await screen.findByTestId("studio-invoke-surface")).toBeTruthy();
expect(screen.getByText("service:script-alpha")).toBeTruthy();
expect(screen.getByText("member:script-alpha")).toBeTruthy();
expect(screen.getByText("services:none")).toBeTruthy();
expect(screen.getByText("endpoint:no-endpoint")).toBeTruthy();
expect(screen.getByText(/empty:script-alpha 还不能直接调用。/)).toBeTruthy();
await waitFor(() => {
expect(screen.getByText("service:script-alpha")).toBeTruthy();
expect(screen.getByText("member:script-alpha")).toBeTruthy();
expect(screen.getByText("services:none")).toBeTruthy();
expect(screen.getByText("endpoint:no-endpoint")).toBeTruthy();
expect(screen.getByText(/empty:script-alpha 还不能直接调用。/)).toBeTruthy();
});
});

it("surfaces the current workflow as a bind candidate before any published service exists", async () => {
Expand All @@ -4161,7 +4297,7 @@ describe("StudioPage", () => {
]);
(studioApi.getScopeBinding as jest.Mock).mockResolvedValueOnce(null);

renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy();

Expand Down Expand Up @@ -4235,7 +4371,7 @@ describe("StudioPage", () => {
updatedAt: "2026-04-27T08:15:01Z",
});

renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy();

Expand Down Expand Up @@ -4522,7 +4658,7 @@ describe("StudioPage", () => {
);

renderStudioPage(
"/studio?scopeId=scope-1&memberId=draft1&focus=workflow%3Aworkflow-1&step=bind&tab=bindings"
"/studio?scopeId=scope-1&member=member%3Adraft1&focus=workflow%3Aworkflow-1&step=bind&tab=bindings"
);

expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy();
Expand Down Expand Up @@ -4645,7 +4781,7 @@ describe("StudioPage", () => {
);

renderStudioPage(
"/studio?scopeId=scope-1&memberId=joker&focus=workflow%3Aworkflow-1&tab=studio"
"/studio?scopeId=scope-1&member=member%3Ajoker&focus=workflow%3Aworkflow-1&tab=studio"
);

expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy();
Expand Down Expand Up @@ -4744,7 +4880,7 @@ describe("StudioPage", () => {
})
);

renderStudioPage("/studio?scopeId=scope-1&memberId=joker&step=bind&tab=bindings");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Ajoker&step=bind&tab=bindings");

expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy();
await waitFor(() => {
Expand Down Expand Up @@ -5004,7 +5140,7 @@ describe("StudioPage", () => {
})
);

renderStudioPage("/studio?scopeId=scope-1&memberId=joker&step=bind&tab=bindings");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Ajoker&step=bind&tab=bindings");

expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy();
await waitFor(() => {
Expand Down Expand Up @@ -5862,7 +5998,7 @@ describe("StudioPage", () => {
);

renderStudioPage(
"/studio?scopeId=scope-1&memberId=script-member&step=bind&tab=bindings"
"/studio?scopeId=scope-1&member=member%3Ascript-member&step=bind&tab=bindings"
);

expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy();
Expand Down Expand Up @@ -6173,7 +6309,7 @@ describe("StudioPage", () => {
});

it("opens the Studio invoke surface from the bind surface endpoint action", async () => {
renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

fireEvent.click(await screen.findByRole("button", { name: "Bind" }));
await waitFor(() => {
Expand Down Expand Up @@ -6330,7 +6466,7 @@ describe("StudioPage", () => {
});

it("walks the lifecycle flow from build to bind to invoke to observe", async () => {
renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio");
renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio");

expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy();

Expand Down
Loading
Loading