Skip to content

fix(jsonc): 处理 Sourcery 复查意见——超大整数、内存占用、同名窗口 - #42

Merged
KhazixW2 merged 3 commits into
mainfrom
fix/jsonc-review-fixes
Nov 8, 2026
Merged

KhazixW2 merged 3 commits into
mainfrom
fix/jsonc-review-fixes

Conversation

@KhazixW2

@KhazixW2 KhazixW2 commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #41 —— 处理 Sourcery 在 #41 上提出的 5 条复查意见。

逐条实测后处理 3 条真问题,另 2 条确认为误报。

已修(3 条)

1. 超大整数绕过验证(maa_mcp/jsonc.py)

Python 3.10.7+ 限制整数字符串的转换位数(默认 4300 位);3.10.12+ / 3.11.4+ /
3.12+ 的 json C scanner 会强制执行并抛 ValueError。它不是 JSONDecodeError,
会逃出 save_pipeline 与 load_agent_config 的 except 分支,直接以异常形式冲到
MCP 工具调用之外。

现在在 loads 里把它转成 JsoncDecodeError(invalid number: Exceeds the limit ...)。

注:本地 Python 3.10.6 没有这个限制,测试用 sys.set_int_max_str_digits 显式开启,
并在解释器不支持时跳过;CI 的 3.10.18 / 3.11.x / 3.12.x 会真正跑到这条路径。

2. 大输入耗尽内存(maa_mcp/jsonc.py)

预处理原先用 list(text) 逐字符复制,指针数组让峰值内存达到文件大小的 ~9 倍
(7.3 MB 输入 → 66 MB 峰值)。改为按片段拼接(注释只占文件一小部分,只复制
变动区间),峰值降到 ~2 倍:

输入大小: 7.34 MB
新实现峰值: 14.69 MB
旧实现峰值(仅 list(text)+join): 66.09 MB   → 4.5x

3. 同名窗口选错(tests/test_performance_stress.py)

压力测试按标题连接窗口,而 connect_window 走的是名字注册表
(register_by_name),同名窗口会互相覆盖——显式指定标题并不能消除歧义,可能把
输入发给另一个同名窗口。现在标题在 find_window_list() 结果中出现多次时直接跳过;
并且在 connect_window 之后再核对一次实际连接到的 hwnd 与校验过的 hwnd 是否
一致,不一致就跳过(覆盖查找与连接之间的 TOCTOU 窗口)。

确认为误报,未改(2 条)

4. 「字面 emoji 键与代理对转义键撞车会静默覆盖」

标准库 json 的 C scanner 本身就会合成代理对——{"😀": 1, "😀": 2}
在重复键检查时两个键已经是同一个,标准库与 MaaFramework 都会拒绝。我们与
MaaFramework 行为一致,不存在覆盖。

不过仍在 _sanitize_surrogates 里补了显式的撞车检查作为防御:即使换成不做代理
对合成的纯 Python scanner,也不会出现静默覆盖。

5. 「仅 CR 结尾的行注释吞掉后续 JSON」

meojson 的行注释扫描同样只认 \n——实测 MaaFramework 对 // c\r{"A": ...}
也是拒绝的(整段被吞掉)。若改成也认 \r,MaaMCP 就会接受 MaaFramework 拒绝的
文件,破坏「同一份文件在两边解析结果相同」的前提。

保持现状,并在 _strip_comments 的 docstring 与测试里把这个行为写清楚,避免以后
被当成 bug「修掉」。

第二轮复查补充

Sourcery 在本 PR 上又复审了一轮,追加 3 条意见,均已处理:

  • 测试用例把 sys.set_int_max_str_digits 设成 20:低于解释器允许的最小非零值
    (640),会直接抛 ValueError 而不是进入被测分支。改为 setter(640) +
    700 位数字字面量,并补一条不依赖解释器版本的 monkeypatch 转换测试。
  • 注释剥离的断言过弱:原来只断言长度不变,现在同时断言注释内容确实被
    替换成空白、且结果可被标准库解析。
  • 窗口校验存在 TOCTOU:见上文第 3 条,连接后补 hwnd 复核。

验证

🤖 Generated with Claude Code

复查 #41 提出 5 条,逐条实测后处理 3 条:

1. 超大整数绕过验证(真问题):Python 3.10.7+ 限制整数字符串转换位数(默认
   4300 位),3.10.12+/3.11.4+/3.12+ 的 json C scanner 会强制执行并抛
   ValueError——它不是 JSONDecodeError,会逃出 save_pipeline 与
   load_agent_config 的 except 分支直接冲到 MCP 工具调用之外。现在转成
   JsoncDecodeError。
2. 大输入耗尽内存(真问题):预处理原先用 list(text) 逐字符复制,指针数组让
   峰值内存达到文件大小的 ~9 倍。改为按片段拼接后降到 ~2 倍(7.3 MB 输入实测
   66 MB → 14.7 MB)。
3. 同名窗口选错(真问题):压力测试按标题连接窗口,而 connect_window 走名字
   注册表,同名窗口会互相覆盖。标题不唯一时跳过,避免误操作其他窗口。

另两条实测为误报,未改:
- 「字面 emoji 键与代理对转义键撞车会静默覆盖」:标准库 json 的 C scanner
  本身就会合成代理对,两个键在重复键检查时已是同一个,we 与 MaaFramework
  都会拒绝。仍在 _sanitize_surrogates 里加了显式的撞车检查作为防御。
- 「仅 CR 结尾的行注释吞掉后续 JSON」:meojson 的行注释扫描同样只认 \n,
  仅用 CR 换行的文件在 MaaFramework 里也会被整段吞掉。改成也认 \r 会让
  MaaMCP 接受 MaaFramework 拒绝的文件,破坏 parity,保持现状并补测试。

验证:真 MaaFramework 32 例边界对照无回归;20000 例随机合法 JSON 与标准库
差分 0 处不一致;pytest 329 passed / 16 skipped

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

你好——我发现了 3 个问题

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

## Individual Comments

### Comment 1
<location path="tests/test_performance_stress.py" line_range="274" />
<code_context>
                     )
+                # connect_window 按标题查名字注册的表,同名窗口会互相覆盖,
+                # 可能连到另一个同名窗口上去——标题不唯一时宁可跳过。
+                if window_list.count(window_name) > 1:
+                    pytest.skip(
+                        f"窗口标题 {window_name!r} 不唯一(同名窗口 "
</code_context>
<issue_to_address>
**Stress test targets the wrong window**

When another `find_window_list` call updates the registry between the uniqueness check and `connect_window`, `connect_window` resolves the title through the updated registry and connects to the newly registered same-title window, so the stress test sends input to the wrong application.

Make discovery and connection atomic, or connect using a stable window identity instead of looking up the title after the check.
</issue_to_address>

### Comment 2
<location path="tests/test_jsonc.py" line_range="251" />
<code_context>
+            pytest.skip("当前解释器没有整数位数限制")
+
+        old = sys.get_int_max_str_digits()
+        setter(20)
+        try:
+            try:
</code_context>
<issue_to_address>
**Huge-integer test fails before parsing**

When the test runs on an interpreter that provides `sys.set_int_max_str_digits`, `setter(20)` raises `ValueError` because the minimum nonzero limit is 640, before the `try` block, so `loads` is never exercised and the test fails.

Set the limit to a supported value such as 640 and use an integer longer than that limit in the test.

Also at `tests/test_jsonc.py:250`.
</issue_to_address>

### Comment 3
<location path="tests/test_jsonc.py" line_range="237" />
<code_context>
+        """注释替换成等长空白,长度不变(错误位置才对得上原文)。"""
+        text = '{\n// 注释\n"A": 1 /* 块注释 */\n}'
+
+        assert len(_strip_comments(text)) == len(text)
+
+    def test_huge_integer_reported_as_decode_error(self) -> None:
</code_context>
<issue_to_address>
**Comment removal regression passes**

When `_strip_comments` stops removing comments but still returns a string of the input length, `test_comment_stripping_preserves_length` asserts only equal lengths, so `_strip_comments` returning its input unchanged passes despite leaving both comments intact.

Also assert that the returned text has the comments blanked while preserving the input length.
</issue_to_address>

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

Hey - I've found 3 issues

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

## Individual Comments

### Comment 1
<location path="tests/test_performance_stress.py" line_range="274" />
<code_context>
                     )
+                # connect_window 按标题查名字注册的表,同名窗口会互相覆盖,
+                # 可能连到另一个同名窗口上去——标题不唯一时宁可跳过。
+                if window_list.count(window_name) > 1:
+                    pytest.skip(
+                        f"窗口标题 {window_name!r} 不唯一(同名窗口 "
</code_context>
<issue_to_address>
**Stress test targets the wrong window**

When another `find_window_list` call updates the registry between the uniqueness check and `connect_window`, `connect_window` resolves the title through the updated registry and connects to the newly registered same-title window, so the stress test sends input to the wrong application.

Make discovery and connection atomic, or connect using a stable window identity instead of looking up the title after the check.
</issue_to_address>

### Comment 2
<location path="tests/test_jsonc.py" line_range="251" />
<code_context>
+            pytest.skip("当前解释器没有整数位数限制")
+
+        old = sys.get_int_max_str_digits()
+        setter(20)
+        try:
+            try:
</code_context>
<issue_to_address>
**Huge-integer test fails before parsing**

When the test runs on an interpreter that provides `sys.set_int_max_str_digits`, `setter(20)` raises `ValueError` because the minimum nonzero limit is 640, before the `try` block, so `loads` is never exercised and the test fails.

Set the limit to a supported value such as 640 and use an integer longer than that limit in the test.

Also at `tests/test_jsonc.py:250`.
</issue_to_address>

### Comment 3
<location path="tests/test_jsonc.py" line_range="237" />
<code_context>
+        """注释替换成等长空白,长度不变(错误位置才对得上原文)。"""
+        text = '{\n// 注释\n"A": 1 /* 块注释 */\n}'
+
+        assert len(_strip_comments(text)) == len(text)
+
+    def test_huge_integer_reported_as_decode_error(self) -> None:
</code_context>
<issue_to_address>
**Comment removal regression passes**

When `_strip_comments` stops removing comments but still returns a string of the input length, `test_comment_stripping_preserves_length` asserts only equal lengths, so `_strip_comments` returning its input unchanged passes despite leaving both comments intact.

Also assert that the returned text has the comments blanked while preserving the input length.
</issue_to_address>

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

)
# connect_window 按标题查名字注册的表,同名窗口会互相覆盖,
# 可能连到另一个同名窗口上去——标题不唯一时宁可跳过。
if window_list.count(window_name) > 1:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 中 · 压力测试针对了错误的窗口

如果另一次 find_window_list 调用在唯一性检查与 connect_window 之间更新了注册表,connect_window 会通过更新后的注册表解析标题,并连接到新注册的同名窗口,因此压力测试会向错误的应用程序发送输入。

应将发现窗口和连接窗口设为原子操作,或者使用稳定的窗口标识进行连接,而不是在检查后再查找标题。

面向 AI 代理的提示
在 `tests/test_performance_stress.py` 第 274 行:

**压力测试针对了错误的窗口**

如果另一次 `find_window_list` 调用在唯一性检查与 `connect_window` 之间更新了注册表,`connect_window` 会通过更新后的注册表解析标题,并连接到新注册的同名窗口,因此压力测试会向错误的应用程序发送输入。

应将发现窗口和连接窗口设为原子操作,或者使用稳定的窗口标识进行连接,而不是在检查后再查找标题。
Original comment in English

🟡 Medium · Stress test targets the wrong window

When another find_window_list call updates the registry between the uniqueness check and connect_window, connect_window resolves the title through the updated registry and connects to the newly registered same-title window, so the stress test sends input to the wrong application.

Make discovery and connection atomic, or connect using a stable window identity instead of looking up the title after the check.

Prompt for AI agents
In `tests/test_performance_stress.py` at line 274:

**Stress test targets the wrong window**

When another `find_window_list` call updates the registry between the uniqueness check and `connect_window`, `connect_window` resolves the title through the updated registry and connects to the newly registered same-title window, so the stress test sends input to the wrong application.

Make discovery and connection atomic, or connect using a stable window identity instead of looking up the title after the check.

Comment thread tests/test_jsonc.py Resolved
Comment thread tests/test_jsonc.py Resolved
CI 失败原因:sys.set_int_max_str_digits(20) 抛 ValueError
(解释器要求上限至少 640),改为 640 + 700 位整数。

另补一个 monkeypatch 测试:不依赖解释器版本与限制是否生效,直接验证
loads 会把 json.loads 抛出的 ValueError 转成 JsoncDecodeError。
Sourcery 在 #42 上提了 3 条,其中 setter(20) 那条已在上一提交修掉
(解释器要求位数上限至少 640),另两条:

1. test_comment_stripping_preserves_length 断言太弱:只断言长度相等,
   _strip_comments 原样返回输入也能通过。现在同时断言注释内容确实被
   置空、且抹掉后仍是合法 JSON。

2. 压力测试的 TOCTOU:标题唯一性检查与 connect_window 之间,注册表若被
   别的 find_window_list() 刷新过,可能连到另一个同名窗口。现在连上之后
   核对 controller 的 hwnd 与校验过的窗口一致,对不上就跳过,不发任何输入。
@KhazixW2
KhazixW2 merged commit e83d6bd into main Nov 8, 2026
10 checks passed
@KhazixW2
KhazixW2 deleted the fix/jsonc-review-fixes branch November 8, 2026 06:16
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