feat(config): add API route for app configuration and update related logic for monitor guild

This commit is contained in:
MythEclipse
2026-06-01 11:16:07 +07:00
parent 49e9197ce0
commit 8310e58239
10 changed files with 154 additions and 32 deletions
+24 -6
View File
@@ -8,6 +8,7 @@ import { mergeMessages, useMessages } from "./hooks/useMessages";
import { useMediaControl } from "./hooks/useMediaControl"; import { useMediaControl } from "./hooks/useMediaControl";
import { useUIState } from "./hooks/useUIState"; import { useUIState } from "./hooks/useUIState";
import { useVoiceControl } from "./hooks/useVoiceControl"; import { useVoiceControl } from "./hooks/useVoiceControl";
import { getAppConfig } from "./api/client";
import type { MessageRecord } from "./types/messages"; import type { MessageRecord } from "./types/messages";
import type { DashboardTab } from "./types/ui"; import type { DashboardTab } from "./types/ui";
import type { ActiveSpeaker } from "./types/voice"; import type { ActiveSpeaker } from "./types/voice";
@@ -47,6 +48,7 @@ export default function App() {
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [isStreaming, setIsStreaming] = useState(false); const [isStreaming, setIsStreaming] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem("admin-password")); const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem("admin-password"));
const [monitorGuildId, setMonitorGuildId] = useState("");
const audioContextListenRef = useRef<AudioContext | null>(null); const audioContextListenRef = useRef<AudioContext | null>(null);
const audioContextTransmitRef = useRef<AudioContext | null>(null); const audioContextTransmitRef = useRef<AudioContext | null>(null);
const streamRef = useRef<MediaStream | null>(null); const streamRef = useRef<MediaStream | null>(null);
@@ -56,10 +58,11 @@ export default function App() {
const activeTab = uiState.activeTab || "live"; const activeTab = uiState.activeTab || "live";
const selectedVoiceGuild = uiState.selectedVoiceGuild || uiState.selectedGuild || ""; const selectedVoiceGuild = uiState.selectedVoiceGuild || uiState.selectedGuild || "";
const selectedVoiceChannel = uiState.selectedVoiceChannel || ""; const selectedVoiceChannel = uiState.selectedVoiceChannel || "";
const selectedTextGuild = uiState.selectedTextGuild || uiState.selectedGuild || ""; const selectedTextGuild = monitorGuildId || uiState.selectedTextGuild || uiState.selectedGuild || "";
const selectedTextChannel = uiState.selectedTextChannel || ""; const selectedTextChannel = uiState.selectedTextChannel || "";
const selectedAnalyticsGuild = uiState.selectedAnalyticsGuild || uiState.selectedGuild || ""; const selectedAnalyticsGuild = monitorGuildId || uiState.selectedAnalyticsGuild || uiState.selectedGuild || "";
const selectedAnalyticsChannel = uiState.selectedAnalyticsChannel || ""; const selectedAnalyticsChannel = uiState.selectedAnalyticsChannel || "";
const monitorGuild = monitorGuildId ? voice.guilds.find((guild) => guild.id === monitorGuildId) : undefined;
const handleIncomingPcm = useCallback((data: ArrayBuffer) => { const handleIncomingPcm = useCallback((data: ArrayBuffer) => {
const headerView = new DataView(data, 0, 4); const headerView = new DataView(data, 0, 4);
@@ -91,6 +94,22 @@ export default function App() {
window.dispatchEvent(new CustomEvent("analytics_refresh")); window.dispatchEvent(new CustomEvent("analytics_refresh"));
}, []); }, []);
useEffect(() => {
getAppConfig()
.then((config) => {
if (config.monitorGuildId) {
setMonitorGuildId(config.monitorGuildId);
patchUIState({
selectedTextGuild: config.monitorGuildId,
selectedAnalyticsGuild: config.monitorGuildId,
selectedTextChannel: "",
selectedAnalyticsChannel: "",
});
}
})
.catch(() => undefined);
}, [patchUIState]);
const socket = useDashboardSocket({ const socket = useDashboardSocket({
onUIState: (state) => setUIState((prev) => ({ ...prev, ...state })), onUIState: (state) => setUIState((prev) => ({ ...prev, ...state })),
onUserState: setActiveSpeakers, onUserState: setActiveSpeakers,
@@ -164,8 +183,7 @@ export default function App() {
}, [isStreaming, startStreamingLocal, stopStreamingLocal, patchUIState]); }, [isStreaming, startStreamingLocal, stopStreamingLocal, patchUIState]);
useEffect(() => { if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); }, [selectedVoiceGuild]); useEffect(() => { if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); }, [selectedVoiceGuild]);
useEffect(() => { if (selectedTextGuild) voice.loadTextTargets(selectedTextGuild).catch(() => undefined); }, [selectedTextGuild]); useEffect(() => { if (monitorGuildId) voice.loadTextTargets(monitorGuildId).catch(() => undefined); }, [monitorGuildId]);
useEffect(() => { if (selectedAnalyticsGuild) voice.loadTextTargets(selectedAnalyticsGuild).catch(() => undefined); }, [selectedAnalyticsGuild]);
useEffect(() => { if (selectedTextChannel) messages.fetchMessages(selectedTextChannel).catch(() => undefined); }, [selectedTextChannel]); useEffect(() => { if (selectedTextChannel) messages.fetchMessages(selectedTextChannel).catch(() => undefined); }, [selectedTextChannel]);
const toggleListening = useCallback(async () => { const toggleListening = useCallback(async () => {
@@ -232,7 +250,7 @@ export default function App() {
) )
) : activeTab === "messages" ? ( ) : activeTab === "messages" ? (
<MessagesPanel <MessagesPanel
guilds={voice.guilds} guilds={monitorGuild ? [monitorGuild] : []}
channels={voice.textChannels} channels={voice.textChannels}
selectedGuild={selectedTextGuild} selectedGuild={selectedTextGuild}
selectedChannel={selectedTextChannel} selectedChannel={selectedTextChannel}
@@ -251,7 +269,7 @@ export default function App() {
} }
> >
<AnalyticsPanel <AnalyticsPanel
guilds={voice.guilds} guilds={monitorGuild ? [monitorGuild] : []}
channels={voice.textChannels} channels={voice.textChannels}
selectedGuild={selectedAnalyticsGuild} selectedGuild={selectedAnalyticsGuild}
selectedChannel={selectedAnalyticsChannel} selectedChannel={selectedAnalyticsChannel}
+8
View File
@@ -48,6 +48,10 @@ export interface Guild {
icon: string | null; icon: string | null;
} }
export interface AppConfig {
monitorGuildId: string | null;
}
class ApiError extends Error { class ApiError extends Error {
code: string; code: string;
statusCode: number; statusCode: number;
@@ -105,3 +109,7 @@ export async function reanalyzeMessage(id: string): Promise<void> {
export async function getGuilds(): Promise<Guild[]> { export async function getGuilds(): Promise<Guild[]> {
return request<Guild[]>("/api/guilds"); return request<Guild[]>("/api/guilds");
} }
export async function getAppConfig(): Promise<AppConfig> {
return request<AppConfig>("/api/config");
}
+2 -1
View File
@@ -214,7 +214,8 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
const parsed = configSchema.parse(env); const parsed = configSchema.parse(env);
return { return {
...parsed, ...parsed,
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID, // AI text capture and analytics are pinned to the monitor guild.
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID, EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID,
}; };
} catch (error) { } catch (error) {
+2
View File
@@ -12,6 +12,7 @@ import type { createChildLogger } from "../logger.js";
import type { MediaController } from "../media/mediaController.js"; import type { MediaController } from "../media/mediaController.js";
import type { ModerationBroadcaster } from "../moderation/types.js"; import type { ModerationBroadcaster } from "../moderation/types.js";
import { createAnalysisRoutes } from "../routes/analysisRoutes.js"; import { createAnalysisRoutes } from "../routes/analysisRoutes.js";
import { createAppConfigRoutes } from "../routes/appConfigRoutes.js";
import { createAnalyticsRoutes } from "../routes/analyticsRoutes.js"; import { createAnalyticsRoutes } from "../routes/analyticsRoutes.js";
import { createMediaRoutes } from "../routes/mediaRoutes.js"; import { createMediaRoutes } from "../routes/mediaRoutes.js";
import { createMessageRoutes } from "../routes/messageRoutes.js"; import { createMessageRoutes } from "../routes/messageRoutes.js";
@@ -115,6 +116,7 @@ export function createHttpApp(options: CreateHttpAppOptions) {
); );
app.use("/api", createMessageRoutes()); app.use("/api", createMessageRoutes());
app.use("/api", createAnalysisRoutes()); app.use("/api", createAnalysisRoutes());
app.use("/api", createAppConfigRoutes());
app.use("/api", createReviewRoutes()); app.use("/api", createReviewRoutes());
app.use("/api", createAnalyticsRoutes()); app.use("/api", createAnalyticsRoutes());
app.use("/api", createSyncRoutes(options.client)); app.use("/api", createSyncRoutes(options.client));
+33 -13
View File
@@ -235,18 +235,25 @@ export async function getMessagesByChannel(
channelId: string, channelId: string,
limit: number = 50, limit: number = 50,
offset: number = 0, offset: number = 0,
guildId?: string,
): Promise<MessageRecord[]> { ): Promise<MessageRecord[]> {
try { try {
const database = db(); const database = db();
const conditions: SQL[] = [
or(
eq(messagesTable.channel_id, channelId),
eq(messagesTable.thread_id, channelId),
) as SQL,
];
if (guildId) {
conditions.push(eq(messagesTable.guild_id, guildId));
}
const rows = await database const rows = await database
.select() .select()
.from(messagesTable) .from(messagesTable)
.where( .where(and(...conditions))
or(
eq(messagesTable.channel_id, channelId),
eq(messagesTable.thread_id, channelId),
),
)
// P3: add secondary sort by id for stable pagination // P3: add secondary sort by id for stable pagination
.orderBy(desc(messagesTable.created_at), desc(messagesTable.id)) .orderBy(desc(messagesTable.created_at), desc(messagesTable.id))
.limit(limit) .limit(limit)
@@ -290,18 +297,25 @@ export async function getAttachmentsByChannel(
channelId: string, channelId: string,
limit: number = 50, limit: number = 50,
offset: number = 0, offset: number = 0,
guildId?: string,
): Promise<AttachmentRecord[]> { ): Promise<AttachmentRecord[]> {
try { try {
const database = db(); const database = db();
const conditions: SQL[] = [
or(
eq(attachmentsTable.channel_id, channelId),
eq(attachmentsTable.thread_id, channelId),
) as SQL,
];
if (guildId) {
conditions.push(eq(attachmentsTable.guild_id, guildId));
}
const rows = await database const rows = await database
.select() .select()
.from(attachmentsTable) .from(attachmentsTable)
.where( .where(and(...conditions))
or(
eq(attachmentsTable.channel_id, channelId),
eq(attachmentsTable.thread_id, channelId),
),
)
.orderBy(desc(attachmentsTable.created_at)) .orderBy(desc(attachmentsTable.created_at))
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
@@ -732,15 +746,20 @@ export async function getAttachmentsForMessages(
export async function searchMessages(input: { export async function searchMessages(input: {
query: string; query: string;
channelId?: string; channelId?: string;
guildId?: string;
limit?: number; limit?: number;
}): Promise<MessageRecord[]> { }): Promise<MessageRecord[]> {
try { try {
const { query, channelId, limit = 20 } = input; const { query, channelId, guildId, limit = 20 } = input;
const database = db(); const database = db();
const searchPattern = `%${query}%`; const searchPattern = `%${query}%`;
const conditions: (SQL | undefined)[] = [isNull(messagesTable.deleted_at)]; const conditions: (SQL | undefined)[] = [isNull(messagesTable.deleted_at)];
if (guildId) {
conditions.push(eq(messagesTable.guild_id, guildId));
}
if (channelId) { if (channelId) {
conditions.push(channelOrThreadCondition(channelId)); conditions.push(channelOrThreadCondition(channelId));
} }
@@ -767,6 +786,7 @@ export async function searchMessages(input: {
{ {
query: input.query, query: input.query,
channelId: input.channelId, channelId: input.channelId,
guildId: input.guildId,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}, },
"Failed to search messages", "Failed to search messages",
+16
View File
@@ -1,5 +1,6 @@
import type { Router } from "express"; import type { Router } from "express";
import express from "express"; import express from "express";
import { config } from "../config.js";
import { AppError } from "../errors.js"; import { AppError } from "../errors.js";
import { import {
getAnalysisQueueStatus, getAnalysisQueueStatus,
@@ -7,6 +8,7 @@ import {
} from "../moderation/aiAnalyzer.js"; } from "../moderation/aiAnalyzer.js";
import { import {
searchMessages, searchMessages,
getMessageById,
updateMessageAIAnalysis, updateMessageAIAnalysis,
} from "../moderation/messageStore.js"; } from "../moderation/messageStore.js";
import type { MessageRecord } from "../moderation/types.js"; import type { MessageRecord } from "../moderation/types.js";
@@ -49,6 +51,7 @@ export function createAnalysisRoutes(): Router {
const results = await searchMessages({ const results = await searchMessages({
query: q, query: q,
guildId: config.MONITOR_GUILD_ID,
channelId, channelId,
limit: limitNum, limit: limitNum,
}); });
@@ -78,6 +81,19 @@ export function createAnalysisRoutes(): Router {
throw new AppError("Message ID is required", "MISSING_MESSAGE_ID", 400); throw new AppError("Message ID is required", "MISSING_MESSAGE_ID", 400);
} }
const existing = await getMessageById(id);
if (!existing) {
throw new AppError("Message not found", "MESSAGE_NOT_FOUND", 404);
}
if (existing.guild_id !== config.MONITOR_GUILD_ID) {
throw new AppError(
"Message is outside the monitor guild",
"INVALID_GUILD",
403,
);
}
// P3: Single UPDATE + RETURNING instead of GET + UPDATE + GET // P3: Single UPDATE + RETURNING instead of GET + UPDATE + GET
const updated = await updateMessageAIAnalysis(id, { const updated = await updateMessageAIAnalysis(id, {
status: "pending", status: "pending",
+37 -8
View File
@@ -1,5 +1,6 @@
import type { Router } from "express"; import type { Router } from "express";
import express from "express"; import express from "express";
import { config } from "../config.js";
import { AppError } from "../errors.js"; import { AppError } from "../errors.js";
import { import {
getActivityHeatmap, getActivityHeatmap,
@@ -15,6 +16,26 @@ import {
export function createAnalyticsRoutes(): Router { export function createAnalyticsRoutes(): Router {
const router = express.Router(); const router = express.Router();
function assertMonitorGuild(guildId?: string): string {
if (!config.MONITOR_GUILD_ID) {
throw new AppError(
"MONITOR_GUILD_ID is required for analytics",
"MISSING_MONITOR_GUILD_ID",
400,
);
}
if (guildId && guildId !== config.MONITOR_GUILD_ID) {
throw new AppError(
"Analytics are restricted to the monitor guild",
"INVALID_GUILD",
403,
);
}
return config.MONITOR_GUILD_ID;
}
// GET /api/analytics/overview - Full analytics dashboard data // GET /api/analytics/overview - Full analytics dashboard data
// Query params: guildId (required), channelId, hours (default 24) // Query params: guildId (required), channelId, hours (default 24)
router.get("/analytics/overview", async (req, res, next) => { router.get("/analytics/overview", async (req, res, next) => {
@@ -34,9 +55,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const monitorGuildId = assertMonitorGuild(guildId);
const overview = await getAnalyticsOverview({ const overview = await getAnalyticsOverview({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
@@ -66,9 +88,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const monitorGuildId = assertMonitorGuild(guildId);
const stats = await getHourlyStats({ const stats = await getHourlyStats({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
@@ -98,9 +121,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const monitorGuildId = assertMonitorGuild(guildId);
const topics = await getTopicTrends({ const topics = await getTopicTrends({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
@@ -132,9 +156,10 @@ export function createAnalyticsRoutes(): Router {
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20; const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20;
const monitorGuildId = assertMonitorGuild(guildId);
const users = await getUserLeaderboard({ const users = await getUserLeaderboard({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
limit: limitNum, limit: limitNum,
@@ -165,9 +190,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const monitorGuildId = assertMonitorGuild(guildId);
const stats = await getModerationStats({ const stats = await getModerationStats({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
@@ -199,9 +225,10 @@ export function createAnalyticsRoutes(): Router {
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24; const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20; const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20;
const monitorGuildId = assertMonitorGuild(guildId);
const violators = await getTopViolators({ const violators = await getTopViolators({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
limit: limitNum, limit: limitNum,
@@ -232,9 +259,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168; const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168;
const monitorGuildId = assertMonitorGuild(guildId);
const trend = await getDailyTrend({ const trend = await getDailyTrend({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
@@ -264,9 +292,10 @@ export function createAnalyticsRoutes(): Router {
} }
const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168; const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168;
const monitorGuildId = assertMonitorGuild(guildId);
const heatmap = await getActivityHeatmap({ const heatmap = await getActivityHeatmap({
guildId, guildId: monitorGuildId,
channelId, channelId,
hours: hoursNum, hours: hoursNum,
}); });
+15
View File
@@ -0,0 +1,15 @@
import type { Router } from "express";
import express from "express";
import { config } from "../config.js";
export function createAppConfigRoutes(): Router {
const router = express.Router();
router.get("/config", (_req, res) => {
res.json({
monitorGuildId: config.MONITOR_GUILD_ID ?? null,
});
});
return router;
}
+14 -1
View File
@@ -1,5 +1,6 @@
import type { Router } from "express"; import type { Router } from "express";
import express from "express"; import express from "express";
import { config } from "../config.js";
import { AppError } from "../errors.js"; import { AppError } from "../errors.js";
import { import {
getAttachmentsByChannel, getAttachmentsByChannel,
@@ -52,6 +53,7 @@ export function createMessageRoutes(): Router {
}; };
const targetChannel = channelId || channel; const targetChannel = channelId || channel;
const monitorGuildId = config.MONITOR_GUILD_ID;
const limitNum = Math.min(parseInt(limit) || 50, 100); const limitNum = Math.min(parseInt(limit) || 50, 100);
const offsetNum = parseInt(offset) || 0; const offsetNum = parseInt(offset) || 0;
@@ -68,6 +70,7 @@ export function createMessageRoutes(): Router {
targetChannel, targetChannel,
limitNum, limitNum,
offsetNum, offsetNum,
monitorGuildId,
); );
res.json({ res.json({
type: "image", type: "image",
@@ -77,6 +80,7 @@ export function createMessageRoutes(): Router {
}); });
} else if (channelId || cursor || status) { } else if (channelId || cursor || status) {
const result = await listMessages({ const result = await listMessages({
guildId: monitorGuildId,
channelId: targetChannel, channelId: targetChannel,
cursor, cursor,
limit: limitNum, limit: limitNum,
@@ -101,6 +105,7 @@ export function createMessageRoutes(): Router {
targetChannel, targetChannel,
limitNum, limitNum,
offsetNum, offsetNum,
monitorGuildId,
); );
res.json({ res.json({
type: "text", type: "text",
@@ -129,6 +134,14 @@ export function createMessageRoutes(): Router {
throw new AppError("Message not found", "MESSAGE_NOT_FOUND", 404); throw new AppError("Message not found", "MESSAGE_NOT_FOUND", 404);
} }
if (message.guild_id !== config.MONITOR_GUILD_ID) {
throw new AppError(
"Message is outside the monitor guild",
"INVALID_GUILD",
403,
);
}
res.json(message); res.json(message);
} catch (error) { } catch (error) {
next(error); next(error);
@@ -158,7 +171,7 @@ export function createMessageRoutes(): Router {
const limitNum = Math.min(parseInt(limit) || 50, 100); const limitNum = Math.min(parseInt(limit) || 50, 100);
const query: Omit<MessageQuery, "status"> = { const query: Omit<MessageQuery, "status"> = {
guildId, guildId: guildId || config.MONITOR_GUILD_ID,
channelId, channelId,
threadId, threadId,
userId, userId,
+3 -3
View File
@@ -88,11 +88,11 @@ describe("loadConfig", () => {
expect(config.VOICE_CHANNEL_ID).toBe("voice-channel"); expect(config.VOICE_CHANNEL_ID).toBe("voice-channel");
}); });
it("uses explicit split text and voice config before legacy values", async () => { it("pins text capture to the monitor guild even when legacy text config is present", async () => {
process.env = { process.env = {
...originalEnv, ...originalEnv,
DISCORD_TOKEN: "token", DISCORD_TOKEN: "token",
MONITOR_GUILD_ID: "legacy-text-guild", MONITOR_GUILD_ID: "monitor-guild",
GUILD_ID: "legacy-voice-guild", GUILD_ID: "legacy-voice-guild",
TEXT_GUILD_ID: "text-guild", TEXT_GUILD_ID: "text-guild",
TEXT_CHANNEL_ID: "text-channel", TEXT_CHANNEL_ID: "text-channel",
@@ -104,7 +104,7 @@ describe("loadConfig", () => {
const { loadConfig } = await import("../src/config"); const { loadConfig } = await import("../src/config");
const config = loadConfig(process.env); const config = loadConfig(process.env);
expect(config.EFFECTIVE_TEXT_GUILD_ID).toBe("text-guild"); expect(config.EFFECTIVE_TEXT_GUILD_ID).toBe("monitor-guild");
expect(config.TEXT_CHANNEL_ID).toBe("text-channel"); expect(config.TEXT_CHANNEL_ID).toBe("text-channel");
expect(config.EFFECTIVE_VOICE_GUILD_ID).toBe("voice-guild"); expect(config.EFFECTIVE_VOICE_GUILD_ID).toBe("voice-guild");
}); });