chore: Fix types in tests 6/N.

This commit is contained in:
cpojer
2026-02-17 10:52:25 +09:00
parent b6d4f7c00e
commit 003d6c45d6
6 changed files with 180 additions and 116 deletions

View File

@@ -7,12 +7,23 @@ const loadConfig = vi.fn(() => ({
auth: { token: "ltok" }, auth: { token: "ltok" },
}, },
})); }));
const resolveGatewayPort = vi.fn(() => 18789); const resolveGatewayPort = vi.fn((_cfg?: unknown) => 18789);
const discoverGatewayBeacons = vi.fn(async () => []); const discoverGatewayBeacons = vi.fn(
async (_opts?: unknown): Promise<Array<{ tailnetDns: string }>> => [],
);
const pickPrimaryTailnetIPv4 = vi.fn(() => "100.64.0.10"); const pickPrimaryTailnetIPv4 = vi.fn(() => "100.64.0.10");
const sshStop = vi.fn(async () => {}); const sshStop = vi.fn(async () => {});
const resolveSshConfig = vi.fn(async () => null); const resolveSshConfig = vi.fn(
const startSshPortForward = vi.fn(async () => ({ async (
_opts?: unknown,
): Promise<{
user: string;
host: string;
port: number;
identityFiles: string[];
} | null> => null,
);
const startSshPortForward = vi.fn(async (_opts?: unknown) => ({
parsedTarget: { user: "me", host: "studio", port: 22 }, parsedTarget: { user: "me", host: "studio", port: 22 },
localPort: 18789, localPort: 18789,
remotePort: 18789, remotePort: 18789,
@@ -20,7 +31,8 @@ const startSshPortForward = vi.fn(async () => ({
stderr: [], stderr: [],
stop: sshStop, stop: sshStop,
})); }));
const probeGateway = vi.fn(async ({ url }: { url: string }) => { const probeGateway = vi.fn(async (opts: { url: string }) => {
const { url } = opts;
if (url.includes("127.0.0.1")) { if (url.includes("127.0.0.1")) {
return { return {
ok: true, ok: true,
@@ -80,32 +92,32 @@ const probeGateway = vi.fn(async ({ url }: { url: string }) => {
}); });
vi.mock("../config/config.js", () => ({ vi.mock("../config/config.js", () => ({
loadConfig: () => loadConfig(), loadConfig,
resolveGatewayPort: (cfg: unknown) => resolveGatewayPort(cfg), resolveGatewayPort,
})); }));
vi.mock("../infra/bonjour-discovery.js", () => ({ vi.mock("../infra/bonjour-discovery.js", () => ({
discoverGatewayBeacons: (opts: unknown) => discoverGatewayBeacons(opts), discoverGatewayBeacons,
})); }));
vi.mock("../infra/tailnet.js", () => ({ vi.mock("../infra/tailnet.js", () => ({
pickPrimaryTailnetIPv4: () => pickPrimaryTailnetIPv4(), pickPrimaryTailnetIPv4,
})); }));
vi.mock("../infra/ssh-tunnel.js", async (importOriginal) => { vi.mock("../infra/ssh-tunnel.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/ssh-tunnel.js")>(); const actual = await importOriginal<typeof import("../infra/ssh-tunnel.js")>();
return { return {
...actual, ...actual,
startSshPortForward: (opts: unknown) => startSshPortForward(opts), startSshPortForward,
}; };
}); });
vi.mock("../infra/ssh-config.js", () => ({ vi.mock("../infra/ssh-config.js", () => ({
resolveSshConfig: (opts: unknown) => resolveSshConfig(opts), resolveSshConfig,
})); }));
vi.mock("../gateway/probe.js", () => ({ vi.mock("../gateway/probe.js", () => ({
probeGateway: (opts: unknown) => probeGateway(opts), probeGateway,
})); }));
function createRuntimeCapture() { function createRuntimeCapture() {
@@ -198,7 +210,8 @@ describe("gateway-status command", () => {
loadConfig.mockReturnValueOnce({ loadConfig.mockReturnValueOnce({
gateway: { gateway: {
mode: "remote", mode: "remote",
remote: {}, remote: { url: "", token: "" },
auth: { token: "ltok" },
}, },
}); });
discoverGatewayBeacons.mockResolvedValueOnce([ discoverGatewayBeacons.mockResolvedValueOnce([
@@ -226,6 +239,7 @@ describe("gateway-status command", () => {
gateway: { gateway: {
mode: "remote", mode: "remote",
remote: { url: "ws://peters-mac-studio-1.sheep-coho.ts.net:18789", token: "rtok" }, remote: { url: "ws://peters-mac-studio-1.sheep-coho.ts.net:18789", token: "rtok" },
auth: { token: "ltok" },
}, },
}); });
resolveSshConfig.mockResolvedValueOnce({ resolveSshConfig.mockResolvedValueOnce({
@@ -259,6 +273,7 @@ describe("gateway-status command", () => {
gateway: { gateway: {
mode: "remote", mode: "remote",
remote: { url: "ws://studio.example:18789", token: "rtok" }, remote: { url: "ws://studio.example:18789", token: "rtok" },
auth: { token: "ltok" },
}, },
}); });
resolveSshConfig.mockResolvedValueOnce(null); resolveSshConfig.mockResolvedValueOnce(null);
@@ -284,6 +299,7 @@ describe("gateway-status command", () => {
gateway: { gateway: {
mode: "remote", mode: "remote",
remote: { url: "ws://studio.example:18789", token: "rtok" }, remote: { url: "ws://studio.example:18789", token: "rtok" },
auth: { token: "ltok" },
}, },
}); });
resolveSshConfig.mockResolvedValueOnce({ resolveSshConfig.mockResolvedValueOnce({

View File

@@ -1,4 +1,5 @@
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { Mock } from "vitest";
import { captureEnv } from "../test-utils/env.js"; import { captureEnv } from "../test-utils/env.js";
let envSnapshot: ReturnType<typeof captureEnv>; let envSnapshot: ReturnType<typeof captureEnv>;
@@ -323,10 +324,12 @@ const runtime = {
exit: vi.fn(), exit: vi.fn(),
}; };
const runtimeLogMock = runtime.log as Mock<(...args: unknown[]) => void>;
describe("statusCommand", () => { describe("statusCommand", () => {
it("prints JSON when requested", async () => { it("prints JSON when requested", async () => {
await statusCommand({ json: true }, runtime as never); await statusCommand({ json: true }, runtime as never);
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]); const payload = JSON.parse(String(runtimeLogMock.mock.calls[0]?.[0]));
expect(payload.linkChannel.linked).toBe(true); expect(payload.linkChannel.linked).toBe(true);
expect(payload.memory.agentId).toBe("main"); expect(payload.memory.agentId).toBe("main");
expect(payload.memoryPlugin.enabled).toBe(true); expect(payload.memoryPlugin.enabled).toBe(true);
@@ -348,9 +351,9 @@ describe("statusCommand", () => {
it("surfaces unknown usage when totalTokens is missing", async () => { it("surfaces unknown usage when totalTokens is missing", async () => {
await withUnknownUsageStore(async () => { await withUnknownUsageStore(async () => {
(runtime.log as vi.Mock).mockClear(); runtimeLogMock.mockClear();
await statusCommand({ json: true }, runtime as never); await statusCommand({ json: true }, runtime as never);
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]); const payload = JSON.parse(String(runtimeLogMock.mock.calls.at(-1)?.[0]));
expect(payload.sessions.recent[0].totalTokens).toBeNull(); expect(payload.sessions.recent[0].totalTokens).toBeNull();
expect(payload.sessions.recent[0].totalTokensFresh).toBe(false); expect(payload.sessions.recent[0].totalTokensFresh).toBe(false);
expect(payload.sessions.recent[0].percentUsed).toBeNull(); expect(payload.sessions.recent[0].percentUsed).toBeNull();
@@ -360,37 +363,37 @@ describe("statusCommand", () => {
it("prints unknown usage in formatted output when totalTokens is missing", async () => { it("prints unknown usage in formatted output when totalTokens is missing", async () => {
await withUnknownUsageStore(async () => { await withUnknownUsageStore(async () => {
(runtime.log as vi.Mock).mockClear(); runtimeLogMock.mockClear();
await statusCommand({}, runtime as never); await statusCommand({}, runtime as never);
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0])); const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
expect(logs.some((line) => line.includes("unknown/") && line.includes("(?%)"))).toBe(true); expect(logs.some((line) => line.includes("unknown/") && line.includes("(?%)"))).toBe(true);
}); });
}); });
it("prints formatted lines otherwise", async () => { it("prints formatted lines otherwise", async () => {
(runtime.log as vi.Mock).mockClear(); runtimeLogMock.mockClear();
await statusCommand({}, runtime as never); await statusCommand({}, runtime as never);
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0])); const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
expect(logs.some((l) => l.includes("OpenClaw status"))).toBe(true); expect(logs.some((l: string) => l.includes("OpenClaw status"))).toBe(true);
expect(logs.some((l) => l.includes("Overview"))).toBe(true); expect(logs.some((l: string) => l.includes("Overview"))).toBe(true);
expect(logs.some((l) => l.includes("Security audit"))).toBe(true); expect(logs.some((l: string) => l.includes("Security audit"))).toBe(true);
expect(logs.some((l) => l.includes("Summary:"))).toBe(true); expect(logs.some((l: string) => l.includes("Summary:"))).toBe(true);
expect(logs.some((l) => l.includes("CRITICAL"))).toBe(true); expect(logs.some((l: string) => l.includes("CRITICAL"))).toBe(true);
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true); expect(logs.some((l: string) => l.includes("Dashboard"))).toBe(true);
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true); expect(logs.some((l: string) => l.includes("macos 14.0 (arm64)"))).toBe(true);
expect(logs.some((l) => l.includes("Memory"))).toBe(true); expect(logs.some((l: string) => l.includes("Memory"))).toBe(true);
expect(logs.some((l) => l.includes("Channels"))).toBe(true); expect(logs.some((l: string) => l.includes("Channels"))).toBe(true);
expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true); expect(logs.some((l: string) => l.includes("WhatsApp"))).toBe(true);
expect(logs.some((l) => l.includes("Sessions"))).toBe(true); expect(logs.some((l: string) => l.includes("Sessions"))).toBe(true);
expect(logs.some((l) => l.includes("+1000"))).toBe(true); expect(logs.some((l: string) => l.includes("+1000"))).toBe(true);
expect(logs.some((l) => l.includes("50%"))).toBe(true); expect(logs.some((l: string) => l.includes("50%"))).toBe(true);
expect(logs.some((l) => l.includes("LaunchAgent"))).toBe(true); expect(logs.some((l: string) => l.includes("LaunchAgent"))).toBe(true);
expect(logs.some((l) => l.includes("FAQ:"))).toBe(true); expect(logs.some((l: string) => l.includes("FAQ:"))).toBe(true);
expect(logs.some((l) => l.includes("Troubleshooting:"))).toBe(true); expect(logs.some((l: string) => l.includes("Troubleshooting:"))).toBe(true);
expect(logs.some((l) => l.includes("Next steps:"))).toBe(true); expect(logs.some((l: string) => l.includes("Next steps:"))).toBe(true);
expect( expect(
logs.some( logs.some(
(l) => (l: string) =>
l.includes("openclaw status --all") || l.includes("openclaw status --all") ||
l.includes("openclaw --profile isolated status --all") || l.includes("openclaw --profile isolated status --all") ||
l.includes("openclaw status --all") || l.includes("openclaw status --all") ||
@@ -414,10 +417,10 @@ describe("statusCommand", () => {
presence: [], presence: [],
configSnapshot: null, configSnapshot: null,
}); });
(runtime.log as vi.Mock).mockClear(); runtimeLogMock.mockClear();
await statusCommand({}, runtime as never); await statusCommand({}, runtime as never);
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0])); const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
expect(logs.some((l) => l.includes("auth token"))).toBe(true); expect(logs.some((l: string) => l.includes("auth token"))).toBe(true);
} finally { } finally {
if (prevToken === undefined) { if (prevToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN; delete process.env.OPENCLAW_GATEWAY_TOKEN;
@@ -462,9 +465,9 @@ describe("statusCommand", () => {
}, },
}); });
(runtime.log as vi.Mock).mockClear(); runtimeLogMock.mockClear();
await statusCommand({}, runtime as never); await statusCommand({}, runtime as never);
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0])); const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
expect(logs.join("\n")).toMatch(/Signal/i); expect(logs.join("\n")).toMatch(/Signal/i);
expect(logs.join("\n")).toMatch(/iMessage/i); expect(logs.join("\n")).toMatch(/iMessage/i);
expect(logs.join("\n")).toMatch(/gateway:/i); expect(logs.join("\n")).toMatch(/gateway:/i);
@@ -507,7 +510,7 @@ describe("statusCommand", () => {
}); });
await statusCommand({ json: true }, runtime as never); await statusCommand({ json: true }, runtime as never);
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]); const payload = JSON.parse(String(runtimeLogMock.mock.calls.at(-1)?.[0]));
expect(payload.sessions.count).toBe(2); expect(payload.sessions.count).toBe(2);
expect(payload.sessions.paths.length).toBe(2); expect(payload.sessions.paths.length).toBe(2);
expect( expect(

View File

@@ -19,11 +19,18 @@ vi.mock("./node-llama.js", () => ({
})); }));
const createFetchMock = () => const createFetchMock = () =>
vi.fn(async () => ({ vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true, ok: true,
status: 200, status: 200,
json: async () => ({ data: [{ embedding: [1, 2, 3] }] }), json: async () => ({ data: [{ embedding: [1, 2, 3] }] }),
})) as unknown as typeof fetch; }));
function requireProvider(result: Awaited<ReturnType<typeof createEmbeddingProvider>>) {
if (!result.provider) {
throw new Error("Expected embedding provider");
}
return result.provider;
}
describe("embedding provider remote overrides", () => { describe("embedding provider remote overrides", () => {
afterEach(() => { afterEach(() => {
@@ -69,10 +76,12 @@ describe("embedding provider remote overrides", () => {
fallback: "openai", fallback: "openai",
}); });
await result.provider.embedQuery("hello"); const provider = requireProvider(result);
await provider.embedQuery("hello");
expect(authModule.resolveApiKeyForProvider).not.toHaveBeenCalled(); expect(authModule.resolveApiKeyForProvider).not.toHaveBeenCalled();
const [url, init] = fetchMock.mock.calls[0] ?? []; const url = fetchMock.mock.calls[0]?.[0];
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(url).toBe("https://remote.example/v1/embeddings"); expect(url).toBe("https://remote.example/v1/embeddings");
const headers = (init?.headers ?? {}) as Record<string, string>; const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBe("Bearer remote-key"); expect(headers.Authorization).toBe("Bearer remote-key");
@@ -112,19 +121,21 @@ describe("embedding provider remote overrides", () => {
fallback: "openai", fallback: "openai",
}); });
await result.provider.embedQuery("hello"); const provider = requireProvider(result);
await provider.embedQuery("hello");
expect(authModule.resolveApiKeyForProvider).toHaveBeenCalledTimes(1); expect(authModule.resolveApiKeyForProvider).toHaveBeenCalledTimes(1);
const headers = (fetchMock.mock.calls[0]?.[1]?.headers as Record<string, string>) ?? {}; const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
const headers = (init?.headers as Record<string, string>) ?? {};
expect(headers.Authorization).toBe("Bearer provider-key"); expect(headers.Authorization).toBe("Bearer provider-key");
}); });
it("builds Gemini embeddings requests with api key header", async () => { it("builds Gemini embeddings requests with api key header", async () => {
const fetchMock = vi.fn(async () => ({ const fetchMock = vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true, ok: true,
status: 200, status: 200,
json: async () => ({ embedding: { values: [1, 2, 3] } }), json: async () => ({ embedding: { values: [1, 2, 3] } }),
})) as unknown as typeof fetch; }));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({ vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
apiKey: "provider-key", apiKey: "provider-key",
@@ -152,9 +163,11 @@ describe("embedding provider remote overrides", () => {
fallback: "openai", fallback: "openai",
}); });
await result.provider.embedQuery("hello"); const provider = requireProvider(result);
await provider.embedQuery("hello");
const [url, init] = fetchMock.mock.calls[0] ?? []; const url = fetchMock.mock.calls[0]?.[0];
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(url).toBe( expect(url).toBe(
"https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent", "https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent",
); );
@@ -186,15 +199,16 @@ describe("embedding provider auto selection", () => {
}); });
expect(result.requestedProvider).toBe("auto"); expect(result.requestedProvider).toBe("auto");
expect(result.provider.id).toBe("openai"); const provider = requireProvider(result);
expect(provider.id).toBe("openai");
}); });
it("uses gemini when openai is missing", async () => { it("uses gemini when openai is missing", async () => {
const fetchMock = vi.fn(async () => ({ const fetchMock = vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true, ok: true,
status: 200, status: 200,
json: async () => ({ embedding: { values: [1, 2, 3] } }), json: async () => ({ embedding: { values: [1, 2, 3] } }),
})) as unknown as typeof fetch; }));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => { vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "openai") { if (provider === "openai") {
@@ -214,8 +228,9 @@ describe("embedding provider auto selection", () => {
}); });
expect(result.requestedProvider).toBe("auto"); expect(result.requestedProvider).toBe("auto");
expect(result.provider.id).toBe("gemini"); const provider = requireProvider(result);
await result.provider.embedQuery("hello"); expect(provider.id).toBe("gemini");
await provider.embedQuery("hello");
const [url] = fetchMock.mock.calls[0] ?? []; const [url] = fetchMock.mock.calls[0] ?? [];
expect(url).toBe( expect(url).toBe(
`https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_GEMINI_EMBEDDING_MODEL}:embedContent`, `https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_GEMINI_EMBEDDING_MODEL}:embedContent`,
@@ -223,11 +238,11 @@ describe("embedding provider auto selection", () => {
}); });
it("keeps explicit model when openai is selected", async () => { it("keeps explicit model when openai is selected", async () => {
const fetchMock = vi.fn(async () => ({ const fetchMock = vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true, ok: true,
status: 200, status: 200,
json: async () => ({ data: [{ embedding: [1, 2, 3] }] }), json: async () => ({ data: [{ embedding: [1, 2, 3] }] }),
})) as unknown as typeof fetch; }));
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => { vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
if (provider === "openai") { if (provider === "openai") {
@@ -244,11 +259,13 @@ describe("embedding provider auto selection", () => {
}); });
expect(result.requestedProvider).toBe("auto"); expect(result.requestedProvider).toBe("auto");
expect(result.provider.id).toBe("openai"); const provider = requireProvider(result);
await result.provider.embedQuery("hello"); expect(provider.id).toBe("openai");
const [url, init] = fetchMock.mock.calls[0] ?? []; await provider.embedQuery("hello");
const url = fetchMock.mock.calls[0]?.[0];
const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
expect(url).toBe("https://api.openai.com/v1/embeddings"); expect(url).toBe("https://api.openai.com/v1/embeddings");
const payload = JSON.parse(String(init?.body ?? "{}")) as { model?: string }; const payload = JSON.parse(init?.body as string) as { model?: string };
expect(payload.model).toBe("text-embedding-3-small"); expect(payload.model).toBe("text-embedding-3-small");
}); });
}); });
@@ -282,7 +299,8 @@ describe("embedding provider local fallback", () => {
fallback: "openai", fallback: "openai",
}); });
expect(result.provider.id).toBe("openai"); const provider = requireProvider(result);
expect(provider.id).toBe("openai");
expect(result.fallbackFrom).toBe("local"); expect(result.fallbackFrom).toBe("local");
expect(result.fallbackReason).toContain("node-llama-cpp"); expect(result.fallbackReason).toContain("node-llama-cpp");
}); });
@@ -365,7 +383,8 @@ describe("local embedding normalization", () => {
const result = await createLocalProviderForTest(); const result = await createLocalProviderForTest();
const embedding = await result.provider.embedQuery("test query"); const provider = requireProvider(result);
const embedding = await provider.embedQuery("test query");
const magnitude = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0)); const magnitude = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0));
@@ -380,7 +399,8 @@ describe("local embedding normalization", () => {
const result = await createLocalProviderForTest(); const result = await createLocalProviderForTest();
const embedding = await result.provider.embedQuery("test"); const provider = requireProvider(result);
const embedding = await provider.embedQuery("test");
expect(embedding).toEqual([0, 0, 0, 0]); expect(embedding).toEqual([0, 0, 0, 0]);
expect(embedding.every((value) => Number.isFinite(value))).toBe(true); expect(embedding.every((value) => Number.isFinite(value))).toBe(true);
@@ -393,7 +413,8 @@ describe("local embedding normalization", () => {
const result = await createLocalProviderForTest(); const result = await createLocalProviderForTest();
const embedding = await result.provider.embedQuery("test"); const provider = requireProvider(result);
const embedding = await provider.embedQuery("test");
expect(embedding).toEqual([1, 0, 0, 0]); expect(embedding).toEqual([1, 0, 0, 0]);
expect(embedding.every((value) => Number.isFinite(value))).toBe(true); expect(embedding.every((value) => Number.isFinite(value))).toBe(true);
@@ -424,7 +445,8 @@ describe("local embedding normalization", () => {
const result = await createLocalProviderForTest(); const result = await createLocalProviderForTest();
const embeddings = await result.provider.embedBatch(["text1", "text2", "text3"]); const provider = requireProvider(result);
const embeddings = await provider.embedBatch(["text1", "text2", "text3"]);
for (const embedding of embeddings) { for (const embedding of embeddings) {
const magnitude = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0)); const magnitude = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0));

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { Mock } from "vitest";
const { logWarnMock, logDebugMock, logInfoMock } = vi.hoisted(() => ({ const { logWarnMock, logDebugMock, logInfoMock } = vi.hoisted(() => ({
logWarnMock: vi.fn(), logWarnMock: vi.fn(),
@@ -68,7 +69,7 @@ vi.mock("../logging/subsystem.js", () => ({
}, },
})); }));
vi.mock(import("node:child_process"), async (importOriginal) => { vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>(); const actual = await importOriginal<typeof import("node:child_process")>();
return { return {
...actual, ...actual,
@@ -81,7 +82,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveMemoryBackendConfig } from "./backend-config.js"; import { resolveMemoryBackendConfig } from "./backend-config.js";
import { QmdMemoryManager } from "./qmd-manager.js"; import { QmdMemoryManager } from "./qmd-manager.js";
const spawnMock = mockedSpawn as unknown as vi.Mock; const spawnMock = mockedSpawn as unknown as Mock;
describe("QmdMemoryManager", () => { describe("QmdMemoryManager", () => {
let fixtureRoot: string; let fixtureRoot: string;
@@ -195,7 +196,7 @@ describe("QmdMemoryManager", () => {
const { manager } = await createManager({ mode: "full" }); const { manager } = await createManager({ mode: "full" });
expect(releaseUpdate).not.toBeNull(); expect(releaseUpdate).not.toBeNull();
releaseUpdate?.(); (releaseUpdate as (() => void) | null)?.();
await manager?.close(); await manager?.close();
}); });
@@ -256,7 +257,7 @@ describe("QmdMemoryManager", () => {
}); });
await new Promise<void>((resolve) => setImmediate(resolve)); await new Promise<void>((resolve) => setImmediate(resolve));
expect(created).toBe(false); expect(created).toBe(false);
releaseUpdate?.(); (releaseUpdate as (() => void) | null)?.();
const manager = await createPromise; const manager = await createPromise;
await manager?.close(); await manager?.close();
}); });
@@ -340,7 +341,7 @@ describe("QmdMemoryManager", () => {
expect(manager).toBeTruthy(); expect(manager).toBeTruthy();
await manager?.close(); await manager?.close();
const commands = spawnMock.mock.calls.map((call) => call[1] as string[]); const commands = spawnMock.mock.calls.map((call: unknown[]) => call[1] as string[]);
const removeSessions = commands.find( const removeSessions = commands.find(
(args) => (args) =>
args[0] === "collection" && args[1] === "remove" && args[2] === sessionCollectionName, args[0] === "collection" && args[1] === "remove" && args[2] === sessionCollectionName,
@@ -389,7 +390,7 @@ describe("QmdMemoryManager", () => {
const { manager } = await createManager({ mode: "full" }); const { manager } = await createManager({ mode: "full" });
await manager.close(); await manager.close();
const commands = spawnMock.mock.calls.map((call) => call[1] as string[]); const commands = spawnMock.mock.calls.map((call: unknown[]) => call[1] as string[]);
const removeSessions = commands.find( const removeSessions = commands.find(
(args) => (args) =>
args[0] === "collection" && args[1] === "remove" && args[2] === sessionCollectionName, args[0] === "collection" && args[1] === "remove" && args[2] === sessionCollectionName,
@@ -485,12 +486,12 @@ describe("QmdMemoryManager", () => {
await expect(manager.sync({ reason: "manual" })).resolves.toBeUndefined(); await expect(manager.sync({ reason: "manual" })).resolves.toBeUndefined();
const removeCalls = spawnMock.mock.calls const removeCalls = spawnMock.mock.calls
.map((call) => call[1] as string[]) .map((call: unknown[]) => call[1] as string[])
.filter((args) => args[0] === "collection" && args[1] === "remove") .filter((args: string[]) => args[0] === "collection" && args[1] === "remove")
.map((args) => args[2]); .map((args) => args[2]);
const addCalls = spawnMock.mock.calls const addCalls = spawnMock.mock.calls
.map((call) => call[1] as string[]) .map((call: unknown[]) => call[1] as string[])
.filter((args) => args[0] === "collection" && args[1] === "add") .filter((args: string[]) => args[0] === "collection" && args[1] === "add")
.map((args) => args[args.indexOf("--name") + 1]); .map((args) => args[args.indexOf("--name") + 1]);
expect(updateCalls).toBe(2); expect(updateCalls).toBe(2);
@@ -536,8 +537,8 @@ describe("QmdMemoryManager", () => {
); );
const removeCalls = spawnMock.mock.calls const removeCalls = spawnMock.mock.calls
.map((call) => call[1] as string[]) .map((call: unknown[]) => call[1] as string[])
.filter((args) => args[0] === "collection" && args[1] === "remove"); .filter((args: string[]) => args[0] === "collection" && args[1] === "remove");
expect(removeCalls).toHaveLength(0); expect(removeCalls).toHaveLength(0);
await manager.close(); await manager.close();
@@ -575,7 +576,9 @@ describe("QmdMemoryManager", () => {
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }), manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
).resolves.toEqual([]); ).resolves.toEqual([]);
const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search"); const searchCall = spawnMock.mock.calls.find(
(call: unknown[]) => (call[1] as string[])?.[0] === "search",
);
expect(searchCall?.[1]).toEqual([ expect(searchCall?.[1]).toEqual([
"search", "search",
"test", "test",
@@ -585,7 +588,9 @@ describe("QmdMemoryManager", () => {
"-c", "-c",
"workspace-main", "workspace-main",
]); ]);
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false); expect(
spawnMock.mock.calls.some((call: unknown[]) => (call[1] as string[])?.[0] === "query"),
).toBe(false);
expect(maxResults).toBeGreaterThan(0); expect(maxResults).toBeGreaterThan(0);
await manager.close(); await manager.close();
}); });
@@ -628,7 +633,7 @@ describe("QmdMemoryManager", () => {
).resolves.toEqual([]); ).resolves.toEqual([]);
const searchAndQueryCalls = spawnMock.mock.calls const searchAndQueryCalls = spawnMock.mock.calls
.map((call) => call[1]) .map((call: unknown[]) => call[1])
.filter( .filter(
(args): args is string[] => Array.isArray(args) && ["search", "query"].includes(args[0]), (args): args is string[] => Array.isArray(args) && ["search", "query"].includes(args[0]),
); );
@@ -684,7 +689,7 @@ describe("QmdMemoryManager", () => {
if (!releaseFirstUpdate) { if (!releaseFirstUpdate) {
throw new Error("first update release missing"); throw new Error("first update release missing");
} }
releaseFirstUpdate(); (releaseFirstUpdate as () => void)();
await Promise.all([inFlight, forced]); await Promise.all([inFlight, forced]);
expect(updateCalls).toBe(2); expect(updateCalls).toBe(2);
@@ -744,7 +749,7 @@ describe("QmdMemoryManager", () => {
if (!releaseFirstUpdate) { if (!releaseFirstUpdate) {
throw new Error("first update release missing"); throw new Error("first update release missing");
} }
releaseFirstUpdate(); (releaseFirstUpdate as () => void)();
await secondUpdateSpawned.promise; await secondUpdateSpawned.promise;
const forcedTwo = manager.sync({ reason: "manual-again", force: true }); const forcedTwo = manager.sync({ reason: "manual-again", force: true });
@@ -752,7 +757,7 @@ describe("QmdMemoryManager", () => {
if (!releaseSecondUpdate) { if (!releaseSecondUpdate) {
throw new Error("second update release missing"); throw new Error("second update release missing");
} }
releaseSecondUpdate(); (releaseSecondUpdate as () => void)();
await Promise.all([inFlight, forcedOne, forcedTwo]); await Promise.all([inFlight, forcedOne, forcedTwo]);
expect(updateCalls).toBe(3); expect(updateCalls).toBe(3);
@@ -787,7 +792,9 @@ describe("QmdMemoryManager", () => {
const { manager, resolved } = await createManager(); const { manager, resolved } = await createManager();
await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }); await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search"); const searchCall = spawnMock.mock.calls.find(
(call: unknown[]) => (call[1] as string[])?.[0] === "search",
);
const maxResults = resolved.qmd?.limits.maxResults; const maxResults = resolved.qmd?.limits.maxResults;
if (!maxResults) { if (!maxResults) {
throw new Error("qmd maxResults missing"); throw new Error("qmd maxResults missing");
@@ -843,8 +850,8 @@ describe("QmdMemoryManager", () => {
).resolves.toEqual([]); ).resolves.toEqual([]);
const queryCalls = spawnMock.mock.calls const queryCalls = spawnMock.mock.calls
.map((call) => call[1] as string[]) .map((call: unknown[]) => call[1] as string[])
.filter((args) => args[0] === "query"); .filter((args: string[]) => args[0] === "query");
expect(queryCalls).toEqual([ expect(queryCalls).toEqual([
["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"], ["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"], ["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
@@ -894,8 +901,8 @@ describe("QmdMemoryManager", () => {
).resolves.toEqual([]); ).resolves.toEqual([]);
const searchAndQueryCalls = spawnMock.mock.calls const searchAndQueryCalls = spawnMock.mock.calls
.map((call) => call[1] as string[]) .map((call: unknown[]) => call[1] as string[])
.filter((args) => args[0] === "search" || args[0] === "query"); .filter((args: string[]) => args[0] === "search" || args[0] === "query");
expect(searchAndQueryCalls).toEqual([ expect(searchAndQueryCalls).toEqual([
[ [
"search", "search",
@@ -931,7 +938,9 @@ describe("QmdMemoryManager", () => {
const results = await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }); const results = await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
expect(results).toEqual([]); expect(results).toEqual([]);
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false); expect(
spawnMock.mock.calls.some((call: unknown[]) => (call[1] as string[])?.[0] === "query"),
).toBe(false);
await manager.close(); await manager.close();
}); });
@@ -1081,12 +1090,13 @@ describe("QmdMemoryManager", () => {
"utf-8", "utf-8",
); );
const currentMemory = cfg.memory;
cfg = { cfg = {
...cfg, ...cfg,
memory: { memory: {
...cfg.memory, ...currentMemory,
qmd: { qmd: {
...cfg.memory.qmd, ...currentMemory?.qmd,
sessions: { sessions: {
enabled: true, enabled: true,
}, },

View File

@@ -1,19 +1,26 @@
import type { TUI } from "@mariozechner/pi-tui";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { ChatLog } from "./components/chat-log.js";
import { createEventHandlers } from "./tui-event-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js";
import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js";
type MockChatLog = Pick< type MockFn = ReturnType<typeof vi.fn>;
ChatLog, type HandlerChatLog = {
| "startTool" startTool: (...args: unknown[]) => void;
| "updateToolResult" updateToolResult: (...args: unknown[]) => void;
| "addSystem" addSystem: (...args: unknown[]) => void;
| "updateAssistant" updateAssistant: (...args: unknown[]) => void;
| "finalizeAssistant" finalizeAssistant: (...args: unknown[]) => void;
| "dropAssistant" dropAssistant: (...args: unknown[]) => void;
>; };
type MockTui = Pick<TUI, "requestRender">; type HandlerTui = { requestRender: (...args: unknown[]) => void };
type MockChatLog = {
startTool: MockFn;
updateToolResult: MockFn;
addSystem: MockFn;
updateAssistant: MockFn;
finalizeAssistant: MockFn;
dropAssistant: MockFn;
};
type MockTui = { requestRender: MockFn };
describe("tui-event-handlers: handleAgentEvent", () => { describe("tui-event-handlers: handleAgentEvent", () => {
const makeState = (overrides?: Partial<TuiStateAccess>): TuiStateAccess => ({ const makeState = (overrides?: Partial<TuiStateAccess>): TuiStateAccess => ({
@@ -40,15 +47,15 @@ describe("tui-event-handlers: handleAgentEvent", () => {
}); });
const makeContext = (state: TuiStateAccess) => { const makeContext = (state: TuiStateAccess) => {
const chatLog: MockChatLog = { const chatLog = {
startTool: vi.fn(), startTool: vi.fn(),
updateToolResult: vi.fn(), updateToolResult: vi.fn(),
addSystem: vi.fn(), addSystem: vi.fn(),
updateAssistant: vi.fn(), updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(), finalizeAssistant: vi.fn(),
dropAssistant: vi.fn(), dropAssistant: vi.fn(),
}; } as unknown as MockChatLog & HandlerChatLog;
const tui: MockTui = { requestRender: vi.fn() }; const tui = { requestRender: vi.fn() } as unknown as MockTui & HandlerTui;
const setActivityStatus = vi.fn(); const setActivityStatus = vi.fn();
const loadHistory = vi.fn(); const loadHistory = vi.fn();
const localRunIds = new Set<string>(); const localRunIds = new Set<string>();
@@ -136,7 +143,8 @@ describe("tui-event-handlers: handleAgentEvent", () => {
addSystem: vi.fn(), addSystem: vi.fn(),
updateAssistant: vi.fn(), updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(), finalizeAssistant: vi.fn(),
}, dropAssistant: vi.fn(),
} as unknown as HandlerChatLog,
tui, tui,
state, state,
setActivityStatus, setActivityStatus,

View File

@@ -1,12 +1,17 @@
import type { TUI } from "@mariozechner/pi-tui";
import type { ChatLog } from "./components/chat-log.js";
import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js"; import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js";
import { TuiStreamAssembler } from "./tui-stream-assembler.js"; import { TuiStreamAssembler } from "./tui-stream-assembler.js";
import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js";
type EventHandlerContext = { type EventHandlerContext = {
chatLog: ChatLog; chatLog: {
tui: TUI; startTool: (...args: unknown[]) => void;
updateToolResult: (...args: unknown[]) => void;
addSystem: (...args: unknown[]) => void;
updateAssistant: (...args: unknown[]) => void;
finalizeAssistant: (...args: unknown[]) => void;
dropAssistant: (...args: unknown[]) => void;
};
tui: { requestRender: (...args: unknown[]) => void };
state: TuiStateAccess; state: TuiStateAccess;
setActivityStatus: (text: string) => void; setActivityStatus: (text: string) => void;
refreshSessionInfo?: () => Promise<void>; refreshSessionInfo?: () => Promise<void>;