diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 7ac4a15e94e..e97f01a26af 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -253,7 +253,13 @@ const SOURCE_TEST_TARGETS = new Map([ ["extensions/google-meet/src/cli.ts", ["extensions/google-meet/src/cli.test.ts"]], ["extensions/google-meet/src/create.ts", ["extensions/google-meet/index.test.ts"]], ["extensions/google-meet/src/oauth.ts", ["extensions/google-meet/src/oauth.test.ts"]], + ["src/commands/doctor-memory-search.ts", ["src/commands/doctor-memory-search.test.ts"]], ["src/agents/live-model-turn-probes.ts", ["src/agents/live-model-turn-probes.test.ts"]], + [ + "src/memory-host-sdk/host/embedding-defaults.ts", + ["src/memory-host-sdk/host/embeddings.test.ts"], + ], + ["src/memory-host-sdk/host/embeddings.ts", ["src/memory-host-sdk/host/embeddings.test.ts"]], [ "src/auto-reply/reply/dispatch-from-config.ts", ["src/auto-reply/reply/dispatch-from-config.test.ts"], diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index ee5c7cc4ccc..1b836a18e9d 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -14,8 +14,8 @@ import { import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { DEFAULT_LOCAL_MODEL } from "../memory-host-sdk/engine-embeddings.js"; import { checkQmdBinaryAvailability } from "../memory-host-sdk/engine-qmd.js"; +import { DEFAULT_LOCAL_MODEL } from "../memory-host-sdk/host/embedding-defaults.js"; import { hasConfiguredMemorySecretInput } from "../memory-host-sdk/secret.js"; import { auditDreamingArtifacts, diff --git a/src/memory-host-sdk/host/embedding-defaults.ts b/src/memory-host-sdk/host/embedding-defaults.ts new file mode 100644 index 00000000000..fc503c9aca3 --- /dev/null +++ b/src/memory-host-sdk/host/embedding-defaults.ts @@ -0,0 +1,2 @@ +export const DEFAULT_LOCAL_MODEL = + "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf"; diff --git a/src/memory-host-sdk/host/embeddings.ts b/src/memory-host-sdk/host/embeddings.ts index 08d5bd65df0..375127a8fc6 100644 --- a/src/memory-host-sdk/host/embeddings.ts +++ b/src/memory-host-sdk/host/embeddings.ts @@ -1,4 +1,5 @@ import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { DEFAULT_LOCAL_MODEL } from "./embedding-defaults.js"; import { sanitizeAndNormalizeEmbedding } from "./embedding-vectors.js"; import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.types.js"; import { @@ -17,8 +18,7 @@ export type { GeminiTaskType, } from "./embeddings.types.js"; -export const DEFAULT_LOCAL_MODEL = - "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf"; +export { DEFAULT_LOCAL_MODEL } from "./embedding-defaults.js"; export async function createLocalEmbeddingProvider( options: EmbeddingProviderOptions, diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 4d8cdd8dea3..f70d7c4cbc3 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -323,6 +323,22 @@ describe("scripts/test-projects changed-target routing", () => { }); }); + it("routes memory doctor and embedding default edits to focused tests", () => { + expect( + resolveChangedTestTargetPlan([ + "src/commands/doctor-memory-search.ts", + "src/memory-host-sdk/host/embedding-defaults.ts", + "src/memory-host-sdk/host/embeddings.ts", + ]), + ).toEqual({ + mode: "targets", + targets: [ + "src/commands/doctor-memory-search.test.ts", + "src/memory-host-sdk/host/embeddings.test.ts", + ], + }); + }); + it("routes changed utils and shared files to their light scoped lanes", () => { const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ "src/shared/string-normalization.ts", diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index 7a22a95132f..8318c0918c7 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -243,61 +243,32 @@ describe("grouped chat rendering", () => { expect(assistantConfirm?.classList.contains("chat-delete-confirm--right")).toBe(true); }); - it("falls back to the local logo when the assistant avatar is a remote URL", () => { - const container = document.createElement("div"); + it("renders assistant avatar variants", () => { + const renderAvatar = (assistantAvatar: string) => { + const container = document.createElement("div"); + renderAssistantMessage( + container, + { + role: "assistant", + content: "hello", + timestamp: 1000, + }, + { assistantAvatar, assistantName: "Val" }, + ); + return container.querySelector(".chat-avatar.assistant"); + }; - renderAssistantMessage( - container, - { - role: "assistant", - content: "hello", - timestamp: 1000, - }, - { assistantAvatar: "https://example.com/avatar.png" }, - ); + const remoteAvatar = renderAvatar("https://example.com/avatar.png"); + expect(remoteAvatar?.getAttribute("src")).toBe("/openclaw-logo.svg"); - const avatar = container.querySelector(".chat-avatar.assistant"); - expect(avatar).not.toBeNull(); - expect(avatar?.getAttribute("src")).toBe("/openclaw-logo.svg"); - }); + const blobAvatar = renderAvatar("blob:managed-image"); + expect(blobAvatar?.tagName).toBe("IMG"); + expect(blobAvatar?.getAttribute("src")).toBe("blob:managed-image"); - it("renders a blob: assistant avatar as an image", () => { - const container = document.createElement("div"); - - renderAssistantMessage( - container, - { - role: "assistant", - content: "hello", - timestamp: 1000, - }, - { assistantAvatar: "blob:managed-image", assistantName: "Val" }, - ); - - const avatar = container.querySelector(".chat-avatar.assistant"); - expect(avatar).not.toBeNull(); - expect(avatar?.tagName).toBe("IMG"); - expect(avatar?.getAttribute("src")).toBe("blob:managed-image"); - }); - - it("renders a configured assistant text avatar", () => { - const container = document.createElement("div"); - - renderAssistantMessage( - container, - { - role: "assistant", - content: "hello", - timestamp: 1000, - }, - { assistantAvatar: "VC", assistantName: "Val" }, - ); - - const avatar = container.querySelector(".chat-avatar.assistant"); - expect(avatar).not.toBeNull(); - expect(avatar?.tagName).toBe("DIV"); - expect(avatar?.textContent).toContain("VC"); - expect(avatar?.getAttribute("aria-label")).toBe("Val"); + const textAvatar = renderAvatar("VC"); + expect(textAvatar?.tagName).toBe("DIV"); + expect(textAvatar?.textContent).toContain("VC"); + expect(textAvatar?.getAttribute("aria-label")).toBe("Val"); }); it("rejects unsafe invisible controls in assistant text avatars", () => { @@ -307,129 +278,80 @@ describe("grouped chat rendering", () => { expect(resolveAssistantTextAvatar("V\u200bC")).toBeNull(); }); - it("includes cache tokens when rendering assistant context usage", () => { - const container = document.createElement("div"); - - renderAssistantMessage( - container, - { - role: "assistant", - content: "Done", - usage: { - input: 1, - output: 1200, - cacheRead: 438_400, - cacheWrite: 307, + it("renders assistant context usage from input and cache tokens", () => { + const renderUsage = (usage: Record, contextWindow: number) => { + const container = document.createElement("div"); + renderAssistantMessage( + container, + { + role: "assistant", + content: "Done", + usage, + model: "anthropic/claude-opus-4-7", + timestamp: 1000, }, - model: "anthropic/claude-opus-4-7", - timestamp: 1000, - }, - { contextWindow: 1_000_000 }, - ); + { contextWindow }, + ); + return container; + }; - expect(container.querySelector(".msg-meta__ctx")?.textContent).toBe("44% ctx"); - expect(container.textContent).toContain("R438.4k"); - expect(container.textContent).toContain("W307"); + const cached = renderUsage( + { + input: 1, + output: 1200, + cacheRead: 438_400, + cacheWrite: 307, + }, + 1_000_000, + ); + expect(cached.querySelector(".msg-meta__ctx")?.textContent).toBe("44% ctx"); + expect(cached.textContent).toContain("R438.4k"); + expect(cached.textContent).toContain("W307"); + + const outputHeavy = renderUsage( + { + input: 1_000, + output: 9_000, + cacheRead: 0, + cacheWrite: 0, + }, + 10_000, + ); + expect(outputHeavy.querySelector(".msg-meta__ctx")?.textContent).toBe("10% ctx"); }); - it("excludes output tokens when rendering assistant context usage", () => { - const container = document.createElement("div"); - - renderAssistantMessage( - container, - { - role: "assistant", - content: "Long response", - usage: { - input: 1_000, - output: 9_000, - cacheRead: 0, - cacheWrite: 0, + it("renders configured local user names and avatar variants", () => { + const renderUser = (opts: Partial) => { + const container = document.createElement("div"); + renderGroupedMessage( + container, + { + role: "user", + content: "hello", + timestamp: 1000, }, - timestamp: 1000, - }, - { contextWindow: 10_000 }, - ); + "user", + opts, + ); + return container; + }; - expect(container.querySelector(".msg-meta__ctx")?.textContent).toBe("10% ctx"); - }); - - it("renders the configured local user name in user message footers", () => { - const container = document.createElement("div"); - - renderGroupedMessage( - container, - { - role: "user", - content: "hello", - timestamp: 1000, - }, - "user", - { userName: "Buns" }, - ); - - const sender = container.querySelector(".chat-group.user .chat-sender-name"); + const named = renderUser({ userName: "Buns" }); + const sender = named.querySelector(".chat-group.user .chat-sender-name"); expect(sender?.textContent).toBe("Buns"); - }); - it("renders a local user image avatar when provided", () => { - const container = document.createElement("div"); + for (const src of ["data:image/png;base64,AAA", "/avatar/user"]) { + const container = renderUser({ userName: "Buns", userAvatar: src }); + const avatar = container.querySelector(".chat-avatar.user"); + expect(avatar?.getAttribute("src")).toBe(src); + expect(avatar?.getAttribute("alt")).toBe("Buns"); + } - renderGroupedMessage( - container, - { - role: "user", - content: "hello", - timestamp: 1000, - }, - "user", - { userName: "Buns", userAvatar: "data:image/png;base64,AAA" }, + const textAvatar = renderUser({ userAvatar: "🦞" }).querySelector( + ".chat-avatar.user", ); - - const avatar = container.querySelector(".chat-avatar.user"); - expect(avatar).not.toBeNull(); - expect(avatar?.getAttribute("src")).toBe("data:image/png;base64,AAA"); - expect(avatar?.getAttribute("alt")).toBe("Buns"); - }); - - it("renders a local user avatar route when provided", () => { - const container = document.createElement("div"); - - renderGroupedMessage( - container, - { - role: "user", - content: "hello", - timestamp: 1000, - }, - "user", - { userName: "Buns", userAvatar: "/avatar/user" }, - ); - - const avatar = container.querySelector(".chat-avatar.user"); - expect(avatar).not.toBeNull(); - expect(avatar?.getAttribute("src")).toBe("/avatar/user"); - expect(avatar?.getAttribute("alt")).toBe("Buns"); - }); - - it("renders a local user text avatar when provided", () => { - const container = document.createElement("div"); - - renderGroupedMessage( - container, - { - role: "user", - content: "hello", - timestamp: 1000, - }, - "user", - { userAvatar: "🦞" }, - ); - - const avatar = container.querySelector(".chat-avatar.user"); - expect(avatar).not.toBeNull(); - expect(avatar?.tagName).toBe("DIV"); - expect(avatar?.textContent).toContain("🦞"); + expect(textAvatar?.tagName).toBe("DIV"); + expect(textAvatar?.textContent).toContain("🦞"); }); it("keeps inline tool cards collapsed by default and renders expanded state", () => {