From d728cb68bfd21f616bfe9ada5f85a8b17faf9c54 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:42:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20Hook=20=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E7=A1=AE=E8=AE=A4=E4=B8=8E=E7=A1=AC=E9=97=A8=E7=A6=81?= =?UTF-8?q?=E6=95=85=E9=9A=9C=E8=AF=AD=E4=B9=89=20(v0.14.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/plugins/marketplace.json | 8 +-- .codex-plugin/plugin.json | 2 +- .zcode-plugin/plugin.json | 2 +- docs/current-architecture.md | 4 +- hooks/post_tool_lint.py | 1 + hooks/pre_tool_git_guard.py | 29 ++++++---- hooks/user_prompt_validator.py | 1 + kimi.plugin.json | 2 +- .../refactor-codeguard-architecture/design.md | 5 ++ .../specs/execution-kernel/spec.md | 9 ++++ .../refactor-codeguard-architecture/tasks.md | 2 +- .../verification.md | 8 +++ tests/test_git_guard_application.py | 53 +++++++++++++++++-- tests/test_prompt_application.py | 21 +++++++- tests/test_save_application.py | 19 ++++++- 15 files changed, 142 insertions(+), 24 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index bb57553..bc5ccf0 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "https://github.com/partme-ai/partme-codeguard-plugin.git", - "ref": "v0.14.1" + "ref": "v0.14.2" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.14.1", + "version": "0.14.2", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", - "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.1/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png", "interface": { "displayName": "代码规范守卫", "shortDescription": "Trustworthy code checks and Java impact analysis", - "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.1/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png" } } ] diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7006275..0eefcf4 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.14.1+codex.20260923", + "version": "0.14.2+codex.20260923", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI", diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 78c52bb..4aec264 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -5,7 +5,7 @@ "en": "CodeGuard", "zh-CN": "代码规范检查" }, - "version": "0.14.1", + "version": "0.14.2", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "description_i18n": { "en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 27d1fc7..932b669 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -35,7 +35,7 @@ flowchart TD | `bin/codeguard`、`scripts/run_check.py`、`scripts/fix.py` | CLI 参数、输出与 MCP stdio 注册 | 不复制扫描器或判定逻辑 | | `hooks/` | 宿主 JSON、会话事件、通知与退出协议 | 五类 Hook 将检查编排委托应用服务;入口保留旧导入兼容面及 fail-open 协议 | | `scripts/codeguard/check_application.py`、`language_check.py` | 语言选择、检查与修复请求、MCP 工具分发 | 不导入 MCP SDK;旧 `run_per_language.py` 仅再导出 | -| `startup_application.py`、`session_application.py`、`prompt_application.py`、`git_guard_application.py`、`save_application.py` | 五类 Hook 的盘点、状态消费、软提示、Git 守卫及保存反馈编排 | 返回结构化结果,不打印或调用宿主通知;Hook 控制输出、通知与 fail-open。Stop 在 stdout 刷新后才确认消费;若同时有新统计,为避免丢记录,后续 Stop 可能重复展示旧统计 | +| `startup_application.py`、`session_application.py`、`prompt_application.py`、`git_guard_application.py`、`save_application.py` | 五类 Hook 的盘点、状态消费、软提示、Git 守卫及保存反馈编排 | 返回结构化结果,不打印或调用宿主通知;Hook 控制输出、通知与 fail-open。Stop、软提示与保存反馈在 stdout 刷新后确认状态;Git 已知拦截在输出故障时仍拦截但不缓存。并发或交付重试可能重复反馈,不得丢掉未交付记录 | | `gate.py`、`gate_checks.py`、`repository_policy.py`、`baseline.py` | Git 门禁、单语言结果、安全规则及有证据的存量比较 | 只有准确基线失败且逐条诊断的文件归属、内容及次数覆盖当前结果才可豁免;基线临时路径映射回被检查文件 | | `java_build.py`、`java_impact.py`、`java_planning.py` 等 | 构建读取、纯影响闭包、命令选择与环境观察 | 默认保留跳过测试的行为;复杂构建保守扩大检查范围 | | `cve_reports.py`、`cve_policy.py`、`cve_scanners.py`、`cve.py` | 漏洞报告、阈值、进程适配与复扫编排 | 无有效结构化报告就没有安全通过结论 | @@ -66,4 +66,4 @@ python3 scripts/vendor/skill_vendor.py check openspec validate refactor-codeguard-architecture --strict ``` -真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.13.0 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。 +真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.14.1 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。 diff --git a/hooks/post_tool_lint.py b/hooks/post_tool_lint.py index f72ed40..ddf480e 100644 --- a/hooks/post_tool_lint.py +++ b/hooks/post_tool_lint.py @@ -112,6 +112,7 @@ def _main(payload: dict) -> int: if result.system_message is not None: message["systemMessage"] = result.system_message print(json.dumps(message, ensure_ascii=False)) + sys.stdout.flush() result.acknowledge() if result.notification: lang, title, message = result.notification diff --git a/hooks/pre_tool_git_guard.py b/hooks/pre_tool_git_guard.py index 7dc145c..5739ece 100755 --- a/hooks/pre_tool_git_guard.py +++ b/hooks/pre_tool_git_guard.py @@ -79,26 +79,37 @@ def extract_command(payload: dict) -> str: def main() -> int: payload = read_payload() with session_scope(payload): - if not payload.get("tool_use_id"): - return _main(payload) - key = "pre:" + str(payload["tool_use_id"]) + ":" + extract_command(payload) - cached = completed_event(key) + key = ("pre:" + str(payload["tool_use_id"]) + ":" + extract_command(payload) + if payload.get("tool_use_id") else None) + cached = completed_event(key) if key else None if cached is not None and cached["code"] == 2: - sys.stdout.write(cached["stdout"]) - sys.stderr.write(cached["stderr"]) + _deliver(cached["code"], cached["stdout"], cached["stderr"]) return cached["code"] stdout, stderr = io.StringIO(), io.StringIO() with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): code = _main(payload) out, err = stdout.getvalue(), stderr.getvalue() # 只复用阻断结果,硬门放行和未验证永远重跑准确快照。 - sys.stdout.write(out) - sys.stderr.write(err) - if code == 2: + delivered = _deliver(code, out, err) + if key and code == 2 and delivered: record_completed_event(key, code, out, err) return code +def _deliver(code: int, stdout: str, stderr: str) -> bool: + """刷新宿主输出后才允许缓存;已知拦截不能因输出故障降为放行。""" + try: + sys.stdout.write(stdout) + sys.stderr.write(stderr) + sys.stdout.flush() + sys.stderr.flush() + except (OSError, UnicodeError, ValueError): + if code == 2: + return False + raise + return True + + def _main(payload: dict) -> int: ensure_user_path() result = evaluate_git_command( diff --git a/hooks/user_prompt_validator.py b/hooks/user_prompt_validator.py index a9d0396..7a30818 100755 --- a/hooks/user_prompt_validator.py +++ b/hooks/user_prompt_validator.py @@ -102,6 +102,7 @@ def _main(payload: dict) -> int: "hookEventName": "UserPromptSubmit", "additionalContext": result.additional_context, }}, ensure_ascii=False)) + sys.stdout.flush() result.acknowledge() return 0 diff --git a/kimi.plugin.json b/kimi.plugin.json index 23c8e94..c5dcc6c 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.14.1", + "version": "0.14.2", "description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.", "author": { "name": "Full Stack Skills / PartMe.AI" diff --git a/openspec/changes/refactor-codeguard-architecture/design.md b/openspec/changes/refactor-codeguard-architecture/design.md index 5955c10..b15ed10 100644 --- a/openspec/changes/refactor-codeguard-architecture/design.md +++ b/openspec/changes/refactor-codeguard-architecture/design.md @@ -88,6 +88,11 @@ Stop 的宿主入口先准备原子可读的状态快照,成功写入并刷新 后续 Stop 可能重复展示旧统计,但不会丢掉未展示的新统计。旧直接消费接口继续兼容, 不用于宿主 Stop 交付路径。 +同一交付规则也适用于 UserPromptSubmit 和 PostToolUse:入口完成 stdout 刷新后才确认 +已完成事件、保存去重和会话统计。PreToolUse 的已知拦截先保留退出码;写入、编码或刷新 +故障时不缓存未交付结果,不能通过通用 fail-open 异常处理把明确的拦截改成放行。 +这只证明本进程写入管道成功,不能证明宿主最终消费;因此重试允许重复反馈。 + 事件去重不等于权限缓存。观察 Hook 成功返回后才登记完成,异常允许重试;UPS 有 skipped 时 保守地不登记。硬门禁只重放已完成拒绝,放行、未验证和执行中一律不复用;并发副本允许 重复检查而不能预占后直接放行。保存检查只有已获得结论才记去重,未知可立即重试。 diff --git a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md index c42d203..cfef1fd 100644 --- a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md +++ b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md @@ -62,6 +62,7 @@ 统计、绕过明细、冷却与审计的读改写 MUST 在进程锁内完成并原子替换文件。宿主提供 session_id 时,统计 MUST 绑定会话与当前 worktree;Stop MUST 只消费该作用域的记录,并与并发写入互斥。无 session_id 时保留旧共享状态兼容,MUST 明示不能证明跨会话隔离。去重 MUST NOT 把未完成或 UNVERIFIED 的检查当成已完成检查。 Stop MUST 在宿主输出成功刷新后才确认消费;输出失败时记录必须保留供重试。若确认前有并发新写入,允许后续 Stop 重复展示旧统计,但不得删除未展示的新记录。 +UserPromptSubmit 与 PostToolUse 的已完成事件、文件去重和会话统计 MUST 在相应 stdout 成功刷新后才登记;写入或刷新失败时不得把未交付反馈标记为已交付。PreToolUse 已得到拦截结论时,即使宿主输出失败也 MUST 保留拦截退出码,并且不得缓存该次未交付结果。 #### Scenario: Concurrent writers preserve all increments - **WHEN** 多个 Hook 进程同时更新同一统计、绕过记录或审计日志 @@ -75,6 +76,14 @@ Stop MUST 在宿主输出成功刷新后才确认消费;输出失败时记录 - **WHEN** Stop 已准备总结但宿主 stdout 写入或刷新失败,或在输出期间有新统计写入 - **THEN** 输出失败时旧统计可在重试中再次展示;并发新统计不得被确认消费误删,允许为了不丢记录而在后续 Stop 重复展示旧统计 +#### Scenario: Buffered host feedback fails before delivery +- **WHEN** UserPromptSubmit 或 PostToolUse 已写入反馈,但 stdout 刷新失败 +- **THEN** 不登记已完成事件、文件去重或会话统计;下次触发仍可重新检查并交付反馈 + +#### Scenario: Hard Git block survives host output failure +- **WHEN** PreToolUse 已判定需要拦截,但 stdout 或 stderr 写入或刷新失败 +- **THEN** 保持拦截退出码且不缓存该次结果,不能因宿主输出故障改为放行 + #### Scenario: Retry follows an unverified save check - **WHEN** 相同文件未变化,上次保存检查未能获得结论,紧接着再次触发 - **THEN** 再次执行检查,不因此前写入的去重标记而静默跳过 diff --git a/openspec/changes/refactor-codeguard-architecture/tasks.md b/openspec/changes/refactor-codeguard-architecture/tasks.md index f1eeee0..33bb545 100644 --- a/openspec/changes/refactor-codeguard-architecture/tasks.md +++ b/openspec/changes/refactor-codeguard-architecture/tasks.md @@ -34,5 +34,5 @@ - [x] 5.2 四个历史 skipped 已全部恢复;Dockerfile CLI 接入统一报告解析与扫描应用,14 项真实进程边界测试通过;基线比较按诊断内容和次数核验并要求基线实际 FAIL,三项误豁免用例先红后绿。完整 433 项单测零跳过、142 项回归零失败。 - [x] 5.3 新增当前架构 Mermaid 与扩展指南;报告版本读取插件 manifest,双语 README 指向当前证据,旧设计标明历史范围;双语标题/链接/版本及本地链接存在性测试通过。发布版本仍需在 5.5 检查。 - [ ] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过。 -- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.0 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但后续工作树改动仍未发布。宿主安装运行独立记录,未授权不得改宿主。 +- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.1 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但本 change 后续改动仍需独立发布。宿主安装运行独立记录,未授权不得改宿主。 - [ ] 5.6 完成全目标审计,代码/规格/证据一致后同步并归档 change,才可将整体优化标记完成。 diff --git a/openspec/changes/refactor-codeguard-architecture/verification.md b/openspec/changes/refactor-codeguard-architecture/verification.md index 43e3243..bbb2ac0 100644 --- a/openspec/changes/refactor-codeguard-architecture/verification.md +++ b/openspec/changes/refactor-codeguard-architecture/verification.md @@ -170,3 +170,11 @@ - Stop 原逻辑在输出前 `take_json`,故故障注入先 RED:stdout 写入失败时会话记录已被删除。现新增准备结果和输出后确认;写入或刷新失败时记录保留可重试。若准备后有并发新写入,锁内快照不一致就不消费,允许下一次重复展示旧统计而不删除新记录。旧 `consume_summary` 兼容接口保留,Hook 改用 `prepare_summary`。两种输出故障和并发写入均有目标测试。 - 本机现有 `/opt/anaconda3/bin/python3` 3.13.5 完整执行 **460 unittest,全部通过、0 skipped**;`tests/run_all.py` **142/0/0**。ruff、架构门禁、语言 schema 57 项/11 规则、vendor 离线与在线、本 change strict、diff whitespace 均通过;受管技能及 lock 未改。上述为本地证据,远端 CI/版本/市场需在本批发布后另验。 - 这两处消除了已复现的假豁免与输出前丢总结;仍不声称完整 Shell 解释、三宿主现场、Windows、在线漏洞库、全部工具链或 OpenSpec 5.4/5.6 的全目标审计完成。stdout 刷新成功不等于宿主最终消费确认,因此异常重试可出现重复总结;该取舍优先保证不丢统计。 + +## 第十六批实际验证:跨 Hook 输出交付边界 + +- 已发布的 v0.14.1 源码经插件 PR #47、市场 PR #4 合并到双仓 `main`;插件 `skills-check` 通过,注释 tag 与 GitHub Release 指向合并提交 `b8fa3850c877eae454845326fc167103d406b7d3`,三宿主市场清单均为 0.14.1。该证据只覆盖此前源码,不覆盖本批工作树。 +- 临时 CodeGraph 快照同步最新源码后,追踪 `PromptResult`/`SaveResult`/PreToolUse 的交付调用链:Stop 已刷新 stdout 再确认,而其余两个软 Hook 及 Git 硬门在刷新前就登记完成或缓存。当前项目的真实测试文件覆盖范围比 CodeGraph 的受影响测试提示更广,故以源码链和实际测试为准。 +- 新增 RED:UserPromptSubmit 与 PostToolUse 在 stdout 刷新失败时,旧入口仍登记完成;Git 已知拦截的输出写入失败向外抛异常,若由入口顶层 fail-open 捕获则可能放行;刷新失败还会缓存未交付拦截。现在所有确认都在刷新后;已知拦截输出故障保留 exit 2、不缓存,含无 tool_use_id 路径和缓存重放。对普通软反馈仍保持原 fail-open 协议。 +- 定向测试 **26/26**,完整单测 **465/465、0 skipped**,真实 Hook 回归 **142/0/0**;ruff、架构依赖/循环、本 change strict、语言 schema **57 项/11 规则**、vendor 离线和在线以及 diff whitespace 通过。vendor lock 仍为 v0.1.2 → `2c0c8071f96de48dc53e11de2499c083c100e44c`,受管技能与 lock 未修改。 +- 本批只证明本地输出时序与故障注入;stdout 刷新不等于宿主最终消费,可能在重试时重复反馈。三宿主已安装运行、Windows、联网漏洞库和全部语言工具链仍需独立验收;本批在此记录时尚未 bump/提交/发布。 diff --git a/tests/test_git_guard_application.py b/tests/test_git_guard_application.py index 1ff753e..9e578df 100644 --- a/tests/test_git_guard_application.py +++ b/tests/test_git_guard_application.py @@ -64,11 +64,58 @@ def block(_payload): patch.object(pre_tool_git_guard, "_main", side_effect=block), \ patch.object(pre_tool_git_guard, "completed_event", return_value=None), \ patch.object(pre_tool_git_guard, "record_completed_event") as record, \ - patch.object(sys, "stdout", BrokenOutput()), \ - self.assertRaises(OSError): - pre_tool_git_guard.main() + patch.object(sys, "stdout", BrokenOutput()): + self.assertEqual(2, pre_tool_git_guard.main()) record.assert_not_called() + def test_blocked_event_is_not_cached_if_host_flush_fails(self): + class BrokenFlush(io.StringIO): + def flush(self): + raise OSError("host flush unavailable") + + payload = {"tool_use_id": "event-2", "tool_input": {"command": "git commit -m x"}} + + def block(_payload): + print("blocked") + return 2 + + with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \ + patch.object(pre_tool_git_guard, "_main", side_effect=block), \ + patch.object(pre_tool_git_guard, "completed_event", return_value=None), \ + patch.object(pre_tool_git_guard, "record_completed_event") as record, \ + patch.object(sys, "stdout", BrokenFlush()): + self.assertEqual(2, pre_tool_git_guard.main()) + record.assert_not_called() + + def test_block_without_event_id_still_blocks_if_stderr_flush_fails(self): + class BrokenFlush(io.StringIO): + def flush(self): + raise OSError("host stderr flush unavailable") + + def block(_payload): + print("blocked", file=sys.stderr) + return 2 + + payload = {"tool_input": {"command": "git push"}} + with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \ + patch.object(pre_tool_git_guard, "_main", side_effect=block), \ + patch.object(sys, "stderr", BrokenFlush()): + self.assertEqual(2, pre_tool_git_guard.main()) + + def test_cached_block_remains_blocked_if_replay_cannot_write(self): + class BrokenOutput: + def write(self, _text): + raise OSError("host output unavailable") + + payload = {"tool_use_id": "event-3", "tool_input": {"command": "git push"}} + cached = {"code": 2, "stdout": "blocked\n", "stderr": ""} + with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \ + patch.object(pre_tool_git_guard, "completed_event", return_value=cached), \ + patch.object(pre_tool_git_guard, "_main") as run, \ + patch.object(sys, "stdout", BrokenOutput()): + self.assertEqual(2, pre_tool_git_guard.main()) + run.assert_not_called() + if __name__ == "__main__": unittest.main() diff --git a/tests/test_prompt_application.py b/tests/test_prompt_application.py index 5fcb0da..900c601 100644 --- a/tests/test_prompt_application.py +++ b/tests/test_prompt_application.py @@ -11,10 +11,29 @@ from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "scripts")) +sys.path[:0] = [str(ROOT / "scripts"), str(ROOT / "hooks")] class PromptApplicationTests(unittest.TestCase): + def test_hook_does_not_acknowledge_before_stdout_flush(self): + import user_prompt_validator + from codeguard.prompt_application import PromptResult + + class BrokenFlush(io.StringIO): + def flush(self): + raise OSError("host flush unavailable") + + payload = {"user_prompt": "提交代码", "session_id": "session-1"} + delivered = [] + with patch.object(user_prompt_validator, "read_payload", return_value=payload), \ + patch.object(user_prompt_validator, "ensure_user_path"), \ + patch.object(user_prompt_validator, "find_project_root", return_value=Path.cwd()), \ + patch.object(user_prompt_validator.prompt_application, "evaluate_prompt", + return_value=PromptResult("检查反馈", _on_delivered=lambda: delivered.append(True))), \ + patch.object(sys, "stdout", BrokenFlush()), self.assertRaises(OSError): + user_prompt_validator.main() + self.assertEqual([], delivered) + def test_non_git_prompt_does_not_read_config_or_emit_host_output(self): from codeguard.prompt_application import evaluate_prompt diff --git a/tests/test_save_application.py b/tests/test_save_application.py index 3551f5f..44dddec 100644 --- a/tests/test_save_application.py +++ b/tests/test_save_application.py @@ -10,10 +10,27 @@ from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "scripts")) +sys.path[:0] = [str(ROOT / "scripts"), str(ROOT / "hooks")] class SaveApplicationTests(unittest.TestCase): + def test_hook_does_not_acknowledge_before_stdout_flush(self): + import post_tool_lint + from codeguard.save_application import SaveResult + + class BrokenFlush(io.StringIO): + def flush(self): + raise OSError("host flush unavailable") + + delivered = [] + with patch.object(post_tool_lint, "ensure_user_path"), \ + patch.object(post_tool_lint, "find_project_root", return_value=Path.cwd()), \ + patch.object(post_tool_lint.save_application, "evaluate_save", + return_value=SaveResult("检查反馈", _on_delivered=lambda: delivered.append(True))), \ + patch.object(sys, "stdout", BrokenFlush()), self.assertRaises(OSError): + post_tool_lint._main({"file_path": "main.py"}) + self.assertEqual([], delivered) + def test_project_only_checker_reports_unverified_without_running(self): from codeguard import save_application