'use strict'; const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { BotManager, kindOfPosition } = require('../src/bot/botManager'); const { ChatLog, ChatKind } = require('../src/core/chatLog'); const { defaultConfig, normalizeConfig } = require('../src/config/expansionConfig'); const { Logger } = require('../src/util/logger'); /** 建一个不连接的机器人管理器(含公屏日志) */ function setup(options) { const opts = options || {}; const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-botmgr-')); const chatLog = new ChatLog({ dir }); const config = normalizeConfig(Object.assign(defaultConfig(), opts.config || {})); if (opts.config && opts.config.server) { Object.assign(config.server, opts.config.server); } const manager = new BotManager({ id: 'BotA', username: 'BotA', password: 'pw', host: 'mc.example.com', port: 25565, version: '1.21.4', label: '主号' }, config, new Logger({ echo: false }), { chatLog }); const sent = []; manager.bot = { chat: (text) => sent.push(text) }; return { manager, chatLog, config, sent }; } /** 等一轮宏任务,让指令节流定时器真正把内容写出 */ function drainQueue() { return new Promise((resolve) => setTimeout(resolve, 5)); } test('kindOfPosition 把消息通道映射为公屏日志类型', () => { assert.strictEqual(kindOfPosition('chat'), ChatKind.CHAT); assert.strictEqual(kindOfPosition('system'), ChatKind.SYSTEM); assert.strictEqual(kindOfPosition('game_info'), ChatKind.ACTION); assert.strictEqual(kindOfPosition(undefined), ChatKind.CHAT); }); test('isReady 要求已连接、已进入世界、寻路器就绪且完成登录验证', () => { const { manager } = setup(); manager.state = 'online'; manager.bot = { entity: {}, pathfinder: {} }; manager.loggedIn = false; assert.strictEqual(manager.isReady(), false, '未完成登录验证不算就绪'); manager.loggedIn = true; assert.strictEqual(manager.isReady(), true); manager.state = 'connecting'; assert.strictEqual(manager.isReady(), false); }); test('未完成登录验证时拒绝发送指令与聊天', () => { const { manager, sent } = setup(); manager.state = 'online'; manager.loggedIn = false; assert.strictEqual(manager.canOperate(), false); assert.strictEqual(manager.sendCommand('u claimmode'), false); assert.strictEqual(manager.sendChat('大家好'), false); assert.deepStrictEqual(sent, [], '不应真的写出任何内容'); }); test('登录验证通过后指令与聊天都会被写出并留档', () => { const { manager, chatLog, sent } = setup({ config: { server: { commandCooldownMs: 0 } } }); manager.state = 'online'; manager.loggedIn = true; manager.sendCommand('u claimmode', { immediate: true }); manager.sendChat('大家好', { internal: true }); manager._chatQueue.length = 0; manager._writeChat('大家好'); assert.deepStrictEqual(sent, ['/u claimmode', '大家好']); const rows = chatLog.list('BotA'); assert.strictEqual(rows.length, 2); assert.strictEqual(rows[0].kind, ChatKind.COMMAND); assert.strictEqual(rows[0].message, '/u claimmode'); assert.strictEqual(rows[1].kind, ChatKind.OUT); assert.strictEqual(rows[1].message, '大家好'); }); test('以斜杠开头的聊天内容被当作指令发送', () => { const { manager, sent } = setup(); manager.state = 'online'; manager.loggedIn = true; manager.sendChat('/spawn', { internal: true }); manager._chatQueue.length = 0; // 指令走队列,直接驱动写入 manager._writeCommand(manager._commandQueue.pop() || 'spawn'); assert.deepStrictEqual(sent, ['/spawn']); }); test('internal 选项绕过登录门禁(登录 / 注册流程自身使用)', () => { const { manager, sent } = setup(); manager.state = 'online'; manager.loggedIn = false; assert.strictEqual(manager.sendCommand('login pw', { internal: true, immediate: true }), true); assert.deepStrictEqual(sent, ['/login pw']); }); test('收到「登录成功」提示后置为已登录', () => { const { manager } = setup(); manager.config.server.loginCommand = 'login {password}'; manager.loggedIn = false; manager._handleAuthPrompt('登录成功,欢迎回来'); assert.strictEqual(manager.loggedIn, true); assert.strictEqual(manager.loginFailed, false); }); test('收到「已登录」提示同样视为通过验证', () => { const { manager } = setup(); manager.loggedIn = false; manager._handleAuthPrompt('你已登录,无需重复登录'); assert.strictEqual(manager.loggedIn, true); }); test('收到密码错误提示时标记登录失败且不放行操作', () => { const { manager } = setup(); manager.loggedIn = false; manager._handleAuthPrompt('密码错误,请重新输入'); assert.strictEqual(manager.loginFailed, true); assert.strictEqual(manager.loggedIn, false); assert.strictEqual(manager.canOperate(), false); }); test('服务器提示注册时自动发送注册指令(绕过门禁)', async () => { const { manager, sent } = setup({ config: { server: { commandCooldownMs: 0 } } }); manager.config.server.registerCommand = 'register {password}'; manager.config.server.loginCommand = 'login {password}'; manager.loggedIn = false; manager._handleAuthPrompt('请先注册'); await drainQueue(); assert.deepStrictEqual(sent, ['/register pw']); }); test('服务器提示登录时自动发送登录指令(绕过门禁)', async () => { const { manager, sent } = setup({ config: { server: { commandCooldownMs: 0 } } }); manager.config.server.loginCommand = 'login {password}'; manager.loggedIn = false; manager._handleAuthPrompt('请先输入登录密码'); await drainQueue(); assert.deepStrictEqual(sent, ['/login pw']); }); test('登录确认兜底:超时未收到任何提示时按已登录放行', async () => { const { manager } = setup({ config: { server: { loginConfirmTimeoutMs: 20 } } }); manager.loggedIn = false; manager.loginFailed = false; manager._armLoginConfirmFallback(); await new Promise((resolve) => setTimeout(resolve, 60)); assert.strictEqual(manager.loggedIn, true); }); test('登录确认兜底不会覆盖已明确失败的登录', async () => { const { manager } = setup({ config: { server: { loginConfirmTimeoutMs: 20 } } }); manager.loggedIn = false; manager._armLoginConfirmFallback(); manager.loginFailed = true; await new Promise((resolve) => setTimeout(resolve, 60)); assert.strictEqual(manager.loggedIn, false); }); test('收到消息时写入公屏日志(含系统消息与发言者)', () => { const { manager, chatLog } = setup(); manager._recordChat('服务器将在 5 分钟后重启', 'system', null); manager._recordChat('你好', 'chat', 'Steve'); const rows = chatLog.list('BotA'); assert.strictEqual(rows.length, 2); assert.strictEqual(rows[0].kind, ChatKind.SYSTEM); assert.strictEqual(rows[1].kind, ChatKind.CHAT); assert.strictEqual(rows[1].username, 'Steve'); }); test('snapshot 暴露多机器人需要的 id / 备注 / 登录与操作许可', () => { const { manager } = setup(); const snap = manager.snapshot(); assert.strictEqual(snap.id, 'BotA'); assert.strictEqual(snap.label, '主号'); assert.strictEqual(snap.username, 'BotA'); assert.strictEqual(snap.loggedIn, false); assert.strictEqual(snap.canOperate, false); assert.strictEqual(snap.loginFailed, false); });