From 45414b3baaa7c550cb527978b431866e7d88d647 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 25 Jun 2026 14:58:46 +0800 Subject: [PATCH] feat(guest): prune deleted bot from guest scope on removeBot When a bot is deleted, prune its id from config.guestMode.bots (when an array) and persist, mirroring the existing permissions.pruneBot(id) member-access pruning. Thread CONFIG_PATH into BotManager so removeBot can save the updated config. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/bot/manager.test.ts | 87 +++++++++++++++++++++++++++++++++++++++++ src/bot/manager.ts | 12 +++++- src/index.ts | 3 +- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/bot/manager.test.ts diff --git a/src/bot/manager.test.ts b/src/bot/manager.test.ts new file mode 100644 index 0000000..65e2c35 --- /dev/null +++ b/src/bot/manager.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { join } from "node:path"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { BotManager } from "./manager.js"; +import { createDatabase, type BotDatabase } from "../data/database.js"; +import { createPermissionStore } from "../data/permissions.js"; +import { getDefaultConfig, loadConfig, saveConfig, type BotConfig } from "../data/config.js"; +import type { Logger } from "../logger.js"; +import type { MusicProvider } from "../music/provider.js"; +import type { AvatarStore } from "../data/avatars.js"; + +// removeBot only calls logger.info; provide the full shape it could touch. +const stubLogger = { + info() {}, + warn() {}, + error() {}, + debug() {}, + child() { + return stubLogger; + }, +} as unknown as Logger; + +describe("BotManager.removeBot — guest scope pruning", () => { + const dirs: string[] = []; + let db: BotDatabase; + + function makeTmpConfigPath(): string { + const dir = mkdtempSync(join(tmpdir(), "tsmusicbot-manager-test-")); + dirs.push(dir); + return join(dir, "config.json"); + } + + function makeManager(config: BotConfig, configPath: string): BotManager { + db = createDatabase(":memory:"); + const permissions = createPermissionStore(db.db); + saveConfig(configPath, config); + return new BotManager( + {} as unknown as MusicProvider, + {} as unknown as MusicProvider, + {} as unknown as MusicProvider, + db, + config, + stubLogger, + {} as unknown as AvatarStore, + permissions, + configPath + ); + } + + afterEach(() => { + try { + db?.close(); + } catch { + /* ignore */ + } + for (const d of dirs) { + rmSync(d, { recursive: true, force: true }); + } + dirs.length = 0; + }); + + it("prunes a deleted bot from guestMode.bots (array) and persists", async () => { + const configPath = makeTmpConfigPath(); + const config = getDefaultConfig(); + config.guestMode.bots = ["botA", "botB"]; + const manager = makeManager(config, configPath); + + await manager.removeBot("botA"); + + expect(config.guestMode.bots).toEqual(["botB"]); + // Persisted file must also reflect the prune. + expect(loadConfig(configPath).guestMode.bots).toEqual(["botB"]); + }); + + it('leaves guestMode.bots === "all" unchanged (no crash, no change)', async () => { + const configPath = makeTmpConfigPath(); + const config = getDefaultConfig(); + config.guestMode.bots = "all"; + const manager = makeManager(config, configPath); + + await manager.removeBot("botA"); + + expect(config.guestMode.bots).toBe("all"); + expect(loadConfig(configPath).guestMode.bots).toBe("all"); + }); +}); diff --git a/src/bot/manager.ts b/src/bot/manager.ts index f9535d6..f56c55c 100644 --- a/src/bot/manager.ts +++ b/src/bot/manager.ts @@ -7,7 +7,7 @@ import { import type { MusicProvider } from "../music/provider.js"; import { YouTubeProvider } from "../music/youtube.js"; import type { BotDatabase } from "../data/database.js"; -import type { BotConfig } from "../data/config.js"; +import { saveConfig, type BotConfig } from "../data/config.js"; import type { Logger } from "../logger.js"; import type { ServerProtocol } from "../ts-protocol/client.js"; @@ -79,6 +79,7 @@ export class BotManager extends EventEmitter { private logger: Logger; private avatarStore: AvatarStore; private permissions: PermissionStore; + private configPath: string; constructor( neteaseProvider: MusicProvider, @@ -88,7 +89,8 @@ export class BotManager extends EventEmitter { config: BotConfig, logger: Logger, avatarStore: AvatarStore, - permissions: PermissionStore + permissions: PermissionStore, + configPath: string ) { super(); this.neteaseProvider = neteaseProvider; @@ -100,6 +102,7 @@ export class BotManager extends EventEmitter { this.logger = logger; this.avatarStore = avatarStore; this.permissions = permissions; + this.configPath = configPath; } async createBot(params: CreateBotParams): Promise { @@ -160,6 +163,11 @@ export class BotManager extends EventEmitter { } this.database.deleteBotInstance(id); this.permissions.pruneBot(id); + // Prune the deleted bot from the guest scope allow-list (mirrors permissions.pruneBot). + if (Array.isArray(this.config.guestMode.bots) && this.config.guestMode.bots.includes(id)) { + this.config.guestMode.bots = this.config.guestMode.bots.filter((b) => b !== id); + saveConfig(this.configPath, this.config); + } this.emit("botInstanceRemoved", id); this.logger.info({ botId: id }, "Bot instance removed"); } diff --git a/src/index.ts b/src/index.ts index a281a0b..77b42d9 100755 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,8 @@ async function main() { config, logger, avatarStore, - permissions + permissions, + CONFIG_PATH ); await botManager.loadSavedBots();