mirror of https://github.com/openclaw/openclaw.git
test: dedupe voicewake and target helper coverage
This commit is contained in:
parent
6d159a45a8
commit
ddfa6e66c8
|
|
@ -9,11 +9,6 @@ import {
|
||||||
resetDiagnosticEventsForTest,
|
resetDiagnosticEventsForTest,
|
||||||
} from "./diagnostic-events.js";
|
} from "./diagnostic-events.js";
|
||||||
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
|
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
|
||||||
import {
|
|
||||||
defaultVoiceWakeTriggers,
|
|
||||||
loadVoiceWakeConfig,
|
|
||||||
setVoiceWakeTriggers,
|
|
||||||
} from "./voicewake.js";
|
|
||||||
|
|
||||||
describe("infra store", () => {
|
describe("infra store", () => {
|
||||||
describe("state migrations fs", () => {
|
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", () => {
|
describe("diagnostic-events", () => {
|
||||||
it("emits monotonic seq", async () => {
|
it("emits monotonic seq", async () => {
|
||||||
resetDiagnosticEventsForTest();
|
resetDiagnosticEventsForTest();
|
||||||
|
|
|
||||||
|
|
@ -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: " " });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue