mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat(autopause): expose autoPauseOnEmpty via /api/bot/settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8f5bb26b3a
commit
491bc53dec
2 files changed
+197
-21
No files matched your search
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import express from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import request from "supertest";
|
||||
import pino from "pino";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
||||
import { createUserStore } from "../../data/users.js";
|
||||
import { createSessionStore } from "../../data/sessions.js";
|
||||
import { createAvatarStore } from "../../data/avatars.js";
|
||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||
import { createBotRouter } from "./bot.js";
|
||||
import { getDefaultConfig, type BotConfig } from "../../data/config.js";
|
||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||
import type { BotManager } from "../../bot/manager.js";
|
||||
|
||||
/** Records every updateIdleTimeout / updateAutoPause call so the test can assert propagation. */
|
||||
function makeFakeBot() {
|
||||
return {
|
||||
idleTimeoutCalls: [] as number[],
|
||||
autoPauseCalls: [] as boolean[],
|
||||
updateIdleTimeout(minutes: number) {
|
||||
this.idleTimeoutCalls.push(minutes);
|
||||
},
|
||||
updateAutoPause(enabled: boolean) {
|
||||
this.autoPauseCalls.push(enabled);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("bot router /settings", () => {
|
||||
let botDb: BotDatabase;
|
||||
let app: express.Express;
|
||||
let cookie: string;
|
||||
let config: BotConfig;
|
||||
let configPath: string;
|
||||
let tmpDir: string;
|
||||
let fakeBots: ReturnType<typeof makeFakeBot>[];
|
||||
|
||||
beforeEach(async () => {
|
||||
botDb = createDatabase(":memory:");
|
||||
const users = createUserStore(botDb.db);
|
||||
const sessions = createSessionStore(botDb.db);
|
||||
const alice = await users.createUser("alice", "pw-alice", "admin");
|
||||
cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
|
||||
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "botsettings-"));
|
||||
configPath = join(tmpDir, "config.json");
|
||||
config = { ...getDefaultConfig(), idleTimeoutMinutes: 15, autoPauseOnEmpty: true };
|
||||
|
||||
fakeBots = [makeFakeBot(), makeFakeBot()];
|
||||
const fakeManager = {
|
||||
getAllBots: () => fakeBots,
|
||||
} as unknown as BotManager;
|
||||
const avatarStore = createAvatarStore(tmpDir);
|
||||
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use("/api", createRequireAuth(sessions));
|
||||
app.use(
|
||||
"/api/bot",
|
||||
createBotRouter(fakeManager, config, configPath, pino({ level: "silent" }), botDb, avatarStore),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
botDb.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("requires auth", async () => {
|
||||
const res = await request(app).get("/api/bot/settings");
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("GET /settings includes autoPauseOnEmpty reflecting config", async () => {
|
||||
const res = await request(app).get("/api/bot/settings").set("Cookie", cookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.idleTimeoutMinutes).toBe(15);
|
||||
expect(res.body.autoPauseOnEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it("POST /settings with autoPauseOnEmpty:false persists and propagates to bots", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/bot/settings")
|
||||
.set("Cookie", cookie)
|
||||
.send({ autoPauseOnEmpty: false });
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// in-memory config mutated
|
||||
expect(config.autoPauseOnEmpty).toBe(false);
|
||||
|
||||
// propagated to every live bot
|
||||
for (const bot of fakeBots) {
|
||||
expect(bot.autoPauseCalls).toEqual([false]);
|
||||
}
|
||||
|
||||
// follow-up GET reflects the new value
|
||||
const followUp = await request(app).get("/api/bot/settings").set("Cookie", cookie);
|
||||
expect(followUp.body.autoPauseOnEmpty).toBe(false);
|
||||
});
|
||||
|
||||
it("POST /settings still handles idleTimeoutMinutes (no regression)", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/bot/settings")
|
||||
.set("Cookie", cookie)
|
||||
.send({ idleTimeoutMinutes: 42 });
|
||||
expect(res.status).toBe(200);
|
||||
expect(config.idleTimeoutMinutes).toBe(42);
|
||||
for (const bot of fakeBots) {
|
||||
expect(bot.idleTimeoutCalls).toEqual([42]);
|
||||
}
|
||||
const followUp = await request(app).get("/api/bot/settings").set("Cookie", cookie);
|
||||
expect(followUp.body.idleTimeoutMinutes).toBe(42);
|
||||
});
|
||||
|
||||
it("POST /settings handles both fields together", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/bot/settings")
|
||||
.set("Cookie", cookie)
|
||||
.send({ idleTimeoutMinutes: 7, autoPauseOnEmpty: false });
|
||||
expect(res.status).toBe(200);
|
||||
expect(config.idleTimeoutMinutes).toBe(7);
|
||||
expect(config.autoPauseOnEmpty).toBe(false);
|
||||
for (const bot of fakeBots) {
|
||||
expect(bot.idleTimeoutCalls).toEqual([7]);
|
||||
expect(bot.autoPauseCalls).toEqual([false]);
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /settings with only autoPauseOnEmpty does not touch idleTimeout bots", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/bot/settings")
|
||||
.set("Cookie", cookie)
|
||||
.send({ autoPauseOnEmpty: false });
|
||||
expect(res.status).toBe(200);
|
||||
for (const bot of fakeBots) {
|
||||
expect(bot.idleTimeoutCalls).toEqual([]);
|
||||
expect(bot.autoPauseCalls).toEqual([false]);
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /settings ignores non-boolean autoPauseOnEmpty without 400", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/bot/settings")
|
||||
.set("Cookie", cookie)
|
||||
.send({ idleTimeoutMinutes: 5, autoPauseOnEmpty: "yes" });
|
||||
expect(res.status).toBe(200);
|
||||
// idleTimeout still applied
|
||||
expect(config.idleTimeoutMinutes).toBe(5);
|
||||
// autoPause left at its prior value, not propagated
|
||||
expect(config.autoPauseOnEmpty).toBe(true);
|
||||
for (const bot of fakeBots) {
|
||||
expect(bot.autoPauseCalls).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
+37
-21
@@ -21,6 +21,43 @@ export function createBotRouter(
|
||||
res.json({ bots });
|
||||
});
|
||||
|
||||
// GET /api/bot/settings — 读取全局 bot 行为设置
|
||||
// NOTE: must be registered before "/:id" so it isn't shadowed by the param route.
|
||||
router.get("/settings", (_req, res) => {
|
||||
res.json({
|
||||
idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0,
|
||||
autoPauseOnEmpty: config.autoPauseOnEmpty,
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/bot/settings — 保存全局 bot 行为设置
|
||||
router.post("/settings", (req, res) => {
|
||||
const { idleTimeoutMinutes, autoPauseOnEmpty } = req.body;
|
||||
|
||||
const hasIdle = idleTimeoutMinutes !== undefined;
|
||||
if (hasIdle && (typeof idleTimeoutMinutes !== "number" || idleTimeoutMinutes < 0)) {
|
||||
res.status(400).json({ error: "idleTimeoutMinutes must be a non-negative number" });
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAutoPause = typeof autoPauseOnEmpty === "boolean";
|
||||
|
||||
if (hasIdle) config.idleTimeoutMinutes = idleTimeoutMinutes;
|
||||
if (hasAutoPause) config.autoPauseOnEmpty = autoPauseOnEmpty;
|
||||
saveConfig(configPath, config);
|
||||
|
||||
// 通知所有 bot 实例更新
|
||||
for (const bot of botManager.getAllBots()) {
|
||||
if (hasIdle) bot.updateIdleTimeout(config.idleTimeoutMinutes);
|
||||
if (hasAutoPause) bot.updateAutoPause(config.autoPauseOnEmpty);
|
||||
}
|
||||
|
||||
res.json({
|
||||
idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0,
|
||||
autoPauseOnEmpty: config.autoPauseOnEmpty,
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/:id", (req, res) => {
|
||||
const bot = botManager.getBot(req.params.id);
|
||||
if (!bot) {
|
||||
@@ -185,27 +222,6 @@ export function createBotRouter(
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/bot/settings — 读取全局 bot 行为设置
|
||||
router.get("/settings", (_req, res) => {
|
||||
res.json({ idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0 });
|
||||
});
|
||||
|
||||
// POST /api/bot/settings — 保存全局 bot 行为设置
|
||||
router.post("/settings", (req, res) => {
|
||||
const { idleTimeoutMinutes } = req.body;
|
||||
if (typeof idleTimeoutMinutes !== "number" || idleTimeoutMinutes < 0) {
|
||||
res.status(400).json({ error: "idleTimeoutMinutes must be a non-negative number" });
|
||||
return;
|
||||
}
|
||||
config.idleTimeoutMinutes = idleTimeoutMinutes;
|
||||
saveConfig(configPath, config);
|
||||
// 通知所有 bot 实例更新定时器
|
||||
for (const bot of botManager.getAllBots()) {
|
||||
bot.updateIdleTimeout(idleTimeoutMinutes);
|
||||
}
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in new issue
Block a user