2026-08-30 23:26:59 +07:00
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
import {
|
2026-08-31 12:44:36 +07:00
|
|
|
__resetVideoRecorderState,
|
2026-08-30 23:26:59 +07:00
|
|
|
setVideoRecorderClient,
|
|
|
|
|
setVideoRecordingsDir,
|
|
|
|
|
startVideoRecording,
|
|
|
|
|
stopVideoRecording,
|
|
|
|
|
trackChannel,
|
|
|
|
|
untrackChannel,
|
|
|
|
|
} from "../src/modules/voice-recording/videoRecorder.js";
|
2026-08-31 13:46:24 +07:00
|
|
|
import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js";
|
2026-08-30 23:26:59 +07:00
|
|
|
|
2026-08-31 13:46:24 +07:00
|
|
|
// ─── mocks ─────────────────────────────────────────────────────────────
|
2026-08-30 23:26:59 +07:00
|
|
|
function makeChannel(guildId = "g1", channelId = "c1") {
|
|
|
|
|
return {
|
|
|
|
|
id: channelId,
|
|
|
|
|
guild: { id: guildId },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeClient() {
|
|
|
|
|
return {
|
|
|
|
|
user: { id: "bot1" },
|
|
|
|
|
on: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let seq = 0;
|
|
|
|
|
function makeUser() {
|
|
|
|
|
seq += 1;
|
|
|
|
|
return `user-${seq}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2026-08-31 12:44:36 +07:00
|
|
|
__resetVideoRecorderState();
|
2026-08-31 13:46:24 +07:00
|
|
|
vi.restoreAllMocks();
|
2026-08-30 23:26:59 +07:00
|
|
|
setVideoRecordingsDir("/tmp/gmw-vidrec-test");
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-31 13:46:24 +07:00
|
|
|
describe("videoRecorder (stream-watch orchestration)", () => {
|
|
|
|
|
it("registers a voiceStateUpdate + raw listener on the client (idempotent)", () => {
|
2026-08-30 23:26:59 +07:00
|
|
|
const client = makeClient();
|
|
|
|
|
setVideoRecorderClient(client);
|
2026-08-31 13:46:24 +07:00
|
|
|
setVideoRecorderClient(client); // idempotent (videoRecorder + streamWatch)
|
|
|
|
|
// one raw (streamWatch) + one voiceStateUpdate (videoRecorder)
|
|
|
|
|
expect(client.on).toHaveBeenCalledTimes(2);
|
2026-08-30 23:26:59 +07:00
|
|
|
expect(client.on).toHaveBeenCalledWith(
|
|
|
|
|
"voiceStateUpdate",
|
|
|
|
|
expect.any(Function),
|
|
|
|
|
);
|
2026-08-31 13:46:24 +07:00
|
|
|
expect(client.on).toHaveBeenCalledWith("raw", expect.any(Function));
|
2026-08-30 23:26:59 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-31 13:46:24 +07:00
|
|
|
it("startVideoRecording delegates to streamWatchReceiver.startStreamWatch", () => {
|
|
|
|
|
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
|
|
|
|
const ch = makeChannel();
|
|
|
|
|
startVideoRecording(ch, makeUser());
|
|
|
|
|
expect(spy).toHaveBeenCalledWith(ch, expect.any(String));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("refuses to record the bot's own video", () => {
|
|
|
|
|
setVideoRecorderClient(makeClient());
|
|
|
|
|
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
|
|
|
|
startVideoRecording(makeChannel(), "bot1");
|
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stopVideoRecording calls streamWatchReceiver.stopStreamWatch", () => {
|
|
|
|
|
const spy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {});
|
|
|
|
|
const u = makeUser();
|
|
|
|
|
stopVideoRecording("g1", u);
|
|
|
|
|
expect(spy).toHaveBeenCalledWith("g1", u);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("untrackChannel tears down all stream watches for the guild", () => {
|
|
|
|
|
const spy = vi.spyOn(streamWatch, "stopAllStreamWatches").mockImplementation(() => {});
|
|
|
|
|
untrackChannel("g1");
|
|
|
|
|
expect(spy).toHaveBeenCalledWith("g1");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("the voiceStateUpdate handler starts a watch when a member starts streaming", () => {
|
|
|
|
|
const client = makeClient() as any;
|
2026-08-30 23:26:59 +07:00
|
|
|
setVideoRecorderClient(client);
|
|
|
|
|
trackChannel("g1", makeChannel());
|
2026-08-31 13:46:24 +07:00
|
|
|
const startSpy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
|
|
|
|
const listener = client.on.mock.calls.find(
|
|
|
|
|
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
|
|
|
|
)?.[1];
|
|
|
|
|
listener(null, { id: "u1", guild: { id: "g1" }, channelId: "c1", streaming: true });
|
|
|
|
|
expect(startSpy).toHaveBeenCalled();
|
2026-08-30 23:26:59 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-31 13:46:24 +07:00
|
|
|
it("the voiceStateUpdate handler stops a watch when a member stops streaming", () => {
|
|
|
|
|
const client = makeClient() as any;
|
2026-08-30 23:26:59 +07:00
|
|
|
setVideoRecorderClient(client);
|
2026-08-31 13:46:24 +07:00
|
|
|
trackChannel("g1", makeChannel());
|
|
|
|
|
const stopSpy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {});
|
|
|
|
|
const listener = client.on.mock.calls.find(
|
|
|
|
|
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
|
|
|
|
)?.[1];
|
|
|
|
|
listener(null, { id: "u1", guild: { id: "g1" }, channelId: "c1", streaming: false });
|
|
|
|
|
expect(stopSpy).toHaveBeenCalledWith("g1", "u1");
|
2026-08-31 12:44:36 +07:00
|
|
|
});
|
2026-08-30 23:26:59 +07:00
|
|
|
});
|