Skip to content

fix(SwitchOnmyoji): give both switch_role loops a bounded exit - #1842

Open
Enough1122 wants to merge 1 commit into
runhey:devfrom
Enough1122:fix-switch-onmyoji-timeout
Open

Enough1122 wants to merge 1 commit into
runhey:devfrom
Enough1122:fix-switch-onmyoji-timeout

Conversation

@Enough1122

@Enough1122 Enough1122 commented Sep 25, 2026 •

Copy link
Copy Markdown

Summary

SwitchOnmyoji.switch_role had two unconditional while True loops with no exit condition. Add a bounded timeout to each so a stuck page fails fast with a named error instead of spinning until the global GameStuckError watchdog fires.

Real observed failure on client 1.8.66 (versionCode 260610):

23:38:49  [3.3s]Page arrived page_onmyodo
23:38:50  SWITCH ONMYOJI
23:38:54  Click ( 47, 400) @ SO_ONMYOJI_SWITCH
23:38:56  Click ( 52, 408) @ SO_ONMYOJI_SWITCH
23:40:08  WARNING  Wait too long
23:40:10  ERROR    GameStuckError: Wait too long

The three saved stuck-window screenshots show the game sitting on the hero-exchange (英杰交换) sub-page, with 英杰 highlighted instead of 阴阳师. I_ONMYOJI_SWITCH therefore never opens the role list, the loop re-clicks every 0.8s, and the run dies ~70s later on an unrelated watchdog error that names none of the real cause.

Root cause

tasks/Component/SwitchOnmyoji/switch_onmyoji.py — both loops in switch_role are while True; nothing bounds them.

Change

One file, 18 added lines. Each loop now starts a Timer and raises ScriptError after SWITCH_TAB_TIMEOUT / SWITCH_BATTLE_TIMEOUT (20s), with a message that names the failed step.

No sleep added; the timers are plain module.base.timer.Timer instances created per call, so concurrent tasks do not share state.

Verification

Live on the real client, 3 consecutive runs, healthy path unchanged:

healthy#1: switched_ok 3.8s
healthy#2: switched_ok 2.9s
healthy#3: switched_ok 2.6s

Forced-failure path (timeout lowered to 0 so the role list can never be reached):

timeout0: script_error 0.8s | Switch onmyoji: cannot reach the role list, the onmyoji tab was not opened after ...

Unit tests, tests/test_switch_onmyoji.py (5 passed):

  • role list never appears -> ScriptError
  • confirm icon never appears -> ScriptError
  • healthy flow takes no extra clicks
  • ui_click back-path still taken when the confirm icon is missing

Notes

  • The onmyoji/hero asset coordinates themselves are not changed and are not wrong — switching succeeded in all 3 live runs. The bug is only the missing exit condition.
  • tests/tasks/Component/GeneralBattle/test_battle_wait.py fails to import _DEFAULT_PER_BATTLE on this base SHA. Verified pre-existing on the clean baseline, unrelated to this PR.

Sourcery 总结

当无法达到预期的 UI 状态时,让阴阳师角色切换快速失败并提供可操作的错误信息。

错误修复:

  • 为阴阳师角色切换的两个循环都添加超时机制,使卡住的 UI 状态能够抛出包含上下文信息的 ScriptError 异常,而不是无限运行。

增强功能:

  • 保留现有的成功切换流程,同时针对无法打开角色列表和无法确认角色选择分别提供不同的错误信息。

测试:

  • 为两种超时路径、正常角色切换、备用点击方式以及按实例配置的超时行为添加单元测试覆盖。
Original summary in English

Summary by Sourcery

Make onmyoji role switching fail fast with actionable errors when the expected UI state cannot be reached.

Bug Fixes:

  • Bound both onmyoji role-switching loops with timeouts so stuck UI states raise contextual ScriptError exceptions instead of running indefinitely.

Enhancements:

  • Preserve the existing successful switching flow while providing distinct errors for failure to open the role list and failure to confirm role selection.

Tests:

  • Add unit coverage for both timeout paths, healthy switching, fallback clicking, and per-instance timeout behavior.

Both while True loops in switch_role could spin indefinitely. When the game is left on a sub-page of the onmyoji screen (e.g. the hero-exchange page), I_ONMYOJI_SWITCH never opens the role list, so the loop kept clicking until the global ~2min GameStuckError watchdog fired, losing the actual cause. Each loop now raises a ScriptError naming the failure after 20s.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嘿——我发现了 2 个问题

AI 代理提示
请处理本次代码审查中的评论:

## 单独的评论

### 评论 1
<location path="tests/test_switch_onmyoji.py" line_range="15" />
<code_context>
 from module.atom.image import RuleImage
+from module.base.timer import Timer
+from module.exception import ScriptError
 from tasks.Component.SwitchOnmyoji.assets import SwitchOnmyojiAssets
</code_context>
<issue_to_address>
**nitpick:** 新增的 `Timer` 导入未被使用,因此测试模块包含冗余导入;如果 lint 配置将未使用的导入视为错误,检查将失败。

**建议修复:** 删除未使用的 `from module.base.timer import Timer` 导入。
</issue_to_address>

### 评论 2
<location path="tests/test_switch_onmyoji.py" line_range="100-105" />
<code_context>
+    assert task.ui_clicks == 1
+
+
+def test_timeouts_are_not_shared_between_instances():
+    a = ScriptedSwitch(appear_results=[])
+    b = ScriptedSwitch(appear_results=[])
+
+    assert a.SWITCH_TAB_TIMEOUT is not None
+    assert b.SWITCH_TAB_TIMEOUT is not None
</code_context>
<issue_to_address>
**nitpick (testing):** `test_timeouts_are_not_shared_between_instances` 没有测试计时器隔离性:它只断言两个类属性不为 `None`,既没有启动并发的 `switch_role` 调用,也没有检查它们的 `Timer` 实例。即使超时状态被意外共享,该测试也会通过。

**建议修复:** 让两个实例并发执行,或以其他方式验证每次调用都会创建并推进一个独立的计时器。
</issue_to_address>

Sourcery 评估

已批准。


Sourcery 对开源项目免费——如果您喜欢我们的评审,请考虑分享它们 ✨
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/test_switch_onmyoji.py" line_range="15" />
<code_context>
 from module.atom.image import RuleImage
+from module.base.timer import Timer
+from module.exception import ScriptError
 from tasks.Component.SwitchOnmyoji.assets import SwitchOnmyojiAssets
</code_context>
<issue_to_address>
**nitpick:** The added `Timer` import is unused, so the test module contains a redundant import and will fail lint configurations that treat unused imports as errors.

**Suggested fix:** Remove the unused `from module.base.timer import Timer` import.
</issue_to_address>

### Comment 2
<location path="tests/test_switch_onmyoji.py" line_range="100-105" />
<code_context>
+    assert task.ui_clicks == 1
+
+
+def test_timeouts_are_not_shared_between_instances():
+    a = ScriptedSwitch(appear_results=[])
+    b = ScriptedSwitch(appear_results=[])
+
+    assert a.SWITCH_TAB_TIMEOUT is not None
+    assert b.SWITCH_TAB_TIMEOUT is not None
</code_context>
<issue_to_address>
**nitpick (testing):** `test_timeouts_are_not_shared_between_instances` does not test timer isolation: it only asserts that two class attributes are non-`None`, and never starts concurrent `switch_role` calls or inspects their `Timer` instances. The test passes even if timeout state were accidentally shared.

**Suggested fix:** Exercise both instances concurrently or otherwise verify that each invocation creates and advances an independent timer.
</issue_to_address>

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

"""
import pytest

from module.base.timer import Timer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: 新增的 Timer 导入未被使用,因此测试模块包含冗余导入;如果 lint 配置将未使用的导入视为错误,检查将失败。

建议修复: 删除未使用的 from module.base.timer import Timer 导入。

Original comment in English

nitpick: The added Timer import is unused, so the test module contains a redundant import and will fail lint configurations that treat unused imports as errors.

Suggested fix: Remove the unused from module.base.timer import Timer import.

Comment on lines +100 to +105
def test_timeouts_are_not_shared_between_instances():
a = ScriptedSwitch(appear_results=[])
b = ScriptedSwitch(appear_results=[])

assert a.SWITCH_TAB_TIMEOUT is not None
assert b.SWITCH_TAB_TIMEOUT is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (testing): test_timeouts_are_not_shared_between_instances 没有测试计时器隔离性:它只断言两个类属性不为 None,既没有启动并发的 switch_role 调用,也没有检查它们的 Timer 实例。即使超时状态被意外共享,该测试也会通过。

建议修复: 让两个实例并发执行,或以其他方式验证每次调用都会创建并推进一个独立的计时器。

Original comment in English

nitpick (testing): test_timeouts_are_not_shared_between_instances does not test timer isolation: it only asserts that two class attributes are non-None, and never starts concurrent switch_role calls or inspects their Timer instances. The test passes even if timeout state were accidentally shared.

Suggested fix: Exercise both instances concurrently or otherwise verify that each invocation creates and advances an independent timer.

@github-actions

Copy link
Copy Markdown
Contributor

Change Summary

  • 为 SwitchOnmyoji.switch_role 中“等待角色列表出现”和“等待确认回到主界面”两个 while True 分别增加 20 秒 Timer 兜底。
  • 超时从全局 GameStuckError 改为抛出带步骤上下文的 ScriptError。
  • 新增无设备测试,覆盖两条超时路径、正常路径和回退点击路径。

Reconstructed Intent

点击此处展开
  • 这次改动最可能是为了避免页面状态异常、资源失配或停留在错误子页面时持续点击,令角色切换能在有限时间内失败并保留可诊断的失败步骤。
  • 同时保持已有成功切换及确认图标缺失时的回退流程不变。

Observed Constraints

点击此处展开
  • 第一阶段必须能在 battle_dict 的任一图标出现前退出;第二阶段必须能在确认图标出现前退出。
  • 两个计时器在进入各自循环时启动、相互独立;因此最坏等待时间是两个阶段之和,而不是单一全流程 20 秒。
  • Timer.reached() 使用墙上时间,且检查位于每轮截图前;截图/识别或点击调用本身若阻塞,实际返回时间可能超过配置值。
  • ScriptError 的上层处理方式决定这次“快速失败”是否最终转化为重试、重启或人工接管。

Intent Alignment

  • 实现与推断出的 intent 基本一致:两个原本无界的循环都获得了独立的有界退出,成功路径的点击顺序未改变。
  • 未观察到超出该 intent 的明显行为变化,但异常类型从原先最终的全局 watchdog 错误变为 ScriptError,需要确认上层调度对该异常的处理符合预期。

Release Risk

  • 风险等级:中
  • 运行时改动范围小,但 UI 识别/点击卡住时现在会提前结束任务;若上层未按预期处理 ScriptError,可能改变重试或重启行为。
  • 两个阶段各自 20 秒,端到端仍可能等待约 40 秒;这满足“有界”但不等同于全流程 20 秒失败。
  • 当前 check runs 尚在进行中,尚无完成的 CI 结果可供发布判断。

Validation Gaps

点击此处展开
  • 新测试将超时覆盖压到 0 秒,验证了异常分支但没有验证真实 20 秒配置、边界时序或慢截图/点击场景。
  • 测试只断言抛出 ScriptError,未断言两个不同失败阶段的错误上下文,也未验证上层对该异常的处理。
  • test_timeouts_are_not_shared_between_instances 仅检查实例属性非空,没有实际验证计时器状态不会在实例间共享。
  • 建议至少等待 CI 完成,并补充基于可控时钟的阶段边界测试;如项目已有任务调度测试,也应确认 ScriptError 的处理链路。

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by PR Review Intent for #1842 · codex · gpt56 · 8.53 AIC · ⌖ 2.92 AIC · ⊞ 12.1K · ◷

@github-actions

Copy link
Copy Markdown
Contributor

Findings

  • 未发现未通过项

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by PR Review Checklist for #1842 · codex · gpt56 · 14.4 AIC · ⌖ 2.31 AIC · ⊞ 12.1K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant