Update Live-streaming code (auto-daily features)

This commit is contained in:
2026-08-15 17:45:19 +08:00
parent ba39e9743d
commit f623959fd4
19 changed files with 3517 additions and 62 deletions
+102
View File
@@ -0,0 +1,102 @@
import json
import tempfile
import unittest
from pathlib import Path
from app.bettergi_current_party import (
CURRENT_PARTY_SCRIPT_NAME,
CurrentPartyReadError,
clear_current_party_status,
prepare_current_party_read,
read_current_party_status,
)
class BetterGICurrentPartyTests(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.group_dir = self.root / "User" / "ScriptGroup"
self._write_json(
self.group_dir / "切换队伍.json",
{
"index": 2,
"name": "切换队伍",
"config": {"marker": "preserved"},
"projects": [{"name": "旧项目"}],
},
)
self._write_json(
self.group_dir / "其他.json",
{"index": 5, "name": "其他", "config": {}, "projects": []},
)
def tearDown(self):
self.temp_dir.cleanup()
@staticmethod
def _write_json(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def test_prepare_reader_group_clones_config_and_uses_request_id(self):
prepared = prepare_current_party_read(self.root, "request-123")
update = prepared.updates[0]
self.assertEqual(update.data["index"], 6)
self.assertEqual(update.data["config"]["marker"], "preserved")
self.assertEqual(update.data["name"], "直播系统读取当前队伍")
project = update.data["projects"][0]
self.assertEqual(project["folderName"], CURRENT_PARTY_SCRIPT_NAME)
self.assertEqual(project["jsScriptSettingsObject"]["requestId"], "request-123")
self.assertEqual(
prepared.status_path,
self.root / "User" / "JsScript" / CURRENT_PARTY_SCRIPT_NAME / "status.json",
)
def test_status_reader_ignores_stale_and_running_results(self):
prepared = prepare_current_party_read(self.root, "request-123")
self._write_json(
prepared.status_path,
{"state": "success", "request_id": "old", "party_name": "旧队伍"},
)
self.assertIsNone(read_current_party_status(prepared.status_path, "request-123"))
self._write_json(
prepared.status_path,
{"state": "running", "request_id": "request-123"},
)
self.assertIsNone(read_current_party_status(prepared.status_path, "request-123"))
self._write_json(
prepared.status_path,
{
"state": "success",
"request_id": "request-123",
"party_name": "好感队",
"candidates": ["好感队"],
},
)
result = read_current_party_status(prepared.status_path, "request-123")
self.assertEqual(result.party_name, "好感队")
self.assertEqual(result.candidates, ("好感队",))
def test_status_reader_reports_script_error_and_can_be_cleared(self):
prepared = prepare_current_party_read(self.root, "request-123")
self._write_json(
prepared.status_path,
{
"state": "error",
"request_id": "request-123",
"message": "识别到多个候选",
},
)
with self.assertRaisesRegex(CurrentPartyReadError, "多个候选"):
read_current_party_status(prepared.status_path, "request-123")
clear_current_party_status(prepared.status_path)
self.assertFalse(prepared.status_path.exists())
if __name__ == "__main__":
unittest.main()
+503
View File
@@ -0,0 +1,503 @@
import json
import tempfile
import unittest
from pathlib import Path
from app.bettergi_daily import (
DAILY_MODE_COMMISSION,
DAILY_MODE_DOMAIN,
DAILY_MODE_LEY_LINE,
DAILY_MODE_NONE,
DailyAutomationError,
DomainAliasResolver,
apply_json_updates,
parse_daily_request,
prepare_commission_current_party_update,
prepare_daily_run,
prepare_edit_party_update,
prepare_switch_party_update,
resolve_party_members,
)
class BetterGIDailyTestCase(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.alias_path = self.root / "domain_aliases.json"
self._write_json(
self.alias_path,
{
"_说明": "test",
"铭记之谷": ["风本", "少女套"],
},
)
self._write_json(
self.root / "User" / "OneDragon" / "默认配置.json",
{
"Name": "默认配置",
"TaskEnabledList": {
"mail-template-id": True,
"serenitea-template-id": True,
"craft-template-id": True,
"domain-template-id": False,
"ley-line-template-id": False,
"daily-reward-template-id": True,
},
"TaskOrder": [
"mail-template-id",
"serenitea-template-id",
"craft-template-id",
"domain-template-id",
"ley-line-template-id",
"daily-reward-template-id",
],
"TaskDefinitions": {
"mail-template-id": "领取邮件",
"serenitea-template-id": "领取尘歌壶奖励",
"craft-template-id": "合成树脂",
"domain-template-id": "自动秘境",
"ley-line-template-id": "自动地脉花",
"daily-reward-template-id": "领取每日奖励",
},
"CraftingBenchCountry": "枫丹",
"AdventurersGuildCountry": "璃月",
"PartyName": "战斗队",
"DailyRewardPartyName": "好感队",
"SecretTreasureObjects": ["须臾树脂"],
"WeeklyDomainEnabled": True,
"DomainName": "旧秘境",
"CompletionAction": "关机",
},
)
self._write_json(
self.root / "User" / "config.json",
{
"marker": "preserved",
"autoFightConfig": {"strategyName": "策略A"},
"autoDomainConfig": {
"specifyResinUse": True,
"other": 1,
},
"autoLeyLineOutcropConfig": {
"isGoToSynthesizer": True,
"team": "战斗队",
"friendshipTeam": "",
"fightConfig": {"strategyName": "策略A"},
},
},
)
strategy = self.root / "User" / "AutoFight" / "策略A.txt"
strategy.parent.mkdir(parents=True, exist_ok=True)
strategy.write_text("战斗策略", encoding="utf-8")
self._write_json(
self.root / "User" / "JsScript" / "AutoDomain" / "settings.json",
[
{
"name": "domainName",
"type": "select",
"options": ["铭记之谷", "仲夏庭园"],
}
],
)
self._write_json(
self.root / "User" / "ScriptGroup" / "每日委托.json",
{"name": "每日委托", "projects": [{"name": "委托", "status": "Enabled"}]},
)
self._write_json(
self.root / "User" / "ScriptGroup" / "切换队伍.json",
{
"name": "切换队伍",
"projects": [
{
"folderName": "AcceleratedEditionSwitchParty",
"status": "Enabled",
"jsScriptSettingsObject": {"partyName": "旧队伍", "debug": True},
}
],
},
)
self._write_json(
self.root / "User" / "ScriptGroup" / "修改队员.json",
{
"name": "修改队员",
"projects": [
{
"folderName": "AutoSwitchRoles",
"status": "Enabled",
"jsScriptSettingsObject": {"position1": ""},
}
],
},
)
options = [
"水-单手剑-芙宁娜",
"冰-长枪-爱可菲",
"草-法器-纳西妲",
"雷-长枪-雷电将军",
"岩-长枪-钟离",
]
self._write_json(
self.root / "User" / "JsScript" / "AutoSwitchRoles" / "settings.json",
[
{"name": f"position{index}", "type": "select", "options": ["", *options]}
for index in range(1, 5)
],
)
def tearDown(self):
self.temp_dir.cleanup()
@staticmethod
def _write_json(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def _prepare(
self,
argument: str,
*,
craft_before: bool = True,
commission_use_current_party: bool = False,
):
return prepare_daily_run(
self.root,
self.alias_path,
argument,
template_name="默认配置",
managed_name="直播系统自动每日",
ley_line_craft_resin_before=craft_before,
commission_use_current_party=commission_use_current_party,
)
def test_parse_daily_modes(self):
self.assertEqual(parse_daily_request("").mode, DAILY_MODE_NONE)
domain = parse_daily_request("秘境 风本")
self.assertEqual((domain.mode, domain.domain_name), (DAILY_MODE_DOMAIN, "风本"))
ley_line = parse_daily_request("地脉 经验 蒙德")
self.assertEqual(
(ley_line.mode, ley_line.ley_line_type, ley_line.ley_line_country),
(DAILY_MODE_LEY_LINE, "启示之花", "蒙德"),
)
self.assertEqual(parse_daily_request("委托").mode, DAILY_MODE_COMMISSION)
def test_parse_rejects_missing_or_unknown_arguments(self):
with self.assertRaisesRegex(DailyAutomationError, "需要指定秘境"):
parse_daily_request("秘境")
with self.assertRaisesRegex(DailyAutomationError, "地脉模式格式"):
parse_daily_request("地脉 经验")
with self.assertRaisesRegex(DailyAutomationError, "不支持的地脉国家"):
parse_daily_request("地脉 摩拉 天空岛")
with self.assertRaisesRegex(DailyAutomationError, "仅支持"):
parse_daily_request("探索")
def test_domain_alias_resolves_canonical_and_common_names(self):
resolver = DomainAliasResolver(self.alias_path, self.root)
self.assertEqual(resolver.resolve("风本"), "铭记之谷")
self.assertEqual(resolver.resolve("少女套"), "铭记之谷")
self.assertEqual(resolver.resolve("仲夏庭园"), "仲夏庭园")
def test_domain_alias_collision_is_rejected(self):
self._write_json(
self.alias_path,
{
"铭记之谷": ["风本"],
"仲夏庭园": ["风 本"],
},
)
with self.assertRaisesRegex(DailyAutomationError, "同时指向"):
DomainAliasResolver(self.alias_path, self.root).resolve("风本")
def test_domain_run_generates_exact_flow_and_corrects_bgi_config(self):
prepared = self._prepare("秘境 风本")
self.assertEqual(prepared.request.domain_name, "铭记之谷")
self.assertEqual(prepared.task_name, "自动每日(秘境:铭记之谷)")
updates = {update.path.name: update for update in prepared.updates}
corrected = updates["config.json"].data
self.assertFalse(corrected["autoDomainConfig"]["specifyResinUse"])
self.assertEqual(corrected["autoDomainConfig"]["other"], 1)
self.assertEqual(corrected["marker"], "preserved")
managed = updates["直播系统自动每日.json"].data
names = [managed["TaskDefinitions"][task_id] for task_id in managed["TaskOrder"]]
self.assertEqual(
names,
["领取邮件", "合成树脂", "自动秘境", "领取尘歌壶奖励", "领取每日奖励"],
)
self.assertEqual(
managed["TaskOrder"],
[
"mail-template-id",
"craft-template-id",
"domain-template-id",
"serenitea-template-id",
"daily-reward-template-id",
],
)
self.assertFalse(managed["WeeklyDomainEnabled"])
self.assertEqual(managed["DomainName"], "铭记之谷")
self.assertEqual(managed["CompletionAction"], "")
self.assertEqual(managed["PartyName"], "战斗队")
def test_no_mode_skips_only_the_other_task(self):
prepared = self._prepare("")
managed = prepared.updates[-1].data
names = [managed["TaskDefinitions"][task_id] for task_id in managed["TaskOrder"]]
self.assertEqual(
names,
["领取邮件", "合成树脂", "领取尘歌壶奖励", "领取每日奖励"],
)
def test_ley_line_run_sets_all_days_and_respects_craft_toggle(self):
prepared = self._prepare("地脉 摩拉 枫丹", craft_before=False)
updates = {update.path.name: update for update in prepared.updates}
corrected = updates["config.json"].data
self.assertFalse(corrected["autoLeyLineOutcropConfig"]["isGoToSynthesizer"])
managed = updates["直播系统自动每日.json"].data
names = [managed["TaskDefinitions"][task_id] for task_id in managed["TaskOrder"]]
self.assertEqual(
names,
["领取邮件", "自动地脉花", "领取尘歌壶奖励", "领取每日奖励"],
)
self.assertTrue(managed["LeyLineResinExhaustionMode"])
self.assertFalse(managed["LeyLineOpenModeCountMin"])
for day in ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"):
self.assertTrue(managed[f"LeyLineRun{day}"])
self.assertEqual(managed[f"LeyLine{day}Type"], "藏金之花")
self.assertEqual(managed[f"LeyLine{day}Country"], "枫丹")
def test_commission_is_added_as_a_configuration_group_task(self):
prepared = self._prepare("委托")
managed = prepared.updates[-1].data
names = [managed["TaskDefinitions"][task_id] for task_id in managed["TaskOrder"]]
self.assertEqual(
names,
["领取邮件", "合成树脂", "每日委托", "领取尘歌壶奖励", "领取每日奖励"],
)
commission_id = next(
task_id
for task_id, name in managed["TaskDefinitions"].items()
if name == "每日委托"
)
self.assertNotIn(
commission_id,
{
"mail-template-id",
"craft-template-id",
"serenitea-template-id",
"daily-reward-template-id",
},
)
def test_commission_requires_nonempty_group(self):
(self.root / "User" / "ScriptGroup" / "每日委托.json").unlink()
with self.assertRaisesRegex(DailyAutomationError, "每日委托配置组"):
self._prepare("委托")
def test_auto_commission_nova_requires_complete_first_run_config(self):
self._write_json(
self.root / "User" / "ScriptGroup" / "每日委托.json",
{
"name": "每日委托",
"projects": [
{
"folderName": "AutoCommissionNova",
"status": "Enabled",
}
],
},
)
with self.assertRaisesRegex(DailyAutomationError, "缺少 Data/user-config.json"):
self._prepare("委托")
user_config_path = (
self.root
/ "User"
/ "JsScript"
/ "AutoCommissionNova"
/ "Data"
/ "user-config.json"
)
self._write_json(
user_config_path,
{
"party": {
"global": {
"battleTeamName": "战斗队",
"elementTeamName": "",
"battleStrategy": "策略A",
}
}
},
)
with self.assertRaisesRegex(DailyAutomationError, "元素采集队伍"):
self._prepare("委托")
self._write_json(
user_config_path,
{
"party": {
"global": {
"battleTeamName": "战斗队",
"elementTeamName": "采集队",
"battleStrategy": "策略A",
}
}
},
)
self.assertEqual(self._prepare("委托").request.mode, DAILY_MODE_COMMISSION)
def test_auto_commission_nova_can_inject_current_party_for_battle_and_collection(self):
self._write_json(
self.root / "User" / "ScriptGroup" / "每日委托.json",
{
"name": "每日委托",
"projects": [
{
"folderName": "AutoCommissionNova",
"status": "Enabled",
}
],
},
)
user_config_path = (
self.root
/ "User"
/ "JsScript"
/ "AutoCommissionNova"
/ "Data"
/ "user-config.json"
)
self._write_json(
user_config_path,
{
"marker": "preserved",
"party": {
"global": {
"battleTeamName": "",
"elementTeamName": "",
"battleStrategy": "策略A",
}
},
},
)
prepared = self._prepare("委托", commission_use_current_party=True)
self.assertTrue(prepared.requires_current_party)
update = prepare_commission_current_party_update(self.root, "当前队伍")
self.assertEqual(
update.data["party"]["global"]["battleTeamName"],
"当前队伍",
)
self.assertEqual(
update.data["party"]["global"]["elementTeamName"],
"当前队伍",
)
self.assertEqual(update.data["marker"], "preserved")
def test_switch_party_updates_only_target_setting(self):
update = prepare_switch_party_update(self.root, "深渊队")
project = update.data["projects"][0]
self.assertEqual(project["jsScriptSettingsObject"]["partyName"], "深渊队")
self.assertTrue(project["jsScriptSettingsObject"]["debug"])
def test_edit_party_accepts_separators_and_contiguous_names(self):
separated, display = resolve_party_members(
self.root,
"芙宁娜 爱可菲 纳西妲 雷电将军",
)
self.assertEqual(display, ["芙宁娜", "爱可菲", "纳西妲", "雷电将军"])
contiguous, contiguous_display = resolve_party_members(
self.root,
"芙宁娜爱可菲纳西妲雷电将军",
)
self.assertEqual(contiguous, separated)
self.assertEqual(contiguous_display, display)
update, names = prepare_edit_party_update(
self.root,
"芙宁娜、爱可菲、纳西妲、雷电将军",
)
settings = update.data["projects"][0]["jsScriptSettingsObject"]
self.assertEqual(names, ("芙宁娜", "爱可菲", "纳西妲", "雷电将军"))
self.assertEqual(settings["position4"], "雷-长枪-雷电将军")
def test_edit_party_supports_auto_switch_roles_67_character_data(self):
self._write_json(
self.root / "User" / "JsScript" / "AutoSwitchRoles" / "settings.json",
[
{"name": f"position{index}", "type": "input-text", "default": ""}
for index in range(1, 5)
],
)
self._write_json(
self.root / "User" / "JsScript" / "AutoSwitchRoles" / "combat_avatar.json",
[
{"name": "神里绫华", "alias": ["绫华"]},
{"name": "申鹤", "alias": []},
{"name": "枫原万叶", "alias": ["万叶"]},
{"name": "珊瑚宫心海", "alias": ["心海"]},
],
)
update, names = prepare_edit_party_update(
self.root,
"绫华 申鹤 万叶 心海",
)
settings = update.data["projects"][0]["jsScriptSettingsObject"]
self.assertEqual(names, ("神里绫华", "申鹤", "枫原万叶", "珊瑚宫心海"))
self.assertEqual(
[settings[f"position{index}"] for index in range(1, 5)],
["神里绫华", "申鹤", "枫原万叶", "珊瑚宫心海"],
)
resolved, display = resolve_party_members(
self.root,
"神里绫华申鹤枫原万叶珊瑚宫心海",
)
self.assertEqual(resolved, list(display))
def test_edit_party_rejects_ambiguous_combat_avatar_alias(self):
self._write_json(
self.root / "User" / "JsScript" / "AutoSwitchRoles" / "settings.json",
[
{"name": f"position{index}", "type": "input-text", "default": ""}
for index in range(1, 5)
],
)
self._write_json(
self.root / "User" / "JsScript" / "AutoSwitchRoles" / "combat_avatar.json",
[
{"name": "角色甲", "alias": ["同名"]},
{"name": "角色乙", "alias": ["同名"]},
{"name": "角色丙", "alias": []},
{"name": "角色丁", "alias": []},
{"name": "角色戊", "alias": []},
],
)
with self.assertRaisesRegex(DailyAutomationError, "未知或有歧义"):
resolve_party_members(self.root, "同名 角色丙 角色丁 角色戊")
def test_edit_party_requires_four_distinct_known_members(self):
with self.assertRaisesRegex(DailyAutomationError, "必须是4人"):
resolve_party_members(self.root, "芙宁娜 爱可菲 纳西妲")
with self.assertRaisesRegex(DailyAutomationError, "不能重复"):
resolve_party_members(self.root, "芙宁娜 芙宁娜 纳西妲 雷电将军")
with self.assertRaisesRegex(DailyAutomationError, "未知"):
resolve_party_members(self.root, "芙宁娜 爱可菲 纳西妲 不存在")
def test_apply_json_updates_writes_managed_files(self):
prepared = self._prepare("秘境 风本")
apply_json_updates(prepared.updates)
bgi_config = json.loads((self.root / "User" / "config.json").read_text(encoding="utf-8"))
managed = json.loads(
(self.root / "User" / "OneDragon" / "直播系统自动每日.json").read_text(encoding="utf-8")
)
self.assertFalse(bgi_config["autoDomainConfig"]["specifyResinUse"])
self.assertEqual(managed["DomainName"], "铭记之谷")
if __name__ == "__main__":
unittest.main()
+43 -1
View File
@@ -1,8 +1,10 @@
import asyncio
import logging
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from app.danmu_queue import BgiLogMonitor, WebServer
@@ -69,5 +71,45 @@ class BgiLogDisplayFormatTests(unittest.TestCase):
)
class BgiLogCompletionTests(unittest.IsolatedAsyncioTestCase):
async def test_configuration_group_marker_still_finishes_current_group(self):
monitor = BgiLogMonitor(".", logging.getLogger("test-bgi-group-log"))
callback = AsyncMock()
monitor.set_finish_callback(callback)
with patch.object(monitor, "_get_current_log_size", return_value=0):
monitor.set_current_group("薄荷", "group-run")
monitor._process_line('配置组 "薄荷" 执行结束')
await asyncio.sleep(0)
callback.assert_awaited_once_with("薄荷", "group-run")
self.assertIsNone(monitor._current_group)
async def test_one_dragon_marker_finishes_managed_daily(self):
monitor = BgiLogMonitor(".", logging.getLogger("test-bgi-one-dragon-log"))
callback = AsyncMock()
monitor.set_finish_callback(callback)
with patch.object(monitor, "_get_current_log_size", return_value=0):
monitor.set_current_one_dragon("自动每日(委托)", "daily-run")
monitor._process_line("一条龙和配置组任务结束")
await asyncio.sleep(0)
callback.assert_awaited_once_with("自动每日(委托)", "daily-run")
self.assertIsNone(monitor._current_group)
async def test_child_group_completion_does_not_finish_one_dragon_early(self):
monitor = BgiLogMonitor(".", logging.getLogger("test-bgi-child-group-log"))
callback = AsyncMock()
monitor.set_finish_callback(callback)
with patch.object(monitor, "_get_current_log_size", return_value=0):
monitor.set_current_one_dragon("自动每日", "daily-run")
monitor._process_line('配置组 "每日委托" 执行结束')
await asyncio.sleep(0)
callback.assert_not_awaited()
self.assertEqual(monitor._current_group, "自动每日")
if __name__ == "__main__":
unittest.main()
+353
View File
@@ -0,0 +1,353 @@
import asyncio
import logging
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, call, patch
from app.bettergi_current_party import (
CurrentPartyReadError,
CurrentPartyReadResult,
PreparedCurrentPartyRead,
)
from app.bettergi_daily import (
DailyAutomationError,
DailyRequest,
JsonUpdate,
PreparedDailyRun,
)
from app.danmu_queue import (
COMMAND_ALLOWED_ROLE_DEFAULTS,
BetterGIRunner,
CommandHandler,
)
class DailyCommandIntegrationTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.config = SimpleNamespace(
data={
"commands": {
"run": {"enabled": True, "aliases": ["执行", ""]},
"daily": {"enabled": True, "aliases": ["自动每日"]},
"switch_party": {
"enabled": True,
"aliases": ["切换队伍", "更换队伍"],
},
"edit_party": {
"enabled": True,
"aliases": ["修改队员", "更换队员"],
},
}
},
admin_uids=set(),
bettergi_work_dir=str(self.root),
daily_cfg={
"one_dragon_template": "默认配置",
"managed_one_dragon_name": "直播系统自动每日",
"ley_line_craft_resin_before": True,
"commission_use_current_party": True,
"current_party_read_timeout_sec": 45,
},
broadcast_cfg={"tts_categories": {"execution": True}},
music_monitor_cfg={"request_player": {}},
)
self.queue_mgr = MagicMock()
self.queue_mgr.state = {
"current_admin_uid": 123,
"login_status": "logged_in",
"current_group": None,
"default_running": False,
}
self.queue_mgr.is_admin.return_value = True
self.queue_mgr.start_group.return_value = {
"success": True,
"msg": "ok",
"run_id": "daily-run",
}
self.runner = MagicMock()
self.runner.kill_bgi = AsyncMock()
self.runner.start_groups = AsyncMock(return_value=True)
self.runner.start_one_dragon = AsyncMock(return_value=True)
self.runner.sync_js_script.return_value = True
self.log_monitor = MagicMock()
self.broadcaster = SimpleNamespace(broadcast=AsyncMock())
self.handler = CommandHandler(
self.config,
MagicMock(),
self.queue_mgr,
self.runner,
self.log_monitor,
MagicMock(),
logging.getLogger("test-daily-command"),
broadcaster=self.broadcaster,
)
def tearDown(self):
self.temp_dir.cleanup()
def _prepared_daily(self, *, requires_current_party: bool = False) -> PreparedDailyRun:
return PreparedDailyRun(
request=DailyRequest(mode="commission" if requires_current_party else "none"),
config_name="直播系统自动每日",
updates=(
JsonUpdate(
self.root / "User" / "OneDragon" / "直播系统自动每日.json",
{"Name": "直播系统自动每日"},
"test",
),
),
requires_current_party=requires_current_party,
)
async def test_daily_replaces_old_task_writes_config_and_starts_one_dragon(self):
self.queue_mgr.state["current_group"] = "旧任务"
prepared = self._prepared_daily()
with (
patch("app.danmu_queue.prepare_daily_run", return_value=prepared) as prepare,
patch("app.danmu_queue.apply_json_updates") as apply_updates,
patch("app.danmu_queue.uuid.uuid4", return_value=SimpleNamespace(hex="daily-run")),
):
await self.handler._cmd_daily(123, "测试用户", "")
prepare.assert_called_once()
self.queue_mgr.interrupt_group.assert_called_once_with(
"daily_replaced",
status="cancelled",
)
self.runner.kill_bgi.assert_awaited_once_with(reason="自动每日配置更新")
apply_updates.assert_called_once_with(prepared.updates)
self.queue_mgr.start_group.assert_called_once_with(
123,
"自动每日",
run_id="daily-run",
)
self.log_monitor.set_current_one_dragon.assert_called_once_with(
"自动每日",
"daily-run",
)
self.runner.start_one_dragon.assert_awaited_once_with("直播系统自动每日")
async def test_daily_validation_failure_does_not_stop_current_task(self):
self.queue_mgr.state["current_group"] = "旧任务"
with patch(
"app.danmu_queue.prepare_daily_run",
side_effect=DailyAutomationError("未找到一条龙模板"),
):
await self.handler._cmd_daily(123, "测试用户", "秘境 风本")
self.queue_mgr.interrupt_group.assert_not_called()
self.runner.kill_bgi.assert_not_awaited()
self.runner.start_one_dragon.assert_not_awaited()
async def test_daily_commission_reads_current_party_before_starting(self):
prepared = self._prepared_daily(requires_current_party=True)
reader_update = JsonUpdate(
self.root / "User" / "ScriptGroup" / "直播系统读取当前队伍.json",
{"projects": []},
"reader",
)
reader = PreparedCurrentPartyRead(
request_id="daily-run",
group_name="直播系统读取当前队伍",
status_path=self.root / "status.json",
updates=(reader_update,),
)
party_update = JsonUpdate(
self.root / "User" / "JsScript" / "AutoCommissionNova" / "Data" / "user-config.json",
{
"party": {
"global": {
"battleTeamName": "好感队",
"elementTeamName": "好感队",
}
}
},
"party",
)
capture = AsyncMock(return_value="好感队")
with (
patch("app.danmu_queue.prepare_daily_run", return_value=prepared) as prepare,
patch("app.danmu_queue.prepare_current_party_read", return_value=reader),
patch(
"app.danmu_queue.prepare_commission_current_party_update",
return_value=party_update,
) as prepare_party,
patch("app.danmu_queue.apply_json_updates") as apply_updates,
patch.object(self.handler, "_capture_current_party_name", capture),
patch("app.danmu_queue.uuid.uuid4", return_value=SimpleNamespace(hex="daily-run")),
):
await self.handler._cmd_daily(123, "测试用户", "委托")
self.assertTrue(prepare.call_args.kwargs["commission_use_current_party"])
capture.assert_awaited_once_with(reader, 45)
prepare_party.assert_called_once_with(str(self.root), "好感队")
apply_updates.assert_called_once_with((party_update, *prepared.updates))
self.runner.start_one_dragon.assert_awaited_once_with("直播系统自动每日")
async def test_daily_commission_reader_failure_does_not_start_billed_task(self):
prepared = self._prepared_daily(requires_current_party=True)
reader = PreparedCurrentPartyRead(
request_id="daily-run",
group_name="直播系统读取当前队伍",
status_path=self.root / "status.json",
updates=(),
)
with (
patch("app.danmu_queue.prepare_daily_run", return_value=prepared),
patch("app.danmu_queue.prepare_current_party_read", return_value=reader),
patch.object(
self.handler,
"_capture_current_party_name",
new=AsyncMock(side_effect=CurrentPartyReadError("未识别到队伍名称")),
),
patch("app.danmu_queue.apply_json_updates") as apply_updates,
patch("app.danmu_queue.uuid.uuid4", return_value=SimpleNamespace(hex="daily-run")),
):
await self.handler._cmd_daily(123, "测试用户", "委托")
apply_updates.assert_not_called()
self.queue_mgr.start_group.assert_not_called()
self.runner.start_one_dragon.assert_not_awaited()
async def test_current_party_capture_syncs_group_and_waits_for_matching_status(self):
reader_update = JsonUpdate(
self.root / "User" / "ScriptGroup" / "直播系统读取当前队伍.json",
{"projects": []},
"reader",
)
reader = PreparedCurrentPartyRead(
request_id="request-123",
group_name="直播系统读取当前队伍",
status_path=self.root / "status.json",
updates=(reader_update,),
)
with (
patch("app.danmu_queue.apply_json_updates") as apply_updates,
patch(
"app.danmu_queue.read_current_party_status",
return_value=CurrentPartyReadResult("好感队", ("好感队",)),
),
patch("app.danmu_queue.asyncio.sleep", new=AsyncMock()),
):
party_name = await self.handler._capture_current_party_name(reader, 45)
self.assertEqual(party_name, "好感队")
self.runner.sync_js_script.assert_called_once_with("LiveCurrentParty")
apply_updates.assert_called_once_with(reader.updates)
self.runner.start_groups.assert_awaited_once_with(["直播系统读取当前队伍"])
async def test_daily_requires_confirmed_login_before_preparing_files(self):
self.queue_mgr.state["login_status"] = "confirming"
with patch("app.danmu_queue.prepare_daily_run") as prepare:
await self.handler._cmd_daily(123, "测试用户", "")
prepare.assert_not_called()
self.runner.kill_bgi.assert_not_awaited()
async def test_daily_start_failure_clears_monitor_and_enters_reset_flow(self):
prepared = self._prepared_daily()
self.runner.start_one_dragon.return_value = False
reset = AsyncMock()
self.handler.system = SimpleNamespace(_reset_wait_and_retry_login=reset)
with (
patch("app.danmu_queue.prepare_daily_run", return_value=prepared),
patch("app.danmu_queue.apply_json_updates"),
patch("app.danmu_queue.uuid.uuid4", return_value=SimpleNamespace(hex="daily-run")),
):
await self.handler._cmd_daily(123, "测试用户", "")
self.log_monitor.set_current_one_dragon.assert_has_calls(
[call("自动每日", "daily-run"), call(None)]
)
reset.assert_awaited_once_with(
123,
"测试用户",
"自动每日启动失败,正在关闭原神并启动扫码上号",
)
async def test_switch_party_updates_group_then_reuses_group_execution(self):
update = JsonUpdate(
self.root / "User" / "ScriptGroup" / "切换队伍.json",
{"projects": []},
"test",
)
with (
patch("app.danmu_queue.prepare_switch_party_update", return_value=update),
patch("app.danmu_queue.apply_json_updates") as apply_updates,
patch("app.danmu_queue.uuid.uuid4", return_value=SimpleNamespace(hex="party-run")),
patch.object(
self.handler,
"_resolve_group_name",
return_value={
"success": True,
"name": "切换队伍",
"matched": False,
"suggestions": [],
},
),
patch.object(self.handler, "_group_uses_nahida_collect", return_value=False),
):
await self.handler._cmd_switch_party(123, "测试用户", "永冻队")
apply_updates.assert_called_once_with((update,))
self.queue_mgr.start_group.assert_called_once_with(
123,
"切换队伍",
run_id="party-run",
)
self.runner.start_groups.assert_awaited_once_with(["切换队伍"])
def test_execution_command_aliases_support_no_space_arguments(self):
self.assertEqual(self.handler._split_command_text("执行薄荷"), ("执行", "薄荷"))
self.assertEqual(
self.handler._split_command_text("自动每日秘境 风本"),
("自动每日", "秘境 风本"),
)
self.assertEqual(
self.handler._split_command_text("更换队伍永冻队"),
("更换队伍", "永冻队"),
)
self.assertEqual(
self.handler._split_command_text("修改队员芙宁娜纳西妲钟离雷电将军"),
("修改队员", "芙宁娜纳西妲钟离雷电将军"),
)
def test_new_command_permissions_match_run(self):
for key in ("daily", "switch_party", "edit_party"):
self.assertEqual(
COMMAND_ALLOWED_ROLE_DEFAULTS[key],
COMMAND_ALLOWED_ROLE_DEFAULTS["run"],
)
class BetterGIRunnerOneDragonTests(unittest.IsolatedAsyncioTestCase):
async def test_start_one_dragon_uses_supported_cli_shape(self):
runner = BetterGIRunner(
r"C:\BetterGI\BetterGI.exe",
r"C:\BetterGI",
logging.getLogger("test-one-dragon-runner"),
)
create_process = AsyncMock(return_value=SimpleNamespace())
with (
patch("app.danmu_queue.asyncio.create_subprocess_exec", create_process),
patch("app.danmu_queue.asyncio.sleep", new=AsyncMock()),
):
result = await runner.start_one_dragon("直播系统自动每日")
self.assertTrue(result)
create_process.assert_awaited_once_with(
r"C:\BetterGI\BetterGI.exe",
"startOneDragon",
"直播系统自动每日",
cwd=r"C:\BetterGI",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
if __name__ == "__main__":
unittest.main()