test: dedupe voicewake and target helper coverage

This commit is contained in:
Peter Steinberger
2026-03-13 20:00:43 +00:00
parent 6d159a45a8
commit ddfa6e66c8
5 changed files with 187 additions and 52 deletions

View File

@@ -9,11 +9,6 @@ import {
resetDiagnosticEventsForTest,
} from "./diagnostic-events.js";
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
import {
defaultVoiceWakeTriggers,
loadVoiceWakeConfig,
setVoiceWakeTriggers,
} from "./voicewake.js";
describe("infra store", () => {
describe("state migrations fs", () => {
@@ -45,53 +40,6 @@ describe("infra store", () => {
});
});
describe("voicewake store", () => {
it("returns defaults when missing", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
const cfg = await loadVoiceWakeConfig(baseDir);
expect(cfg.triggers).toEqual(defaultVoiceWakeTriggers());
expect(cfg.updatedAtMs).toBe(0);
});
});
it("sanitizes and persists triggers", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
const saved = await setVoiceWakeTriggers([" hi ", "", " there "], baseDir);
expect(saved.triggers).toEqual(["hi", "there"]);
expect(saved.updatedAtMs).toBeGreaterThan(0);
const loaded = await loadVoiceWakeConfig(baseDir);
expect(loaded.triggers).toEqual(["hi", "there"]);
expect(loaded.updatedAtMs).toBeGreaterThan(0);
});
});
it("falls back to defaults when triggers empty", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
const saved = await setVoiceWakeTriggers(["", " "], baseDir);
expect(saved.triggers).toEqual(defaultVoiceWakeTriggers());
});
});
it("sanitizes malformed persisted config values", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
await fs.mkdir(path.join(baseDir, "settings"), { recursive: true });
await fs.writeFile(
path.join(baseDir, "settings", "voicewake.json"),
JSON.stringify({
triggers: [" wake ", "", 42, null],
updatedAtMs: -1,
}),
"utf-8",
);
const loaded = await loadVoiceWakeConfig(baseDir);
expect(loaded.triggers).toEqual(["wake"]);
expect(loaded.updatedAtMs).toBe(0);
});
});
});
describe("diagnostic-events", () => {
it("emits monotonic seq", async () => {
resetDiagnosticEventsForTest();

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { applyTargetToParams } from "./channel-target.js";
describe("applyTargetToParams", () => {
it("maps trimmed target values into the configured target field", () => {
const toParams = {
action: "send",
args: { target: " channel:C1 " } as Record<string, unknown>,
};
applyTargetToParams(toParams);
expect(toParams.args.to).toBe("channel:C1");
const channelIdParams = {
action: "channel-info",
args: { target: " C123 " } as Record<string, unknown>,
};
applyTargetToParams(channelIdParams);
expect(channelIdParams.args.channelId).toBe("C123");
});
it("throws on legacy destination fields when the action has canonical target support", () => {
expect(() =>
applyTargetToParams({
action: "send",
args: {
target: "channel:C1",
to: "legacy",
},
}),
).toThrow("Use `target` instead of `to`/`channelId`.");
});
it("throws when a no-target action receives target or legacy destination fields", () => {
expect(() =>
applyTargetToParams({
action: "broadcast",
args: {
to: "legacy",
},
}),
).toThrow("Use `target` for actions that accept a destination.");
expect(() =>
applyTargetToParams({
action: "broadcast",
args: {
target: "channel:C1",
},
}),
).toThrow("Action broadcast does not accept a target.");
});
it("does nothing when target is blank", () => {
const params = {
action: "send",
args: { target: " " } as Record<string, unknown>,
};
applyTargetToParams(params);
expect(params.args).toEqual({ target: " " });
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import {
ambiguousTargetError,
ambiguousTargetMessage,
missingTargetError,
missingTargetMessage,
unknownTargetError,
unknownTargetMessage,
} from "./target-errors.js";
describe("target error helpers", () => {
it("formats missing-target messages with and without hints", () => {
expect(missingTargetMessage("Slack")).toBe("Delivering to Slack requires target");
expect(missingTargetMessage("Slack", "Use channel:C123")).toBe(
"Delivering to Slack requires target Use channel:C123",
);
expect(missingTargetError("Slack", "Use channel:C123").message).toBe(
"Delivering to Slack requires target Use channel:C123",
);
});
it("formats ambiguous and unknown target messages with labeled hints", () => {
expect(ambiguousTargetMessage("Discord", "general")).toBe(
'Ambiguous target "general" for Discord. Provide a unique name or an explicit id.',
);
expect(ambiguousTargetMessage("Discord", "general", "Use channel:123")).toBe(
'Ambiguous target "general" for Discord. Provide a unique name or an explicit id. Hint: Use channel:123',
);
expect(unknownTargetMessage("Discord", "general", "Use channel:123")).toBe(
'Unknown target "general" for Discord. Hint: Use channel:123',
);
expect(ambiguousTargetError("Discord", "general", "Use channel:123").message).toContain(
"Hint: Use channel:123",
);
expect(unknownTargetError("Discord", "general").message).toBe(
'Unknown target "general" for Discord.',
);
});
});

View File

@@ -0,0 +1,30 @@
import { Buffer } from "node:buffer";
import { describe, expect, it, vi } from "vitest";
const randomBytesMock = vi.hoisted(() => vi.fn());
vi.mock("node:crypto", async () => {
const actual = await vi.importActual<typeof import("node:crypto")>("node:crypto");
return {
...actual,
randomBytes: (...args: unknown[]) => randomBytesMock(...args),
};
});
import { generatePairingToken, PAIRING_TOKEN_BYTES, verifyPairingToken } from "./pairing-token.js";
describe("generatePairingToken", () => {
it("uses the configured byte count and returns a base64url token", () => {
randomBytesMock.mockReturnValueOnce(Buffer.from([0xfb, 0xff, 0x00]));
expect(generatePairingToken()).toBe("-_8A");
expect(randomBytesMock).toHaveBeenCalledWith(PAIRING_TOKEN_BYTES);
});
});
describe("verifyPairingToken", () => {
it("uses constant-time comparison semantics", () => {
expect(verifyPairingToken("secret-token", "secret-token")).toBe(true);
expect(verifyPairingToken("secret-token", "secret-tokEn")).toBe(false);
});
});

View File

@@ -0,0 +1,55 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
defaultVoiceWakeTriggers,
loadVoiceWakeConfig,
setVoiceWakeTriggers,
} from "./voicewake.js";
describe("voicewake config", () => {
it("returns defaults when missing", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
await expect(loadVoiceWakeConfig(baseDir)).resolves.toEqual({
triggers: defaultVoiceWakeTriggers(),
updatedAtMs: 0,
});
});
});
it("sanitizes and persists triggers", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
const saved = await setVoiceWakeTriggers([" hi ", "", " there "], baseDir);
expect(saved.triggers).toEqual(["hi", "there"]);
expect(saved.updatedAtMs).toBeGreaterThan(0);
await expect(loadVoiceWakeConfig(baseDir)).resolves.toEqual({
triggers: ["hi", "there"],
updatedAtMs: saved.updatedAtMs,
});
});
});
it("falls back to defaults for empty or malformed persisted values", async () => {
await withTempDir("openclaw-voicewake-", async (baseDir) => {
const emptySaved = await setVoiceWakeTriggers(["", " "], baseDir);
expect(emptySaved.triggers).toEqual(defaultVoiceWakeTriggers());
await fs.mkdir(path.join(baseDir, "settings"), { recursive: true });
await fs.writeFile(
path.join(baseDir, "settings", "voicewake.json"),
JSON.stringify({
triggers: [" wake ", "", 42, null],
updatedAtMs: -1,
}),
"utf8",
);
await expect(loadVoiceWakeConfig(baseDir)).resolves.toEqual({
triggers: ["wake"],
updatedAtMs: 0,
});
});
});
});