openclaw/extensions/discord/src/voice-message.test.ts
scoootscooob 5682ec37fa
refactor: move Discord channel implementation to extensions/ (#45660)
* refactor: move Discord channel implementation to extensions/discord/src/

Move all Discord source files from src/discord/ to extensions/discord/src/,
following the extension migration pattern. Source files in src/discord/ are
replaced with re-export shims. Channel-plugin files from
src/channels/plugins/*/discord* are similarly moved and shimmed.

- Copy all .ts source files preserving subdirectory structure (monitor/, voice/)
- Move channel-plugin files (actions, normalize, onboarding, outbound, status-issues)
- Fix all relative imports to use correct paths from new location
- Create re-export shims at original locations for backward compatibility
- Delete test files from shim locations (tests live in extension now)
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." to accommodate
  extension files outside src/
- Update write-plugin-sdk-entry-dts.ts to match new declaration output paths

* fix: add importOriginal to thread-bindings session-meta mock for extensions test

* style: fix formatting in thread-bindings lifecycle test
2026-03-14 02:53:57 -07:00

147 lines
4.6 KiB
TypeScript

import type { ChildProcess, ExecFileOptions } from "node:child_process";
import { promisify } from "node:util";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
type ExecCallback = (
error: NodeJS.ErrnoException | null,
stdout: string | Buffer,
stderr: string | Buffer,
) => void;
type ExecCall = {
command: string;
args: string[];
options?: ExecFileOptions;
};
type MockExecResult = {
stdout?: string;
stderr?: string;
error?: NodeJS.ErrnoException;
};
const execCalls: ExecCall[] = [];
const mockExecResults: MockExecResult[] = [];
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
const execFileImpl = (
file: string,
args?: readonly string[] | null,
optionsOrCallback?: ExecFileOptions | ExecCallback | null,
callbackMaybe?: ExecCallback,
) => {
const normalizedArgs = Array.isArray(args) ? [...args] : [];
const callback =
typeof optionsOrCallback === "function" ? optionsOrCallback : (callbackMaybe ?? undefined);
const options =
typeof optionsOrCallback === "function" ? undefined : (optionsOrCallback ?? undefined);
execCalls.push({
command: file,
args: normalizedArgs,
options,
});
const next = mockExecResults.shift() ?? { stdout: "", stderr: "" };
queueMicrotask(() => {
callback?.(next.error ?? null, next.stdout ?? "", next.stderr ?? "");
});
return {} as ChildProcess;
};
const execFileWithCustomPromisify = execFileImpl as unknown as typeof actual.execFile & {
[promisify.custom]?: (
file: string,
args?: readonly string[] | null,
options?: ExecFileOptions | null,
) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>;
};
execFileWithCustomPromisify[promisify.custom] = (
file: string,
args?: readonly string[] | null,
options?: ExecFileOptions | null,
) =>
new Promise<{ stdout: string | Buffer; stderr: string | Buffer }>((resolve, reject) => {
execFileImpl(file, args, options, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
return {
...actual,
execFile: execFileWithCustomPromisify,
};
});
vi.mock("../../../src/infra/tmp-openclaw-dir.js", () => ({
resolvePreferredOpenClawTmpDir: () => "/tmp",
}));
const { ensureOggOpus } = await import("./voice-message.js");
describe("ensureOggOpus", () => {
beforeEach(() => {
execCalls.length = 0;
mockExecResults.length = 0;
});
afterEach(() => {
execCalls.length = 0;
mockExecResults.length = 0;
});
it("rejects URL/protocol input paths", async () => {
await expect(ensureOggOpus("https://example.com/audio.ogg")).rejects.toThrow(
/local file path/i,
);
expect(execCalls).toHaveLength(0);
});
it("keeps .ogg only when codec is opus and sample rate is 48kHz", async () => {
mockExecResults.push({ stdout: "opus,48000\n" });
const result = await ensureOggOpus("/tmp/input.ogg");
expect(result).toEqual({ path: "/tmp/input.ogg", cleanup: false });
expect(execCalls).toHaveLength(1);
expect(execCalls[0].command).toBe("ffprobe");
expect(execCalls[0].args).toContain("stream=codec_name,sample_rate");
expect(execCalls[0].options?.timeout).toBe(10_000);
});
it("re-encodes .ogg opus when sample rate is not 48kHz", async () => {
mockExecResults.push({ stdout: "opus,24000\n" });
mockExecResults.push({ stdout: "" });
const result = await ensureOggOpus("/tmp/input.ogg");
const ffmpegCall = execCalls.find((call) => call.command === "ffmpeg");
expect(result.cleanup).toBe(true);
expect(result.path).toMatch(/^\/tmp\/voice-.*\.ogg$/);
expect(ffmpegCall).toBeDefined();
expect(ffmpegCall?.args).toContain("-t");
expect(ffmpegCall?.args).toContain("1200");
expect(ffmpegCall?.args).toContain("-ar");
expect(ffmpegCall?.args).toContain("48000");
expect(ffmpegCall?.options?.timeout).toBe(45_000);
});
it("re-encodes non-ogg input with bounded ffmpeg execution", async () => {
mockExecResults.push({ stdout: "" });
const result = await ensureOggOpus("/tmp/input.mp3");
const ffprobeCalls = execCalls.filter((call) => call.command === "ffprobe");
const ffmpegCalls = execCalls.filter((call) => call.command === "ffmpeg");
expect(result.cleanup).toBe(true);
expect(ffprobeCalls).toHaveLength(0);
expect(ffmpegCalls).toHaveLength(1);
expect(ffmpegCalls[0].options?.timeout).toBe(45_000);
expect(ffmpegCalls[0].args).toEqual(expect.arrayContaining(["-vn", "-sn", "-dn"]));
});
});