From 86c5c15a2fb5801b188aec304b1980ebd74bf74f Mon Sep 17 00:00:00 2001 From: graphlemon Date: Thu, 13 Aug 2026 23:15:18 +0800 Subject: [PATCH] graphlemon: benchmark artifact for task b243dfe8-4da8-49bd-895b-8b9e9ed17280 --- .graphlemon/runs/b243dfe8-4da8-49bd-895b-8b9e9ed17280/benchmark.md | 345 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+), 0 deletions(-) create mode 100644 .graphlemon/runs/b243dfe8-4da8-49bd-895b-8b9e9ed17280/benchmark.md diff --git a/.graphlemon/runs/b243dfe8-4da8-49bd-895b-8b9e9ed17280/benchmark.md b/.graphlemon/runs/b243dfe8-4da8-49bd-895b-8b9e9ed17280/benchmark.md new file mode 100644 index 0000000..4c138c4 --- /dev/null +++ b/.graphlemon/runs/b243dfe8-4da8-49bd-895b-8b9e9ed17280/benchmark.md @@ -0,0 +1,345 @@ +# Benchmark:Skill 详情页展示真实内容(`v_skill_audit_log` 契约收口) + +对应 SDD:《SDD:Skill 详情页展示真实内容(`v_skill_audit_log` 契约收口)》,Unit DU-01。 + +本文档定义 DU-01 的可验证验收场景,覆盖三层:`src/skill_query.py::get_skill_content()` +单元层、`src/backend_server.py` HTTP 路由层、`frontend/app.js` 手工验证层,以及既有测试的回归 +范围。每个场景给出**具体输入、预期行为、可观察的通过标准**(返回值/状态码/DOM 内容/异常类 +型),不是场景标题的罗列。 + +--- + +## 0. 测试环境与前置数据约定 + +所有单元层 / 路由层场景假定以下共享 fixture(除非场景自身另行说明覆盖值): + +- 临时目录 `repo_root/`,其下 `repo_root/skills/skill-1/SKILL.md`,内容为已知字符串 + `"# Skill One\n\nBody text.\n"`。 +- SQLite `skill` 表插入一行: + + ``` + skill_id='skill-1', owner_org_node='coder', file_path='skills/skill-1', + current_git_commit_sha='unknown', source='migrated_from_hermes', + migrated_from_profile='code', status='active' + ``` + +- `skill_iteration` 表为空(对应 PRD 背景中"当前没有迭代记录"这句话本身正确的前提)。 + +每个场景会在此基线上做局部覆盖(如删除文件、篡改 `file_path`、增加支持文件等),并在场景描 +述中明确写出与基线的差异。 + +--- + +## 1. 单元层场景:`src/skill_query.py::get_skill_content()` + +### U1(happy path)正常内容读取,无支持文件 + +- **输入**:基线 fixture,调用 `get_skill_content(conn, "skill-1", repo_path=repo_root)`。 +- **预期行为**:函数正常返回,不抛任何异常。 +- **通过标准**(逐字段断言): + ```python + result = get_skill_content(conn, "skill-1", repo_path=repo_root) + assert result == { + "skill_id": "skill-1", + "owner_org_node": "coder", + "file_path": "skills/skill-1", + "current_git_commit_sha": "unknown", + "source": "migrated_from_hermes", + "migrated_from_profile": "code", + "status": "active", + "content": "# Skill One\n\nBody text.\n", + "supporting_files": [], + } + ``` +- **对应测试**:`GetSkillContentTest.test_returns_real_file_content`(重写)。 + +### U2(happy path)支持文件枚举,多层子目录 + +- **输入**:在基线基础上,额外创建: + - `repo_root/skills/skill-1/references/foo.md` + - `repo_root/skills/skill-1/scripts/bar.py` + - `repo_root/skills/skill-1/SKILL.md` 内容不变。 +- **预期行为**:`content` 字段仍是 `SKILL.md` 全文;`supporting_files` 列出另外两个文件,使用 + 相对 `skill_dir` 的相对路径,且**升序排序**、**不包含 `SKILL.md` 自身**。 +- **通过标准**: + ```python + result = get_skill_content(conn, "skill-1", repo_path=repo_root) + assert result["content"] == "# Skill One\n\nBody text.\n" + assert result["supporting_files"] == ["references/foo.md", "scripts/bar.py"] + assert "SKILL.md" not in result["supporting_files"] + ``` +- **对应测试**:新增/重写 `GetSkillContentTest.test_lists_supporting_files_sorted`。 + +### U3(boundary)目录下只有 `SKILL.md`,无其他文件 + +- **输入**:基线 fixture(无额外文件)。 +- **预期行为**:`supporting_files == []`(空列表,不是 `None`,不是缺字段)。 +- **通过标准**:`isinstance(result["supporting_files"], list) and result["supporting_files"] == []`。 +- **对应测试**:并入 U1 断言即可覆盖,无需单独用例。 + +### U4(failure→非异常语义)`SKILL.md` 磁盘文件缺失,DB 行仍存在 + +- **输入**:DB 中 `skill-1` 行照常存在,但删除/从不创建 + `repo_root/skills/skill-1/SKILL.md`(`repo_root/skills/skill-1/` 目录本身也不存在,或存在但 + 为空目录——两种子场景都要覆盖)。 +- **预期行为**:**不抛出任何异常**(不是 `SkillContentMissingError`,该类已被删除)。函数正常 + 返回一个 dict,`content` 字段为 `None`。 +- **通过标准**: + ```python + result = get_skill_content(conn, "skill-1", repo_path=repo_root) + assert result["content"] is None + assert result["supporting_files"] == [] # 目录不存在时 os.walk 返回空,不报错 + assert result["skill_id"] == "skill-1" # 其余字段仍从 DB 正常返回 + ``` +- **对应测试**:`GetSkillContentTest.test_missing_file_returns_none_content`(重写,原断言 + `assertRaises(SkillContentMissingError)` 改为断言 `content is None` 且不抛异常)。 + +### U5(failure)`skill_id` 在 `skill` 表中不存在 + +- **输入**:`get_skill_content(conn, "skill-does-not-exist", repo_path=repo_root)`。 +- **预期行为**:抛出 `SkillNotFoundError`,且**不做任何文件系统访问**(DB 查询先于文件路径解 + 析)。 +- **通过标准**: + ```python + with self.assertRaises(skill_query.SkillNotFoundError): + get_skill_content(conn, "skill-does-not-exist", repo_path=repo_root) + ``` +- **对应测试**:既有 `GetSkillContentTest.test_nonexistent_skill_raises_not_found`(保留,行为 + 未变,仅确认契约变更没有波及此分支)。 + +### U6(failure/security)路径穿越——`file_path` 含相对 `../` 跳出 `skills/` 目录 + +- **输入**:DB 中插入一行 `skill_id='evil-1'`,`file_path='skills/../../etc/passwd'`(或等价的 + `../` 相对路径写法,具体依 `_skill_file_path()` 的既有解析规则构造)。调用 + `get_skill_content(conn, "evil-1", repo_path=repo_root)`。 +- **预期行为**:抛出 `SkillPathTraversalError`(不是 `SkillContentMissingError`——该类型已删 + 除)。**在异常抛出前,函数从未调用 `open()`**——用哨兵验证:在越界目标路径预先放置一个内容 + 可识别的哨兵文件(例如 repo 外层一个 `sentinel.txt` 写入 `"SENTINEL-SHOULD-NEVER-BE-READ"`), + 断言异常抛出且该哨兵内容从未出现在任何返回值或日志中。 +- **通过标准**: + ```python + with self.assertRaises(skill_query.SkillPathTraversalError): + get_skill_content(conn, "evil-1", repo_path=repo_root) + # 哨兵文件从未被打开:可通过 mock open() 断言 open 从未以哨兵路径为参数被调用, + # 或通过文件系统 side-channel(哨兵文件 mtime/访问标记不变)间接验证 + ``` +- **对应测试**:`GetSkillContentTest.test_traversal_via_relative_dotdot_in_file_path`(重写, + 异常类型改为 `SkillPathTraversalError`)。 + +### U7(failure/security)路径穿越——`file_path` 通过目录组件字面量跳出(如 `skills-evil/`) + +- **输入**:DB 中插入一行 `skill_id='evil-2'`,`file_path='skills-evil/leak'`(`realpath` 前缀 + 匹配上容易因字符串前缀误判为在 `skills_root` 内,例如 `"...skills"` 是 `"...skills-evil"` 的 + 字符串前缀但不是路径前缀——用以验证用的是 `os.path.commonpath`,不是朴素的 `str.startswith`)。 +- **预期行为**:抛出 `SkillPathTraversalError`。 +- **通过标准**: + ```python + with self.assertRaises(skill_query.SkillPathTraversalError): + get_skill_content(conn, "evil-2", repo_path=repo_root) + ``` +- **对应测试**:`GetSkillContentTest.test_traversal_via_directory_component_returns_path_traversal_error` + (重写,函数名/断言从 `..._returns_content_missing` 改为反映新异常类型)。 + +### U8(boundary)`SkillContentMissingError` 符号已被移除 + +- **输入**:静态检查,非运行时用例。 +- **预期行为**:`src/skill_query.py` 中不再存在 `SkillContentMissingError` 类定义; + `src/backend_server.py` 中不再有引用该符号的 `except` 分支。 +- **通过标准**:`grep -rn SkillContentMissingError src/` 返回空结果。 +- **验证方式**:Code Review / grep,非单元测试断言(记录在 DoD 清单中)。 + +--- + +## 2. 路由层场景:`GET /api/skills/:skill_id`(`SkillRouteTest`) + +共享前置:`backend_server` 测试基线起一个真实 `HTTPServer` 线程或使用既有测试基建(与 +`SkillRouteTest` 现有其他用例一致的启动方式),DB fixture 同第 0 节。 + +### R1(happy path)正常内容,200 + +- **请求**:`GET /api/skills/skill-1`(基线 fixture,含 U2 的两个支持文件)。 +- **预期状态码**:`200`。 +- **预期响应体**(JSON,逐字段): + ```json + { + "skill_id": "skill-1", + "owner_org_node": "coder", + "file_path": "skills/skill-1", + "current_git_commit_sha": "unknown", + "source": "migrated_from_hermes", + "migrated_from_profile": "code", + "status": "active", + "content": "# Skill One\n\nBody text.\n", + "supporting_files": ["references/foo.md", "scripts/bar.py"] + } + ``` +- **通过标准**:状态码等值比较 + 响应体逐 key 相等比较(不是子集匹配,字段名必须恰好是 + `supporting_files`,不是 `support_files`)。 +- **对应测试**:`SkillRouteTest.test_content_returns_real_content`(重写,URL 从 + `/api/skills/skill-1/content` 改为 `/api/skills/skill-1`,字段名同步改)。 + +### R2(boundary)磁盘文件缺失,200 + `content: null` + +- **请求**:`GET /api/skills/skill-1`,DB 行存在但 `SKILL.md` 文件已删除(同 U4 场景)。 +- **预期状态码**:`200`(**不是** 404——这是本次契约变更的核心行为差异之一)。 +- **预期响应体**: + ```json + {"skill_id": "skill-1", "...": "...", "content": null, "supporting_files": []} + ``` +- **通过标准**:`resp.status == 200` 且 `json.loads(resp.body)["content"] is None`。 +- **对应测试**:新增 `SkillRouteTest.test_content_missing_file_returns_200_null`(路由层此前 + 完全没有覆盖越界/缺失场景,仅 `skill_query` 单元层有——本用例是 SDD §6 明确要求新增,不是重 + 写)。 + +### R3(failure)skill 不存在,404 + +- **请求**:`GET /api/skills/does-not-exist`。 +- **预期状态码**:`404`。 +- **预期响应体**:`{"error": "NOT_FOUND"}`。 +- **通过标准**:状态码 + body 精确匹配。 +- **对应测试**:`SkillRouteTest.test_content_nonexistent_skill_returns_404`(重写,URL 改为裸 + `skill_id`)。 + +### R4(failure/security)路径穿越,400 + +- **请求**:`GET /api/skills/evil-1`,DB 中该行 `file_path` 为越界值(同 U6 fixture)。 +- **预期状态码**:`400`(**不是** 404,也不是 500)。 +- **预期响应体**:`{"error": "INVALID_SKILL_ID"}`。 +- **通过标准**:状态码 + body 精确匹配;额外断言响应体中不含任何越界目标路径的文件内容片段。 +- **对应测试**:新增 `SkillRouteTest.test_content_path_traversal_returns_400`(路由层新增, + SDD §6 明确要求)。 + +### R5(boundary)路由匹配优先级——`/audit` 后缀不被裸 `skill_id` 分支误吞 + +- **请求**:`GET /api/skills/skill-1/audit`(既有的迭代审计接口)。 +- **预期行为**:命中既有 `_handle_skill_audit_log`,**不**命中新的裸 `skill_id` 分支(新分支要 + 求 `"/" not in skill_id`,`"skill-1/audit"` 含 `/`,天然不满足,但需要显式测试验证路由顺序 + 没有被写反)。 +- **预期状态码/响应体**:与改动前完全一致——`skill_iteration` 表为空时返回 `200` + `[]`。 +- **通过标准**:`resp.status == 200 and json.loads(resp.body) == []`。 +- **对应测试**:既有 `SkillRouteTest.test_audit_log_empty_returns_empty_list`(原样保留,作为 + 路由顺序回归证据,不修改)。 + +### R6(boundary)路由匹配优先级——`/diff` 端点不受影响 + +- **请求**:`GET /api/skills/diff?...`(既有 diff 接口的合法查询)。 +- **预期行为**:命中既有 `_handle_skill_diff`分支(在裸 `skill_id` 分支**之前**注册),行为与 + 改动前完全一致。 +- **通过标准**:既有 `GetDiffTest`/`SkillRouteTest.test_diff_*` 全部保持通过,不需新增用例。 + +### R7(failure)`POST /api/skills/:skill_id/rollback` 不受本次改动影响 + +- **请求**:既有 `test_rollback_*` 用例的请求不变。 +- **预期行为**:`do_POST` 分支未改动,行为完全一致。 +- **通过标准**:既有 `SkillRouteTest.test_rollback_*` 全部保持通过,不需新增用例。 + +--- + +## 3. 前端手工验证场景(`frontend/app.js`,无自动化测试基建,与 PRD §8 一致) + +环境:`python3 -m src.backend_server --port 8420`,浏览器打开 +`http://127.0.0.1:8420/#/skills`。 + +### F1(happy path)点击已迁移 Skill,两面板并列显示 + +- **操作**:在 `#/skills` 列表点击任一真实存在磁盘文件的 Skill(如迁移后的 `coder`/`planner` + 等真实 skill_id)。 +- **预期页面**:跳转到 `#/skills/:id/audit`;页面同时可见两个独立面板: + 1. "Skill 内容"面板:`
` 元素内是该 Skill `SKILL.md` 的完整原始文本(可
+     用浏览器"查看源代码"/`document.getElementById("skill_content")` 核对文本与磁盘文件字节
+     一致,包括换行)。
+  2. "迭代记录"面板:显示"该 Skill 尚无自主迭代记录"(因为 `skill_iteration` 表为空,这是
+     PRD 背景中确认为正确的既有行为,不应被本次改动改变)。
+- **通过标准**:两个面板同时非空/各自呈现预期内容,其中一个面板的渲染逻辑失败不影响另一个(
+  可通过临时让 `/audit` 请求失败——如断网模拟——验证"Skill 内容"面板仍正常显示)。
+
+### F2(boundary)无第三方渲染依赖
+
+- **操作**:打开浏览器开发者工具 Network 面板,刷新 `#/skills/:id/audit` 页面。
+- **预期行为**:页面加载的脚本资源里不出现任何 Markdown 渲染库或代码高亮库(如
+  `marked.js`/`highlight.js`/`prismjs` 等);"Skill 内容"面板的 DOM 元素是纯 `
` 文本节
+  点,未被转换为渲染后的 HTML 结构(如 `

`/``)。 +- **通过标准**:Network 面板资源列表 + `document.getElementById("skill_content").innerHTML` + 中除转义后的原始 Markdown 文本外不含渲染产物标签。 + +### F3(failure)磁盘文件缺失时的降级显示 + +- **操作**:临时删除某个已迁移 Skill 目录下的 `SKILL.md`(测试环境操作,验证后恢复),刷新对 + 应详情页。 +- **预期页面**:"Skill 内容"面板显示文案"(内容文件不可用)",不抛 JS 异常,不出现空白面 + 板或浏览器控制台报错;"迭代记录"面板不受影响,正常渲染。 +- **通过标准**:浏览器控制台无未捕获异常;"Skill 内容"面板文本节点内容精确等于 + "(内容文件不可用)"。 + +### F4(failure)Skill 完全不存在时的整页错误态 + +- **操作**:直接在地址栏输入 `http://127.0.0.1:8420/#/skills/does-not-exist/audit`。 +- **预期页面**:页面顶部出现统一错误横幅,文案含"该 Skill 不存在";"Skill 内容"面板与"迭 + 代记录"面板均不渲染任何内容(不是各自显示两条不一致的错误提示)。 +- **通过标准**:`#skill_audit_error` 元素 `display` 不为 `none` 且文本含"不存在"; + `#skill_content`、`#skill_audit_list` 的 `innerHTML` 均为空字符串。 + +### F5(boundary)支持文件清单展示 + +- **前置**:为某个测试用 Skill 目录额外放置 `references/example.md`。 +- **操作**:打开该 Skill 详情页。 +- **预期页面**:"Skill 内容"面板在正文 `
` 之外(之前或之后,与现有 DOM 结构一致的位
+  置)显示一行提示,形如"支持文件:references/example.md";若无支持文件的 Skill 则显示"无
+  支持文件"。
+- **通过标准**:对应 hint 文本节点内容与磁盘实际支持文件列表一致;不存在支持文件的 Skill 显
+  示"无支持文件"而不是空白或报错。
+
+---
+
+## 4. 回归范围:既有测试必须保持通过,断言不得修改
+
+以下测试类/方法不在本次契约变更范围内,运行 `python3 -m unittest src.test_skill_query -v` 与
+`python3 -m unittest src.test_backend_server.SkillRouteTest -v` 时必须保持全绿,且其断言内容
+(包括请求 URL、期望字段名、期望状态码)**逐字不变**:
+
+- `src/test_skill_query.py`:`ListSkillsTest` 全部用例、`GetSkillAuditLogTest` 全部用例、
+  `GetDiffTest` 全部用例。
+- `src/test_backend_server.py::SkillRouteTest`:`test_list_returns_seeded_skill`、
+  `test_audit_log_*`、`test_diff_*`、`test_full_update_diff_rollback_cycle`、
+  `test_rollback_*`。
+
+**通过标准**:`git diff` 中上述测试方法体不出现在改动范围内(除非是纯粹的、不改变断言语义的
+重排/import 调整)。若 diff 触碰了这些方法的断言内容,视为违反 SDD §0 裁决,判定为不通过。
+
+---
+
+## 5. 全量回归
+
+- **命令**:`python3 -m unittest discover -s src`。
+- **通过标准**:全部用例通过(0 failure,0 error),特别关注
+  `src/test_agent_profile.py`、`src/test_skill_update.py` 等间接依赖 `skill_query`/
+  `skill_update` 模块符号的测试文件不因 `SkillContentMissingError` 删除而出现
+  `ImportError`/`AttributeError`。
+
+---
+
+## 6. 验收标准总表(PRD AC 逐条 → 本文档场景编号)
+
+| PRD AC | 描述 | 覆盖场景 |
+|---|---|---|
+| AC1 | 真实内容以 `
` 展示 | F1、F2、R1 |
+| AC2 | 变更审计面板行为不变 | R5、F1(迭代记录部分)|
+| AC3 | 两面板并列、互不依赖 | F1、F3(局部失败不影响另一面板)|
+| AC4 | 文件缺失显示"内容文件不可用",不 500 | U4、R2、F3 |
+| AC5 | skill 不存在 → 整页错误态,两面板都不渲染,404 | U5、R3、F4 |
+| AC6 | 越界 `file_path` 不读出目录外内容,400 非 500 | U6、U7、R4 |
+| AC7 | 既有测试全通过、断言不被无理由篡改 | 第 4 节回归范围 |
+| AC8 | 前端无新第三方依赖 | F2 |
+
+---
+
+## 7. Definition of Done 检查清单(可勾选执行)
+
+- [ ] U1–U8 全部通过(`GetSkillContentTest` 重写后的类)。
+- [ ] R1–R7 全部通过(`SkillRouteTest` 重写 + 新增用例)。
+- [ ] `grep -rn SkillContentMissingError src/` 输出为空。
+- [ ] `python3 -m unittest src.test_skill_query -v` 全绿。
+- [ ] `python3 -m unittest src.test_backend_server.SkillRouteTest -v` 全绿。
+- [ ] `python3 -m unittest discover -s src` 全绿。
+- [ ] F1–F5 手工验证全部通过(浏览器实测,非自动化)。
+- [ ] 第 4 节列出的既有测试方法体逐字未改动(`git diff` 复核)。
--
libgit2 0.21.4