Avatar Customization + Update Agent Brain (#23)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -1,34 +1,23 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import { stubStudioRoute } from "./helpers/studioRoute";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route("**/api/studio", async (route, request) => {
|
||||
if (request.method() === "PUT") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() !== "GET") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
await stubStudioRoute(page, {
|
||||
version: 1,
|
||||
gateway: null,
|
||||
focused: {},
|
||||
avatars: {
|
||||
"ws://localhost:18789": {
|
||||
"agent-1": createDefaultAgentAvatarProfile("seed-1"),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("empty focused view shows zero agents when disconnected", async ({ page }) => {
|
||||
test("structured avatar settings fixture does not break focused load", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("studio-menu-toggle")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Connect" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { stubStudioRoute } from "./helpers/studioRoute";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route("**/api/studio", async (route, request) => {
|
||||
if (request.method() === "PUT") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() !== "GET") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
});
|
||||
await stubStudioRoute(page);
|
||||
});
|
||||
|
||||
test("shows_connection_settings_control_in_header", async ({ page }) => {
|
||||
test("shows_office_header_controls", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("brain-files-toggle")).toHaveCount(0);
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await expect(page.getByTestId("gateway-settings-toggle")).toBeVisible();
|
||||
await expect(page.getByTitle("Voice reply settings")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("mobile_header_shows_connection_control", async ({ page }) => {
|
||||
test("mobile_header_shows_office_controls", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("brain-files-toggle")).toHaveCount(0);
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await expect(page.getByTestId("gateway-settings-toggle")).toBeVisible();
|
||||
await expect(page.getByTitle("Voice reply settings")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -5,13 +5,10 @@ test.beforeEach(async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
});
|
||||
|
||||
test("connection panel reflects disconnected state", async ({ page }) => {
|
||||
test("office settings panel reflects current gateway state", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await page.getByTestId("gateway-settings-toggle").click();
|
||||
await expect(page.getByLabel("Upstream URL")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /^(Connect|Disconnect)$/ })
|
||||
).toBeVisible();
|
||||
await page.getByTitle("Voice reply settings").click();
|
||||
await expect(page.getByRole("button", { name: "Disconnect gateway" })).toBeVisible();
|
||||
await expect(page.getByText("Current studio connection and endpoint details.")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { stubStudioRoute } from "./helpers/studioRoute";
|
||||
|
||||
test("connection settings persist to the studio settings API", async ({ page }) => {
|
||||
test("voice reply settings persist to the studio settings API", async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await page.getByTestId("gateway-settings-toggle").click();
|
||||
await expect(page.getByLabel("Upstream URL")).toBeVisible();
|
||||
await page.getByTitle("Voice reply settings").click();
|
||||
await expect(page.getByRole("switch", { name: "Voice replies" })).toBeVisible();
|
||||
await page.waitForFunction(() => {
|
||||
const element = document.querySelector('[aria-label="Voice replies"]');
|
||||
return element instanceof HTMLButtonElement && !element.disabled;
|
||||
});
|
||||
|
||||
await page.getByLabel("Upstream URL").fill("ws://gateway.example:18789");
|
||||
await page.getByLabel("Upstream token").fill("token-123");
|
||||
|
||||
const request = await page.waitForRequest((req) => {
|
||||
const requestPromise = page.waitForRequest((req) => {
|
||||
if (!req.url().includes("/api/studio") || req.method() !== "PUT") {
|
||||
return false;
|
||||
}
|
||||
const payload = JSON.parse(req.postData() ?? "{}") as Record<string, unknown>;
|
||||
const gateway = (payload.gateway ?? {}) as { url?: string; token?: string };
|
||||
return gateway.url === "ws://gateway.example:18789" && gateway.token === "token-123";
|
||||
const voiceReplies = (payload.voiceReplies ?? {}) as Record<string, { enabled?: boolean }>;
|
||||
return Object.values(voiceReplies).some((entry) => entry.enabled === true);
|
||||
});
|
||||
await page.getByRole("switch", { name: "Voice replies" }).click();
|
||||
const request = await requestPromise;
|
||||
|
||||
const payload = JSON.parse(request.postData() ?? "{}") as Record<string, unknown>;
|
||||
const gateway = (payload.gateway ?? {}) as { url?: string; token?: string };
|
||||
expect(gateway.url).toBe("ws://gateway.example:18789");
|
||||
expect(gateway.token).toBe("token-123");
|
||||
await expect(
|
||||
page.getByRole("button", { name: /^(Connect|Disconnect)$/ })
|
||||
).toBeVisible();
|
||||
const voiceReplies = (payload.voiceReplies ?? {}) as Record<string, { enabled?: boolean }>;
|
||||
expect(Object.keys(voiceReplies).length).toBeGreaterThan(0);
|
||||
expect(Object.values(voiceReplies).some((entry) => entry.enabled === true)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -5,11 +5,14 @@ test.beforeEach(async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
});
|
||||
|
||||
test("shows_disconnected_connect_surface", async ({ page }) => {
|
||||
test("shows_office_shell_from_root_redirect", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByLabel("Upstream URL")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /^(Connect|Connecting…)$/ })).toBeVisible();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname)
|
||||
.toBe("/office");
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("persists_gateway_fields_to_studio_settings", async ({ page }) => {
|
||||
@@ -35,17 +38,20 @@ test("persists_gateway_fields_to_studio_settings", async ({ page }) => {
|
||||
test("focused_preferences_persist_across_reload", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await expect(page.getByTestId("gateway-settings-toggle")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("studio-menu-toggle")).toBeVisible();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname)
|
||||
.toBe("/office");
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("clears_unseen_indicator_on_focus", async ({ page }) => {
|
||||
test("shows_chat_entrypoint_in_office_shell", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("studio-menu-toggle").click();
|
||||
await expect(page.getByTestId("gateway-settings-toggle")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
await expect(page.getByTitle("Voice reply settings")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { stubStudioRoute } from "./helpers/studioRoute";
|
||||
|
||||
test("loads focused studio empty state", async ({ page }) => {
|
||||
await page.route("**/api/studio", async (route, request) => {
|
||||
if (request.method() === "PUT") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method() !== "GET") {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
settings: { version: 1, gateway: null, focused: {}, avatars: {} },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("loads office shell from root", async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("studio-menu-toggle")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Connect" }).first()).toBeVisible();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname)
|
||||
.toBe("/office");
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "CHAT" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Page, Route, Request } from "@playwright/test";
|
||||
import type { AgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
|
||||
export type StudioSettingsFixture = {
|
||||
version: 1;
|
||||
gateway: { url: string; token: string } | null;
|
||||
focused: Record<string, { mode: "focused"; filter: string; selectedAgentId: string | null }>;
|
||||
avatars: Record<string, Record<string, string>>;
|
||||
avatars: Record<string, Record<string, AgentAvatarProfile>>;
|
||||
};
|
||||
|
||||
const DEFAULT_SETTINGS: StudioSettingsFixture = {
|
||||
@@ -65,25 +66,31 @@ const createStudioRoute = (initial: StudioSettingsFixture = DEFAULT_SETTINGS) =>
|
||||
}
|
||||
|
||||
if (patch.avatars && typeof patch.avatars === "object") {
|
||||
const avatarsPatch = patch.avatars as Record<string, Record<string, string | null> | null>;
|
||||
const avatarsPatch = patch.avatars as
|
||||
| Record<string, Record<string, AgentAvatarProfile | null> | null>
|
||||
| null;
|
||||
const avatarsNext: StudioSettingsFixture["avatars"] = { ...next.avatars };
|
||||
for (const [gatewayKey, gatewayPatch] of Object.entries(avatarsPatch)) {
|
||||
for (const [gatewayKey, gatewayPatch] of Object.entries(avatarsPatch ?? {})) {
|
||||
if (gatewayPatch === null) {
|
||||
delete avatarsNext[gatewayKey];
|
||||
continue;
|
||||
}
|
||||
const existing = avatarsNext[gatewayKey] ? { ...avatarsNext[gatewayKey] } : {};
|
||||
for (const [agentId, seedPatch] of Object.entries(gatewayPatch)) {
|
||||
if (seedPatch === null) {
|
||||
for (const [agentId, avatarPatch] of Object.entries(gatewayPatch)) {
|
||||
if (avatarPatch === null) {
|
||||
delete existing[agentId];
|
||||
continue;
|
||||
}
|
||||
const seed = typeof seedPatch === "string" ? seedPatch.trim() : "";
|
||||
if (!seed) {
|
||||
if (
|
||||
typeof avatarPatch !== "object" ||
|
||||
avatarPatch === null ||
|
||||
typeof avatarPatch.seed !== "string" ||
|
||||
avatarPatch.seed.trim().length === 0
|
||||
) {
|
||||
delete existing[agentId];
|
||||
continue;
|
||||
}
|
||||
existing[agentId] = seed;
|
||||
existing[agentId] = avatarPatch;
|
||||
}
|
||||
avatarsNext[gatewayKey] = existing;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ test.beforeEach(async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
});
|
||||
|
||||
test("redirects unknown app routes to root", async ({ page }) => {
|
||||
test("redirects unknown app routes to office", async ({ page }) => {
|
||||
await page.goto("/not-a-real-route");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname, {
|
||||
message: "Expected invalid route to redirect to root path.",
|
||||
message: "Expected invalid route to redirect to office path.",
|
||||
})
|
||||
.toBe("/");
|
||||
await expect(page.getByTestId("studio-menu-toggle")).toBeVisible();
|
||||
.toBe("/office");
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -5,16 +5,13 @@ test.beforeEach(async ({ page }) => {
|
||||
await stubStudioRoute(page);
|
||||
});
|
||||
|
||||
test("settings route shows connect UI while disconnected and can return to chat", async ({ page }) => {
|
||||
test("settings route redirects to office", async ({ page }) => {
|
||||
await page.goto("/agents/main/settings");
|
||||
|
||||
await expect(page.getByRole("button", { name: "Back to chat" })).toBeVisible();
|
||||
await expect(page.getByLabel("Upstream URL")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Back to chat" }).click();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname, {
|
||||
message: "Expected back button to return to chat route.",
|
||||
message: "Expected settings route to redirect to office.",
|
||||
})
|
||||
.toBe("/");
|
||||
.toBe("/office");
|
||||
await expect(page.getByRole("button", { name: "Open headquarters sidebar" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { AgentAvatarCreatorModal } from "@/features/agents/components/AgentAvatarCreatorModal";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
|
||||
vi.mock("@/features/agents/components/AgentAvatarPreview3D", () => ({
|
||||
AgentAvatarPreview3D: () => <div data-testid="avatar-preview-3d">preview</div>,
|
||||
}));
|
||||
|
||||
describe("AgentAvatarCreatorModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("saves the edited avatar profile", async () => {
|
||||
const initialProfile = createDefaultAgentAvatarProfile("seed-a");
|
||||
const onSave = vi.fn(async () => {});
|
||||
|
||||
render(
|
||||
<AgentAvatarCreatorModal
|
||||
open
|
||||
agentId="agent-1"
|
||||
agentName="Agent One"
|
||||
initialProfile={initialProfile}
|
||||
onClose={() => {}}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Backpack" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save avatar" }));
|
||||
|
||||
expect(onSave).toHaveBeenCalledTimes(1);
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
seed: "seed-a",
|
||||
accessories: expect.objectContaining({
|
||||
backpack: !initialProfile.accessories.backpack,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { createElement } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { AgentEditorModal } from "@/features/agents/components/AgentEditorModal";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import type { AgentState } from "@/features/agents/state/store";
|
||||
import type { GatewayClient } from "@/lib/gateway/GatewayClient";
|
||||
|
||||
vi.mock("@/features/agents/components/AgentAvatarPreview3D", () => ({
|
||||
AgentAvatarPreview3D: () => createElement("div", { "data-testid": "avatar-preview-3d" }, "preview"),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/agents/components/inspect/AgentBrainPanel", () => ({
|
||||
AgentBrainPanel: ({
|
||||
selectedAgentId,
|
||||
activeSection,
|
||||
}: {
|
||||
selectedAgentId: string | null;
|
||||
activeSection?: string;
|
||||
}) =>
|
||||
createElement(
|
||||
"div",
|
||||
{ "data-testid": "brain-panel" },
|
||||
`brain:${selectedAgentId}:${activeSection ?? "all"}`,
|
||||
),
|
||||
}));
|
||||
|
||||
const buildAgent = (): AgentState =>
|
||||
({
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
avatarProfile: createDefaultAgentAvatarProfile("seed-a"),
|
||||
avatarSeed: "seed-a",
|
||||
avatarUrl: null,
|
||||
status: "idle",
|
||||
sessionCreated: false,
|
||||
awaitingUserInput: false,
|
||||
hasUnseenActivity: false,
|
||||
outputLines: [],
|
||||
lastResult: null,
|
||||
lastDiff: null,
|
||||
runId: null,
|
||||
runStartedAt: null,
|
||||
streamText: null,
|
||||
thinkingTrace: null,
|
||||
latestOverride: null,
|
||||
latestOverrideKind: null,
|
||||
lastAssistantMessageAt: null,
|
||||
lastActivityAt: null,
|
||||
latestPreview: null,
|
||||
lastUserMessage: null,
|
||||
draft: "",
|
||||
queuedMessages: [],
|
||||
sessionSettingsSynced: false,
|
||||
historyLoadedAt: null,
|
||||
historyFetchLimit: null,
|
||||
historyFetchedCount: null,
|
||||
historyMaybeTruncated: false,
|
||||
toolCallingEnabled: true,
|
||||
showThinkingTraces: false,
|
||||
sessionKey: "session-1",
|
||||
model: undefined,
|
||||
thinkingLevel: undefined,
|
||||
}) as AgentState;
|
||||
|
||||
describe("AgentEditorModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("saves avatar changes from the avatar section", async () => {
|
||||
const agent = buildAgent();
|
||||
const onAvatarSave = vi.fn(async () => {});
|
||||
const initialBackpack = agent.avatarProfile?.accessories.backpack;
|
||||
|
||||
render(
|
||||
createElement(AgentEditorModal, {
|
||||
open: true,
|
||||
client: {} as GatewayClient,
|
||||
agents: [agent],
|
||||
agent,
|
||||
onClose: () => {},
|
||||
onAvatarSave,
|
||||
}),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Backpack" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save avatar" }));
|
||||
|
||||
expect(onAvatarSave).toHaveBeenCalledTimes(1);
|
||||
expect(onAvatarSave).toHaveBeenCalledWith(
|
||||
"agent-1",
|
||||
expect.objectContaining({
|
||||
seed: "seed-a",
|
||||
accessories: expect.objectContaining({ backpack: !initialBackpack }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("switches to another file section", () => {
|
||||
const agent = buildAgent();
|
||||
|
||||
render(
|
||||
createElement(AgentEditorModal, {
|
||||
open: true,
|
||||
client: {} as GatewayClient,
|
||||
agents: [agent],
|
||||
agent,
|
||||
onClose: () => {},
|
||||
onAvatarSave: () => {},
|
||||
}),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Tools/i }));
|
||||
|
||||
expect(screen.getByTestId("brain-panel")).toHaveTextContent("brain:agent-1:TOOLS.md");
|
||||
});
|
||||
|
||||
it("honors the initial file section", () => {
|
||||
const agent = buildAgent();
|
||||
|
||||
render(
|
||||
createElement(AgentEditorModal, {
|
||||
open: true,
|
||||
client: {} as GatewayClient,
|
||||
agents: [agent],
|
||||
agent,
|
||||
initialSection: "MEMORY.md",
|
||||
onClose: () => {},
|
||||
onAvatarSave: () => {},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("brain-panel")).toHaveTextContent("brain:agent-1:MEMORY.md");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { hydrateAgentFleetFromGateway } from "@/features/agents/operations/agentFleetHydration";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import type { StudioSettings } from "@/lib/studio/settings";
|
||||
|
||||
describe("hydrateAgentFleetFromGateway", () => {
|
||||
@@ -13,12 +14,13 @@ describe("hydrateAgentFleetFromGateway", () => {
|
||||
focused: {},
|
||||
avatars: {
|
||||
[gatewayUrl]: {
|
||||
"agent-1": "persisted-seed",
|
||||
"agent-1": createDefaultAgentAvatarProfile("persisted-seed"),
|
||||
},
|
||||
},
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
};
|
||||
|
||||
const call = vi.fn(async (method: string, params: unknown) => {
|
||||
@@ -127,6 +129,7 @@ describe("hydrateAgentFleetFromGateway", () => {
|
||||
name: "One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
avatarSeed: "persisted-seed",
|
||||
avatarProfile: expect.objectContaining({ seed: "persisted-seed" }),
|
||||
avatarUrl: "https://example.com/one.png",
|
||||
model: "openai/gpt-4.1",
|
||||
thinkingLevel: "medium",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { deriveHydrateAgentFleetResult } from "@/features/agents/operations/agentFleetHydrationDerivation";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import type { StudioSettings } from "@/lib/studio/settings";
|
||||
|
||||
describe("deriveHydrateAgentFleetResult", () => {
|
||||
@@ -13,12 +14,13 @@ describe("deriveHydrateAgentFleetResult", () => {
|
||||
focused: {},
|
||||
avatars: {
|
||||
[gatewayUrl]: {
|
||||
"agent-1": "persisted-seed",
|
||||
"agent-1": createDefaultAgentAvatarProfile("persisted-seed"),
|
||||
},
|
||||
},
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
};
|
||||
|
||||
const result = deriveHydrateAgentFleetResult({
|
||||
@@ -93,6 +95,7 @@ describe("deriveHydrateAgentFleetResult", () => {
|
||||
name: "One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
avatarSeed: "persisted-seed",
|
||||
avatarProfile: expect.objectContaining({ seed: "persisted-seed" }),
|
||||
avatarUrl: "https://example.com/one.png",
|
||||
model: "openai/gpt-4.1",
|
||||
thinkingLevel: "medium",
|
||||
|
||||
@@ -166,6 +166,7 @@ describe("studioBootstrapOperation", () => {
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
}),
|
||||
isFocusFilterTouched: () => false,
|
||||
});
|
||||
@@ -203,6 +204,7 @@ describe("studioBootstrapOperation", () => {
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
}),
|
||||
isFocusFilterTouched: () => true,
|
||||
});
|
||||
|
||||
@@ -157,6 +157,7 @@ describe("studioBootstrapWorkflow", () => {
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
@@ -197,6 +198,7 @@ describe("studioBootstrapWorkflow", () => {
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createDefaultAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
|
||||
import {
|
||||
mergeStudioSettings,
|
||||
@@ -12,6 +13,7 @@ describe("studio settings normalization", () => {
|
||||
expect(normalized.gateway).toBeNull();
|
||||
expect(normalized.focused).toEqual({});
|
||||
expect(normalized.avatars).toEqual({});
|
||||
expect(normalized.office).toEqual({});
|
||||
});
|
||||
|
||||
it("normalizes gateway entries", () => {
|
||||
@@ -114,16 +116,17 @@ describe("studio settings normalization", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalized.avatars["ws://localhost:18789"]).toEqual({
|
||||
"agent-1": "seed-1",
|
||||
});
|
||||
expect(normalized.avatars["ws://localhost:18789"]?.["agent-1"]?.seed).toBe("seed-1");
|
||||
});
|
||||
|
||||
it("merges avatar patches", () => {
|
||||
const firstProfile = createDefaultAgentAvatarProfile("seed-1");
|
||||
const replacementProfile = createDefaultAgentAvatarProfile("seed-2");
|
||||
const secondProfile = createDefaultAgentAvatarProfile("seed-3");
|
||||
const current = normalizeStudioSettings({
|
||||
avatars: {
|
||||
"ws://localhost:18789": {
|
||||
"agent-1": "seed-1",
|
||||
"agent-1": firstProfile,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -131,15 +134,55 @@ describe("studio settings normalization", () => {
|
||||
const merged = mergeStudioSettings(current, {
|
||||
avatars: {
|
||||
"ws://localhost:18789": {
|
||||
"agent-1": "seed-2",
|
||||
"agent-2": "seed-3",
|
||||
"agent-1": replacementProfile,
|
||||
"agent-2": secondProfile,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(merged.avatars["ws://localhost:18789"]).toEqual({
|
||||
"agent-1": "seed-2",
|
||||
"agent-2": "seed-3",
|
||||
expect(merged.avatars["ws://localhost:18789"]?.["agent-1"]?.seed).toBe("seed-2");
|
||||
expect(merged.avatars["ws://localhost:18789"]?.["agent-2"]?.seed).toBe("seed-3");
|
||||
});
|
||||
|
||||
it("normalizes office title preferences per gateway", () => {
|
||||
const normalized = normalizeStudioSettings({
|
||||
office: {
|
||||
" ws://localhost:18789 ": {
|
||||
title: " Team Orbit ",
|
||||
},
|
||||
bad: {
|
||||
title: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalized.office["ws://localhost:18789"]).toEqual({
|
||||
title: "Team Orbit",
|
||||
});
|
||||
expect(normalized.office.bad).toEqual({
|
||||
title: "Luke Headquarters",
|
||||
});
|
||||
});
|
||||
|
||||
it("merges office title patches", () => {
|
||||
const current = normalizeStudioSettings({
|
||||
office: {
|
||||
"ws://localhost:18789": {
|
||||
title: "Luke Headquarters",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const merged = mergeStudioSettings(current, {
|
||||
office: {
|
||||
"ws://localhost:18789": {
|
||||
title: "Orbit Control",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(merged.office["ws://localhost:18789"]).toEqual({
|
||||
title: "Orbit Control",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,18 +47,18 @@ describe("studio settings route", () => {
|
||||
|
||||
const response = await GET();
|
||||
const body = (await response.json()) as {
|
||||
settings?: { gateway?: { url?: string; token?: string } | null };
|
||||
localGatewayDefaults?: { url?: string; token?: string } | null;
|
||||
settings?: { gateway?: { url?: string; tokenConfigured?: boolean } | null };
|
||||
localGatewayDefaults?: { url?: string; tokenConfigured?: boolean } | null;
|
||||
};
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.localGatewayDefaults).toEqual({
|
||||
url: "ws://localhost:18791",
|
||||
token: "local-token",
|
||||
tokenConfigured: true,
|
||||
});
|
||||
expect(body.settings?.gateway).toEqual({
|
||||
url: "ws://localhost:18791",
|
||||
token: "local-token",
|
||||
tokenConfigured: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,11 @@ describe("studio settings route", () => {
|
||||
|
||||
const patch = {
|
||||
gateway: { url: "ws://example.test:1234", token: "t" },
|
||||
office: {
|
||||
"ws://example.test:1234": {
|
||||
title: "Orbit Control",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const putResponse = await PUT({
|
||||
@@ -91,16 +96,31 @@ describe("studio settings route", () => {
|
||||
|
||||
const getResponse = await GET();
|
||||
const body = (await getResponse.json()) as {
|
||||
settings?: { gateway?: { url?: string; token?: string } | null };
|
||||
settings?: {
|
||||
gateway?: { url?: string; tokenConfigured?: boolean } | null;
|
||||
office?: Record<string, { title?: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
expect(getResponse.status).toBe(200);
|
||||
expect(body.settings?.gateway).toEqual({ url: "ws://example.test:1234", token: "t" });
|
||||
expect(body.settings?.gateway).toEqual({
|
||||
url: "ws://example.test:1234",
|
||||
tokenConfigured: true,
|
||||
});
|
||||
expect(body.settings?.office?.["ws://example.test:1234"]).toEqual({
|
||||
title: "Orbit Control",
|
||||
});
|
||||
|
||||
const settingsPath = path.join(tempDir, "claw3d", "settings.json");
|
||||
expect(fs.existsSync(settingsPath)).toBe(true);
|
||||
const raw = fs.readFileSync(settingsPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as { gateway?: { url?: string; token?: string } | null };
|
||||
const parsed = JSON.parse(raw) as {
|
||||
gateway?: { url?: string; token?: string } | null;
|
||||
office?: Record<string, { title?: string }>;
|
||||
};
|
||||
expect(parsed.gateway).toEqual({ url: "ws://example.test:1234", token: "t" });
|
||||
expect(parsed.office?.["ws://example.test:1234"]).toEqual({
|
||||
title: "Orbit Control",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user