mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(tooling): 补全PR测试报告解析并修复并发测试
- 更新 gframework-pr-review 脚本以提取 CTRF 测试摘要和失败用例详情 - 修复 SettingsModelTests 在 NET9+ 下错误使用 Monitor 持锁的并发测试语义 - 同步 analyzer-warning-reduction 的 active todo 与 trace 真值
This commit is contained in:
parent
1f560635a8
commit
1c87272f6b
@ -62,6 +62,7 @@ The script should produce:
|
||||
- Pre-merge failed checks, if present
|
||||
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
|
||||
- Test summary, including failed-test signals when present
|
||||
- Detailed failed-test rows from GitHub Test Reporter / CTRF comments when the PR comment includes `Name` / `Failure Message` content
|
||||
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
|
||||
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
|
||||
|
||||
|
||||
@ -257,6 +257,11 @@ def strip_markdown_links(text: str) -> str:
|
||||
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||
|
||||
|
||||
def strip_markdown_images(text: str) -> str:
|
||||
"""Drop Markdown image syntax while keeping surrounding text readable."""
|
||||
return re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text)
|
||||
|
||||
|
||||
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
|
||||
"""Extract text between a start marker and the earliest matching end marker."""
|
||||
start = text.find(start_marker)
|
||||
@ -486,43 +491,190 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
|
||||
return report
|
||||
|
||||
|
||||
def clean_markdown_table_cell(text: str) -> str:
|
||||
"""Normalize a Markdown table cell for structured parsing."""
|
||||
cleaned = strip_markdown_images(strip_markdown_links(html.unescape(text)))
|
||||
cleaned = cleaned.replace("\xa0", " ")
|
||||
cleaned = cleaned.replace("**", "").replace("*", "").replace("`", "")
|
||||
return collapse_whitespace(cleaned)
|
||||
|
||||
|
||||
def parse_int_from_text(text: str) -> int | None:
|
||||
"""Extract the first integer value from text."""
|
||||
match = re.search(r"\d+", text)
|
||||
return int(match.group(0)) if match else None
|
||||
|
||||
|
||||
def parse_duration_from_text(text: str) -> str:
|
||||
"""Extract a duration token from text when present."""
|
||||
match = re.search(r"\d+(?:\.\d+)?(?:ms|s|m|h)", text)
|
||||
if match is not None:
|
||||
return match.group(0)
|
||||
|
||||
return collapse_whitespace(text)
|
||||
|
||||
|
||||
def parse_markdown_table(table_text: str) -> tuple[list[str], list[list[str]]]:
|
||||
"""Parse a Markdown table into header cells and row cells."""
|
||||
lines = [line.strip() for line in table_text.splitlines() if line.strip().startswith("|")]
|
||||
if len(lines) < 2:
|
||||
return [], []
|
||||
|
||||
headers = [clean_markdown_table_cell(cell) for cell in lines[0].strip("|").split("|")]
|
||||
rows: list[list[str]] = []
|
||||
for line in lines[2:]:
|
||||
cells = [clean_markdown_table_cell(cell) for cell in line.strip("|").split("|")]
|
||||
if cells:
|
||||
rows.append(cells)
|
||||
|
||||
return headers, rows
|
||||
|
||||
|
||||
def extract_markdown_table_after_heading(block: str, heading: str) -> tuple[list[str], list[list[str]]]:
|
||||
"""Extract the first Markdown table that appears after a heading."""
|
||||
section = extract_section(block, heading, ["\n### ", "\n#### ", "\n<details>", "\n<table>", "\n<sub>"])
|
||||
if section is None:
|
||||
return [], []
|
||||
|
||||
table_match = re.search(r"(\|.*\|\n\|[-| :]+\|\n(?:\|.*\|\n?)*)", section, re.S)
|
||||
if table_match is None:
|
||||
return [], []
|
||||
|
||||
return parse_markdown_table(table_match.group(1))
|
||||
|
||||
|
||||
def normalize_stat_header(header: str) -> str:
|
||||
"""Normalize a human-readable stats header into a stable machine key."""
|
||||
ascii_only = re.sub(r"[^A-Za-z]+", "", header).lower()
|
||||
aliases = {
|
||||
"tests": "tests",
|
||||
"passed": "passed",
|
||||
"failed": "failed",
|
||||
"skipped": "skipped",
|
||||
"pending": "pending",
|
||||
"other": "other",
|
||||
"flaky": "flaky",
|
||||
"duration": "duration",
|
||||
}
|
||||
return aliases.get(ascii_only, ascii_only)
|
||||
|
||||
|
||||
def parse_stats_table(headers: list[str], rows: list[list[str]]) -> dict[str, Any]:
|
||||
"""Convert a parsed Markdown stats table into the report stats shape."""
|
||||
if not headers or not rows:
|
||||
return {}
|
||||
|
||||
first_row = rows[0]
|
||||
stats: dict[str, Any] = {}
|
||||
for header, value in zip(headers, first_row):
|
||||
key = normalize_stat_header(header)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
if key == "duration":
|
||||
stats[key] = parse_duration_from_text(value)
|
||||
continue
|
||||
|
||||
parsed_value = parse_int_from_text(value)
|
||||
if parsed_value is not None:
|
||||
stats[key] = parsed_value
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def normalize_failure_message(text: str) -> str:
|
||||
"""Normalize a failed-test message while preserving the meaningful lines."""
|
||||
cleaned = html.unescape(text)
|
||||
cleaned = re.sub(r"(?i)<br\s*/?>", "\n", cleaned)
|
||||
cleaned = re.sub(r"</?(?:p|div|tbody|thead|tr|td|th|table)>", "\n", cleaned)
|
||||
cleaned = re.sub(r"<[^>]+>", " ", cleaned)
|
||||
lines = [collapse_whitespace(line) for line in cleaned.splitlines()]
|
||||
meaningful_lines = [line for line in lines if line]
|
||||
return "\n".join(meaningful_lines)
|
||||
|
||||
|
||||
def parse_failed_test_summary_list(block: str) -> list[str]:
|
||||
"""Parse the compact failed-tests summary list from CTRF details blocks."""
|
||||
failed_tests_section = re.search(
|
||||
r"<details><summary><strong>\s*Failed Tests.*?</summary>(?P<body>.*?)</details>",
|
||||
block,
|
||||
re.S,
|
||||
)
|
||||
if failed_tests_section is None:
|
||||
return []
|
||||
|
||||
summary_body = strip_markdown_links(strip_markdown_images(html.unescape(failed_tests_section.group("body"))))
|
||||
failed_tests: list[str] = []
|
||||
for raw_line in summary_body.splitlines():
|
||||
line = collapse_whitespace(raw_line)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if "arrow-right" in raw_line:
|
||||
parts = [part.strip() for part in line.split("arrow-right") if part.strip()]
|
||||
candidate = parts[-1] if parts else line
|
||||
elif ">" in line:
|
||||
candidate = line.split(">")[-1].strip()
|
||||
else:
|
||||
candidate = line
|
||||
|
||||
if candidate:
|
||||
failed_tests.append(candidate)
|
||||
|
||||
return failed_tests
|
||||
|
||||
|
||||
def parse_failed_test_details(block: str) -> list[dict[str, str]]:
|
||||
"""Parse the detailed failed-test HTML table from GitHub Test Reporter comments."""
|
||||
details: list[dict[str, str]] = []
|
||||
table_section = re.search(
|
||||
r"### ❌ \*\*Some tests failed!\*\*.*?<tbody>(?P<body>.*?)</tbody>",
|
||||
block,
|
||||
re.S,
|
||||
)
|
||||
if table_section is None:
|
||||
return details
|
||||
|
||||
for name_cell, message_cell in re.findall(r"<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>\s*</tr>", table_section.group("body"), re.S):
|
||||
name = collapse_whitespace(strip_tags(html.unescape(name_cell))).lstrip("❌").strip()
|
||||
failure_message = normalize_failure_message(message_cell)
|
||||
if name:
|
||||
details.append(
|
||||
{
|
||||
"name": name,
|
||||
"failure_message": failure_message,
|
||||
}
|
||||
)
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def parse_test_report(block: str) -> dict[str, Any]:
|
||||
"""Parse a CTRF or GitHub test-reporter comment block."""
|
||||
report: dict[str, Any] = {
|
||||
"raw": block.strip(),
|
||||
"stats": {},
|
||||
"failed_tests": [],
|
||||
"failed_test_details": [],
|
||||
"has_failed_tests": False,
|
||||
}
|
||||
|
||||
summary_row_match = re.search(
|
||||
r"\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|"
|
||||
r"\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?([^\|]+?)\*?\*?\s*\|",
|
||||
block,
|
||||
)
|
||||
if summary_row_match is not None:
|
||||
report["stats"] = {
|
||||
"tests": int(summary_row_match.group(1)),
|
||||
"passed": int(summary_row_match.group(2)),
|
||||
"failed": int(summary_row_match.group(3)),
|
||||
"skipped": int(summary_row_match.group(4)),
|
||||
"other": int(summary_row_match.group(5)),
|
||||
"flaky": int(summary_row_match.group(6)),
|
||||
"duration": summary_row_match.group(7).strip(),
|
||||
}
|
||||
summary_headers, summary_rows = extract_markdown_table_after_heading(block, "### Summary")
|
||||
report["stats"] = parse_stats_table(summary_headers, summary_rows)
|
||||
|
||||
failed_tests_section = extract_section(
|
||||
block,
|
||||
"### Failed Tests",
|
||||
["### Slowest Tests", "### Insights", "<sub>", "[Github Test Reporter]"],
|
||||
)
|
||||
if failed_tests_section:
|
||||
lines = [line.strip("- ").strip() for line in failed_tests_section.splitlines()[1:] if line.strip()]
|
||||
report["failed_tests"] = lines
|
||||
report["has_failed_tests"] = True
|
||||
elif "No failed tests in this run." in block or "All tests passed!" in block:
|
||||
report["failed_tests"] = []
|
||||
report["has_failed_tests"] = False
|
||||
if not report["stats"]:
|
||||
build_headers, build_rows = extract_markdown_table_after_heading(block, "### build-and-test:")
|
||||
report["stats"] = parse_stats_table(build_headers, build_rows)
|
||||
|
||||
failed_test_details = parse_failed_test_details(block)
|
||||
failed_test_names = parse_failed_test_summary_list(block)
|
||||
if not failed_test_names and failed_test_details:
|
||||
failed_test_names = [detail["name"] for detail in failed_test_details]
|
||||
|
||||
report["failed_tests"] = failed_test_names
|
||||
report["failed_test_details"] = failed_test_details
|
||||
failed_count = int(report["stats"].get("failed", 0) or 0)
|
||||
report["has_failed_tests"] = bool(failed_test_names or failed_test_details or failed_count > 0)
|
||||
|
||||
return report
|
||||
|
||||
@ -1103,8 +1255,17 @@ def format_text(
|
||||
lines.append(f"- Report {index}: no structured test stats parsed")
|
||||
|
||||
if report["has_failed_tests"]:
|
||||
for failed_test in report["failed_tests"]:
|
||||
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
||||
failed_test_details = report.get("failed_test_details", [])
|
||||
if failed_test_details:
|
||||
for failed_test_detail in failed_test_details:
|
||||
lines.append(f" Failed test: {truncate_text(failed_test_detail['name'], max_description_length)}")
|
||||
lines.append(
|
||||
" Failure: "
|
||||
f"{truncate_text(failed_test_detail['failure_message'].replace(chr(10), ' | '), max_description_length)}"
|
||||
)
|
||||
else:
|
||||
for failed_test in report["failed_tests"]:
|
||||
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
||||
else:
|
||||
lines.append(" Failed tests: none reported")
|
||||
|
||||
|
||||
@ -137,12 +137,10 @@ public sealed class SettingsModelTests
|
||||
var migrationMapLock = lockField!.GetValue(model);
|
||||
Assert.That(migrationMapLock, Is.Not.Null);
|
||||
|
||||
Task initializeTask;
|
||||
Task registerTask;
|
||||
lock (migrationMapLock!)
|
||||
var tasks = WithSynchronizationLockHeld(migrationMapLock!, () =>
|
||||
{
|
||||
initializeTask = Task.Run(() => model.InitializeAsync());
|
||||
registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
||||
var initializeTask = Task.Run(() => model.InitializeAsync());
|
||||
var registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
||||
|
||||
Thread.Sleep(50);
|
||||
|
||||
@ -151,7 +149,11 @@ public sealed class SettingsModelTests
|
||||
Assert.That(initializeTask.IsCompleted, Is.False);
|
||||
Assert.That(registerTask.IsCompleted, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
return (initializeTask, registerTask);
|
||||
});
|
||||
|
||||
var (initializeTask, registerTask) = tasks;
|
||||
|
||||
await Task.WhenAll(initializeTask, registerTask);
|
||||
|
||||
@ -171,6 +173,35 @@ public sealed class SettingsModelTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以与被测代码相同的同步原语持有反射获取到的锁对象,避免在 .NET 9+ 上把 <see cref="System.Threading.Lock" />
|
||||
/// 退化成 <see cref="Monitor" /> 语义,导致并发测试误判。
|
||||
/// </summary>
|
||||
/// <param name="syncRoot">通过反射读取到的私有锁字段。</param>
|
||||
/// <typeparam name="TResult">持锁代码返回的结果类型。</typeparam>
|
||||
/// <param name="action">持锁期间执行的断言与并发调度逻辑。</param>
|
||||
/// <returns>持锁代码的返回值。</returns>
|
||||
private static TResult WithSynchronizationLockHeld<TResult>(object syncRoot, Func<TResult> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(syncRoot);
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
if (syncRoot is System.Threading.Lock typedLock)
|
||||
{
|
||||
using (typedLock.EnterScope())
|
||||
{
|
||||
return action();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
return action();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSettingsData : ISettingsData
|
||||
{
|
||||
public string Value { get; set; } = "default";
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-079`
|
||||
- 当前阶段:`Phase 79`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-080`
|
||||
- 当前阶段:`Phase 80`
|
||||
- 当前焦点:
|
||||
- `2026-04-27` 已按 PR `#295` 的 latest-head review 收口本地仍成立的问题:`AsyncExtensionsTests` 的 `ArgumentException.ParamName` 契约与 active `ai-plan` 文档过长问题
|
||||
- 当前轮次已重新确认 `origin/main` 基线与 `HEAD` 同为 `617e0bf`;当前已提交 `HEAD` 的 stop metric 仍为 `30 / 50` files、`642` changed lines,本轮 PR review 同步尚未提交入该指标
|
||||
- 当前剩余 warning 主要集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review 收口的低风险边界
|
||||
- `2026-04-27` 已补齐 `$gframework-pr-review` 对 GitHub Test Reporter / CTRF 测试摘要与失败用例详情的提取,当前文本输出可以直接显示失败测试名和 failure message 摘要
|
||||
- `SettingsModelTests.RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 已按 `System.Threading.Lock` 的真实语义修正测试实现,并完成针对性 Release 测试验证
|
||||
- 当前剩余 warning 热点仍集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review / test follow-up 的低风险边界
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
@ -19,12 +19,10 @@
|
||||
- 当前 PR review 真值:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
||||
- 最新结果:成功;当前分支对应 PR 为 `#295`
|
||||
- latest-head review 在本轮修复前共有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 与 active `trace`
|
||||
- 本轮已在本地收口上述两类问题,待提交后可重新执行 `$gframework-pr-review` 确认线程自动收口
|
||||
- 提权后的直接验证当前确认为:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 最新结果:成功;`126 Warning(s)`、`0 Error(s)`
|
||||
- 当前仍可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008`
|
||||
- 当前测试报告输出已能显示 `Summary` 统计、失败测试名称,以及 `Name / Failure Message` 表格中的关键信息
|
||||
- 当前直接验证结果:
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"`
|
||||
- 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
|
||||
- 当前分支 stop-condition 指标:
|
||||
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||
- 最新结果:`30`
|
||||
@ -33,6 +31,7 @@
|
||||
- 当前批次摘要:
|
||||
- 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397`
|
||||
- 当前批次的已完成 slice 明细已迁移到归档,active todo 仅保留恢复真值
|
||||
- 本轮新增内容为 PR review 工具链补强与单个失败测试修正,不扩展 warning reduction 的热点清理边界
|
||||
- 当前建议保留到下一波次的候选:
|
||||
- `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`(单点可修,但文件本身同时承载其他高耦合 warning)
|
||||
- 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数)
|
||||
@ -43,8 +42,8 @@
|
||||
- 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。
|
||||
- `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。
|
||||
- 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。
|
||||
- 当前 PR open thread 的自动收口仍依赖新提交进入远端 PR head。
|
||||
- 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,仅以最新 head review 为准。
|
||||
- 当前 PR open thread 与 CI 失败信号仍依赖新提交进入远端 PR head 才能复核。
|
||||
- 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,同时确认 review thread 与 failed test signal 是否一起收口。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
@ -69,6 +68,6 @@
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 提交本轮 PR review 修复与 `ai-plan` 精简同步,再重跑 `$gframework-pr-review` 验证 PR `#295` 的 open thread 是否随新 head 收口。
|
||||
2. 若后续继续推进 warning reduction,建议另开下一波次,优先明确是否接受 `YamlConfigLoader.cs` 热点触碰,或是否要专门做测试项目 `MA0048` 拆分波次。
|
||||
3. 默认在当前恢复点停下,因为继续推进已不再符合本轮“PR review 收口 + 少文件同步”的边界。
|
||||
1. 提交本轮 `$gframework-pr-review` 解析增强、`SettingsModelTests` 修复与 `ai-plan` 同步。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 open thread 是否已更新为新 head 真值。
|
||||
3. 若后续继续推进 warning reduction,建议另开下一波次处理 `YamlConfigLoader.cs` 热点或测试项目 `MA0048` 拆分波次。
|
||||
|
||||
@ -1,37 +1,38 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-27 — RP-079
|
||||
## 2026-04-27 — RP-080
|
||||
|
||||
### 阶段:按 PR `#295` latest-head review 收口当前仍成立的 open thread
|
||||
### 阶段:补强 `$gframework-pr-review` 的测试报告提取并修复 `SettingsModelTests` 失败用例
|
||||
|
||||
- 触发背景:
|
||||
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应的最新公开 PR 为 `#295`,而不是旧 trace 中记录的 `#291`
|
||||
- latest-head review 仍有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 的参数名契约问题,以及 active trace 过长问题
|
||||
- 用户补充了 GitHub Test Reporter / CTRF 的完整 PR 评论,指出现有 `$gframework-pr-review` 输出虽然能显示 `failed=1`,但没有抓到失败测试名称与详细报错
|
||||
- PR 评论中的真实失败用例为 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache`,报错集中在测试通过反射持锁后两个任务提前完成
|
||||
- 主线程实施:
|
||||
- 修正 `ThrowShouldNotRetry` 中 `ArgumentException` 的 `ParamName` 传递逻辑,并补充断言锁定 `nameof(taskFactory)` 契约
|
||||
- 将 active trace 精简为单一恢复入口,并把 `RP073` 到 `RP078` 的详细过程迁入归档
|
||||
- 同步压缩 active todo,只保留当前恢复点真值与归档指针
|
||||
- 扩展 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 `parse_test_report`,增加 CTRF `Summary` 统计、紧凑 failed-tests 列表、以及 `Name` / `Failure Message` HTML 表格的提取
|
||||
- 更新 `gframework-pr-review` skill 文档的输出预期,明确脚本现在应提取 GitHub Test Reporter / CTRF 的失败详情
|
||||
- 修正 `GFramework.Game.Tests/Setting/SettingsModelTests.cs`,让反射取得的 `_migrationMapLock` 在 `NET9_0_OR_GREATER` 下通过 `System.Threading.Lock.EnterScope()` 持锁,而不是退化成 `Monitor` 语义
|
||||
- 验证里程碑:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
||||
- 结果:成功;确认 PR `#295` latest-head review 在本轮修复前共有 `2` 条 open thread
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:成功;`126 Warning(s)`、`0 Error(s)`
|
||||
- 当前可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008`
|
||||
- `python3 -c "... parse_test_report(...)"`(基于 `/tmp/current-pr-review.json` 的现有原始评论)
|
||||
- 结果:成功;已能解析 `tests=2156`、`passed=2155`、`failed=1`、`duration=35.3s`,并抓到 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 的 failure message
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-v2.json`
|
||||
- 结果:成功;文本输出已直接显示失败测试名与报错摘要
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"`
|
||||
- 结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
|
||||
- 当前结论:
|
||||
- 当前仍成立的 latest-head review 问题已经在本地收口,下一步应以新提交刷新 PR head 再复查
|
||||
- active `ai-plan` 入口现已回到“当前真值 + 归档指针”的体量,符合仓库对恢复文档的要求
|
||||
- `$gframework-pr-review` 现在已经能把 PR 测试评论里的关键失败用例信息直接提出来,不再只停留在 `failed=1`
|
||||
- `SettingsModelTests` 当前失败是测试锁语义与生产代码不一致导致的误判,已在本地复现并修正通过
|
||||
|
||||
## 活跃风险
|
||||
|
||||
- PR review thread 是否自动关闭仍取决于新提交是否进入远端 PR head。
|
||||
- 缓解措施:提交后重新执行 `$gframework-pr-review`,只以新的 latest-head 结果为准。
|
||||
- PR 上的 latest-head review thread 与测试报告仍需要等新提交进入远端后再复核。
|
||||
- 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只以新的 latest-head 和 test report 为准。
|
||||
- `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与 `MA0048` 拆分仍是下一波次的高耦合候选。
|
||||
- 缓解措施:保持本轮边界只处理 PR review 收口,不顺手扩展 warning reduction 范围。
|
||||
- 缓解措施:保持本轮边界只处理 PR review 工具链与失败测试,不顺手扩展 warning reduction 范围。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 完成本轮验证并提交。
|
||||
2. 重新执行 `$gframework-pr-review`,确认 PR `#295` latest-head 是否还有 unresolved thread。
|
||||
1. 完成本轮提交。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 unresolved thread 是否已刷新。
|
||||
|
||||
## 历史归档指针
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user