Compare commits

...

8 Commits

Author SHA1 Message Date
pzhang_zywl 1477dbdd18 fix: step3 _normalize_rule 为缺失 section 的 table/text source 补齐字段 - Closes #53
CI / test (pull_request) Successful in 8s
LLM 生成的 source 有时缺少 section 字段,导致 Layer A schema 验证失败。
在 _normalize_rule 中添加防御性处理:从兄弟 source 或 rule path 推断 section。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:46:59 +08:00
pzhang_zywl 6d0a5284e7 Merge pull request 'fix: [test] QE-Agent bypass 模式完善:自动运行 pipeline + pytest + curl - Closes #51' (#52) from test/issue-51 into main
CI / test (push) Successful in 11s
2026-06-02 15:20:04 +08:00
pzhang_zywl b193aaf8f7 test: QE-Agent bypass 模式扩展 allowlist 实现全自动 e2e - Closes #51
CI / test (pull_request) Successful in 8s
新增 bypass 权限:run_pipeline, pytest, curl, create_failure_issue, git 全命令

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:19:23 +08:00
pzhang_zywl a4ab3ef27e Merge pull request 'fix: 任何对git管理的内容的修改都应该走完整流程 - Closes #49' (#50) from test/issue-49 into main
CI / test (push) Successful in 8s
2026-06-02 15:03:46 +08:00
pzhang_zywl db0a73dda7 docs: Agent 关键约束新增完整改动流程规则 - Closes #49
CI / test (pull_request) Successful in 7s
任何对 git 管理内容的修改必须走:开 Issue → 改动 → PR → CI → merge → close
适用于自主轮询和用户互动触发的所有改动。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:02:57 +08:00
pzhang_zywl f0fb098451 Merge pull request 'fix: [test] blocked-check 只扫描 body 不扫描 comments 导致遗漏阻塞引用 - Closes #47' (#48) from test/issue-47 into main
CI / test (push) Successful in 8s
2026-06-02 14:52:37 +08:00
pzhang_zywl 6e67975eca test: blocked-check 同时扫描 body + comments 寻找阻塞引用 - Closes #47
CI / test (pull_request) Successful in 8s
- 新增 _get_blocking_refs() 辅助函数,同时扫描 Issue body 和 comments
- blocked_check() 和 _unblock_issues_blocked_by() 改用新函数
- 无阻塞引用但有 blocked 标签:视为残留标签自动移除
- 验证:成功解除 #18 的 blocked 标签(引用在 comments 中)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 14:51:32 +08:00
pzhang_zywl 85358bbe4a Merge pull request 'fix: 改进 blocked label的处理 - Closes #43' (#46) from test/issue-43 into main
CI / test (push) Successful in 11s
2026-06-02 14:40:48 +08:00
6 changed files with 126 additions and 28 deletions
+17 -3
View File
@@ -1,3 +1,17 @@
{ {
"permissionMode": "bypass" "permissionMode": "bypass",
} "permissions": {
"allow": [
"Bash(git *)",
"Bash(python scripts/agent_poller.py *)",
"Bash(python scripts/run_pipeline.py *)",
"Bash(python scripts/create_failure_issue.py *)",
"Bash(python -m pytest *)",
"Bash(python -c *)",
"Bash(curl *)"
]
}
}
+4
View File
@@ -225,6 +225,10 @@ QE-Agent 开 Issue (qe-feedback / bug / ci-failure)
验证不通过 → 重新分析根因 → 回到开发 验证不通过 → 重新分析根因 → 回到开发
``` ```
## 关键约束
1. **任何对 git 管理内容的修改必须走完整流程**:开 Issue → 改动 → 提交 PR → CI 通过 → merge → close Issue。无论是自主轮询还是与用户互动触发的改动,一律遵守此规则。绝不直接改文件而不走 Issue 流程。
## 提交规范 ## 提交规范
- **格式**`fix: <简短描述> - Closes #N``feat: <描述> - Closes #N` - **格式**`fix: <简短描述> - Closes #N``feat: <描述> - Closes #N`
+7 -6
View File
@@ -303,12 +303,13 @@ QE-Agent 领取 (step 1-2)
## 关键约束 ## 关键约束
1. **只修改 `tests/acceptance/`** — 不碰应用代码、不碰 `skills/`、不碰 `scripts/`(除非是修复 agent_poller 或 create_failure_issue 1. **任何对 git 管理内容的修改必须走完整流程**:开 Issue → 改动 → 提交 PR → CI 通过 → merge → close Issue。无论是自主轮询还是与用户互动触发的改动,一律遵守此规则。绝不直接改文件而不走 Issue 流程。
2. **不碰 `tests/unit/`、`tests/integration/`** — 那是开发团队维护的 2. **只修改 `tests/acceptance/`** — 不碰应用代码、不碰 `skills/`、不碰 `scripts/`(除非是修复 agent_poller 或 create_failure_issue
3. **每次只处理一个 issue** — 不混入多个 issue 的改动 3. **不碰 `tests/unit/`、`tests/integration/`** — 那是开发团队维护的
4. **`Closes #<N>` 必须出现在 commit message 中** 4. **每次只处理一个 issue** — 不混入多个 issue 的改动
5. **本地验证必须通过再 push** — 至少 Layer A + Layer B 5. **`Closes #<N>` 必须出现在 commit message 中**
6. **如果 Layer CQE Audit)需要验证但 API 不可用** — 在 issue 下评论注明,标记 `--run-acceptance` 通过后 merge 6. **本地验证必须通过再 push** — 至少 Layer A + Layer B
7. **如果 Layer CQE Audit)需要验证但 API 不可用** — 在 issue 下评论注明,标记 `--run-acceptance` 通过后 merge
## Session 收尾 ## Session 收尾
+31 -19
View File
@@ -74,11 +74,34 @@ def list_issues(labels: list[str] | None = None):
return issues return issues
def _get_blocking_refs(issue_num: int) -> set[int]:
"""Extract all issue references from an issue body + comments.
Scans both the issue body and all comments for #N patterns,
returning a set of referenced issue numbers.
"""
refs: set[int] = set()
# Body
issue = _req("GET", f"/issues/{issue_num}")
body = issue.get("body", "") or ""
refs.update(int(m.group(1)) for m in re.finditer(r'#(\d+)', body))
# Comments
try:
comments = _req("GET", f"/issues/{issue_num}/comments")
for c in comments:
cbody = c.get("body", "") or ""
refs.update(int(m.group(1)) for m in re.finditer(r'#(\d+)', cbody))
except SystemExit:
pass
return refs
def blocked_check(): def blocked_check():
"""Check all blocked issues: if blocking issues are now closed, unblock. """Check all blocked issues: if blocking issues are now closed, unblock.
Used by agents during polling to ensure blocked label doesn't persist Scans issue body + comments for blocking references.
after blocking issues are resolved. If no references found or all referenced issues are closed,
removes the 'blocked' label.
""" """
try: try:
all_blocked = _req("GET", "/issues?state=open&labels=blocked") all_blocked = _req("GET", "/issues?state=open&labels=blocked")
@@ -92,13 +115,7 @@ def blocked_check():
unblocked_count = 0 unblocked_count = 0
for issue in all_blocked: for issue in all_blocked:
body = issue.get("body", "") blocking_nums = _get_blocking_refs(issue["number"])
if not body:
continue
blocking_nums = {int(m.group(1)) for m in re.finditer(r'#(\d+)', body)}
if not blocking_nums:
continue
all_resolved = True all_resolved = True
for blk in blocking_nums: for blk in blocking_nums:
@@ -115,9 +132,9 @@ def blocked_check():
new_label_names = [l for l in current_label_names if l != "blocked"] new_label_names = [l for l in current_label_names if l != "blocked"]
new_label_ids = _label_names_to_ids(new_label_names) new_label_ids = _label_names_to_ids(new_label_names)
_req("PUT", f"/issues/{issue['number']}/labels", {"labels": new_label_ids}) _req("PUT", f"/issues/{issue['number']}/labels", {"labels": new_label_ids})
reason = "所有阻塞 Issue 均已关闭" if blocking_nums else "无阻塞引用,移除残留 blocked 标签"
print(f"Unblocked #{issue['number']}: {issue['title']}") print(f"Unblocked #{issue['number']}: {issue['title']}")
comment_issue(issue["number"], comment_issue(issue["number"], f"阻塞已解除:{reason}")
f"阻塞已解除:所有阻塞 Issue 均已关闭。")
unblocked_count += 1 unblocked_count += 1
if unblocked_count == 0: if unblocked_count == 0:
@@ -158,8 +175,8 @@ def close_issue(num, body=None):
def _unblock_issues_blocked_by(closed_num): def _unblock_issues_blocked_by(closed_num):
"""Check issues blocked by *closed_num* and unblock if all blockers resolved. """Check issues blocked by *closed_num* and unblock if all blockers resolved.
Finds open issues with 'blocked' label whose body references *closed_num* Scans both body and comments for #N references. If *closed_num* appears
via a '阻塞: #N' pattern. If all referenced blocking issues are now closed, in any blocked issue and all referenced issues are now closed,
removes the 'blocked' label and comments on the unblocked issue. removes the 'blocked' label and comments on the unblocked issue.
""" """
try: try:
@@ -170,12 +187,7 @@ def _unblock_issues_blocked_by(closed_num):
return return
for issue in all_blocked: for issue in all_blocked:
body = issue.get("body", "") blocking_nums = _get_blocking_refs(issue["number"])
if not body:
continue
# Extract all issue numbers from the body (e.g. #21, #40)
blocking_nums = {int(m.group(1)) for m in re.finditer(r'#(\d+)', body)}
if closed_num not in blocking_nums: if closed_num not in blocking_nums:
continue continue
@@ -169,6 +169,27 @@ def _normalize_rule(rule: dict) -> dict:
"value": "active" "value": "active"
}] }]
# Ensure table/text sources have a section field (defensive against LLM omission)
sources = rule.get("sources", [])
if sources:
# try to infer a default section from sibling sources or the rule path
default_section = ""
for s in sources:
sec = s.get("section", "")
if sec and sec.strip():
default_section = sec.strip()
break
if not default_section:
path = rule.get("path", "")
if path:
default_section = path.split(" > ")[0] if " > " in path else path
for src in sources:
stype = src.get("type", "")
if stype in ("table", "text"):
if not src.get("section"):
src["section"] = default_section
return rule return rule
@@ -465,3 +465,49 @@ class TestNormalizeRule:
normalized = _normalize_rule(rule) normalized = _normalize_rule(rule)
assert normalized["trigger"]["operator"] == "AND" assert normalized["trigger"]["operator"] == "AND"
assert normalized["trigger"]["conditions"][0]["operator"] == ">=" assert normalized["trigger"]["conditions"][0]["operator"] == ">="
def test_normalize_source_missing_section_from_sibling(self):
"""Table/text sources without section get it from sibling sources."""
rule = {
"trigger": {"conditions": [{"signal": "x", "operator": "==", "value": "1"}]},
"sources": [
{"type": "table", "section": "3.1.1 系统限制", "row": 1},
{"type": "text", "text_snippet": "missing section"},
],
}
normalized = _normalize_rule(rule)
assert normalized["sources"][1]["section"] == "3.1.1 系统限制"
def test_normalize_source_missing_section_from_path(self):
"""Table/text sources without section and no sibling fall back to rule path."""
rule = {
"trigger": {"conditions": [{"signal": "x", "operator": "==", "value": "1"}]},
"path": "4.2 关闭流程 > decision_speed > action_disable",
"sources": [
{"type": "table", "row": 3, "text_snippet": "no section anywhere"},
],
}
normalized = _normalize_rule(rule)
assert normalized["sources"][0]["section"] == "4.2 关闭流程"
def test_normalize_source_keeps_existing_section(self):
"""Sources that already have section are not modified."""
rule = {
"trigger": {"conditions": [{"signal": "x", "operator": "==", "value": "1"}]},
"sources": [
{"type": "table", "section": "1.0 概述", "row": 1},
],
}
normalized = _normalize_rule(rule)
assert normalized["sources"][0]["section"] == "1.0 概述"
def test_normalize_source_skips_logic_tree(self):
"""Logic tree sources are not touched (don't need section)."""
rule = {
"trigger": {"conditions": [{"signal": "x", "operator": "==", "value": "1"}]},
"sources": [
{"type": "logic_tree", "image_id": "img1", "node_ids": ["n1"]},
],
}
normalized = _normalize_rule(rule)
assert "section" not in normalized["sources"][0]