mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 12:00:43 +00:00
test: dedupe voicewake and target helper coverage
This commit is contained in:
@@ -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();
|
||||
|
||||
63
src/infra/outbound/channel-target.test.ts
Normal file
63
src/infra/outbound/channel-target.test.ts
Normal 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: " " });
|
||||
});
|
||||
});
|
||||
39
src/infra/outbound/target-errors.test.ts
Normal file
39
src/infra/outbound/target-errors.test.ts
Normal 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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
30
src/infra/pairing-token.test.ts
Normal file
30
src/infra/pairing-token.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
55
src/infra/voicewake.test.ts
Normal file
55
src/infra/voicewake.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user