@optima-chat/dev-skills 0.16.3 → 0.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/gateway-admin/SKILL.md +15 -0
- package/.claude/skills/yzsgo-e2e/SKILL.md +63 -0
- package/.claude/skills/yzsgo-e2e/SYNC.md +10 -0
- package/.claude/skills/yzsgo-e2e/bootstrap.py +64 -0
- package/.claude/skills/yzsgo-e2e/chat_driver.py +659 -0
- package/.claude/skills/yzsgo-e2e/judge_outcome.js +11 -0
- package/.claude/skills/yzsgo-e2e/judge_workflow.js +100 -0
- package/.claude/skills/yzsgo-e2e/preflight.py +74 -0
- package/.claude/skills/yzsgo-e2e/prep_conversation.py +21 -0
- package/.claude/skills/yzsgo-e2e/pull_wire.py +312 -0
- package/.claude/skills/yzsgo-e2e/run_e2e.py +138 -0
- package/.claude/skills/yzsgo-e2e/verify_drift.py +42 -0
- package/.codex/skills/yzsgo-e2e/SKILL.md +63 -0
- package/.codex/skills/yzsgo-e2e/SYNC.md +10 -0
- package/.codex/skills/yzsgo-e2e/bootstrap.py +64 -0
- package/.codex/skills/yzsgo-e2e/chat_driver.py +659 -0
- package/.codex/skills/yzsgo-e2e/judge_outcome.js +11 -0
- package/.codex/skills/yzsgo-e2e/judge_workflow.js +100 -0
- package/.codex/skills/yzsgo-e2e/preflight.py +74 -0
- package/.codex/skills/yzsgo-e2e/prep_conversation.py +21 -0
- package/.codex/skills/yzsgo-e2e/pull_wire.py +312 -0
- package/.codex/skills/yzsgo-e2e/run_e2e.py +138 -0
- package/.codex/skills/yzsgo-e2e/verify_drift.py +42 -0
- package/AGENTS.md +1 -0
- package/README.md +1 -0
- package/docs/superpowers/plans/2026-08-31-yzsgo-e2e.md +969 -0
- package/docs/superpowers/specs/2026-08-31-yzsgo-e2e-design.md +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
# yzsgo-e2e 通用端到端测试 skill — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 在 optima-dev-skills 里新增 `yzsgo-e2e` skill:Claude Code 用 playwright attach 调试端口 Chrome 驱动 www.yzsgo.com 完成一段对话,拉 gateway wire,复用 conversation-iq 语义管线判定前后端一致性与网关缺陷,confirmed 自动提 issue。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 方案 A(自包含 vendor)。从 optima-store-skills 拷 `chat_driver.py`/`pull_wire.py`,从 optima-gateway conversation-iq 改编 `prep_conversation.py`/`judge_workflow.js`,全部随包进 `.claude/skills/yzsgo-e2e/` 并镜像到 `.codex/`。新写的可判定逻辑(对话定位、浏览器证据合并、三态裁决、报告渲染、preflight 汇总、漂移比对)抽成纯函数并 TDD。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.11(随包脚本,测试用标准库 `unittest`,无第三方依赖);playwright(仅驱动脚本运行时需要,纯函数测试不需要);JavaScript ESM(`judge_workflow.js` 走 Workflow,`judge_outcome.js` 纯函数用 `node --test`);`gh` CLI 提 issue;`sshpass`+buildbox 拉 wire。
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-08-31-yzsgo-e2e-design.md`
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
以下为项目级约束,每个任务的要求都隐含包含本节(值逐字取自 spec):
|
|
16
|
+
|
|
17
|
+
- **skill 名固定 `yzsgo-e2e`**;`.claude/skills/yzsgo-e2e/` 与 `.codex/skills/yzsgo-e2e/` **同名镜像**,内容一致。
|
|
18
|
+
- **随包脚本纯 Python/JS,放 skill 目录内,不进 `bin/`、不进 TS/dist 体系**(对齐 read-code / generate-test-token 的纯随包路线)。
|
|
19
|
+
- **用户不敲 CLI**:脚本参数由 Claude 内部填;SKILL.md 教 Claude 把用户自然语言映射成驱动动作。
|
|
20
|
+
- **三态命名固定**:`confirmed` / `needs_review` / `rejected`。
|
|
21
|
+
- **提 issue 规则**:`confirmed` 直接用 `gh` 提到 **Optima-Chat/optima-gateway**(不给草稿、不设人工门);`needs_review` **不自动提**、进报告待人工;环境类(积分/超时/桌面没连/session 泄漏)**不提**、报告标 `blocked`。
|
|
22
|
+
- **定位本次对话禁用 `ls -t`**:只按 `(started_ts, first_message)` 匹配(沿用 store-skills 血泪纪律)。
|
|
23
|
+
- **vendored 文件钉明上游信源**:`SYNC.md` 记每个 vendored 文件的上游 repo 路径 + commit hash + 同步日期。
|
|
24
|
+
- **两环境**:`--env cn-prod|cn-stage`,**先把 cn-prod 全链路打通**,cn-stage 未验证部分明确标注、不假装支持。
|
|
25
|
+
- **Python 纯函数测试用标准库 `unittest`**(`python3 -m unittest`),测试文件放仓库根 `tests/yzsgo-e2e/`(不进 `.claude`,不随包发布);测试内用 `sys.path.insert` 指到 skill 目录 import 被测脚本。
|
|
26
|
+
- **时间戳统一 UTC ISO8601**(`datetime.now(timezone.utc).isoformat()`),与 wire records 的 `ts` 字段可字符串比较。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## File Structure
|
|
31
|
+
|
|
32
|
+
**新建(随包,`.claude/skills/yzsgo-e2e/`,同名镜像到 `.codex/skills/yzsgo-e2e/`):**
|
|
33
|
+
- `SKILL.md` — 何时用 / 用户说人话如何映射 / 四段流程 / 诚实边界 / vendor 纪律 / 两环境
|
|
34
|
+
- `preflight.py` — 环境自检(探测 + `summarize_preflight` 纯函数汇总)
|
|
35
|
+
- `chat_driver.py` — [vendored ← store-skills] playwright attach 9222 驱动
|
|
36
|
+
- `pull_wire.py` — [vendored ← store-skills] 拉 wire + seq 切分 + 渲染;本 skill 增 `emit_conversation_index` + `locate_conversation`
|
|
37
|
+
- `prep_conversation.py` — [改编 ← conversation-iq/prep_session.py] 备料 + `merge_browser_evidence`
|
|
38
|
+
- `judge_outcome.js` — 纯函数 `decideOutcome`(从 conversation-iq 三态逻辑抽出,可 `node --test`)
|
|
39
|
+
- `judge_workflow.js` — [改编 ← conversation-iq/workflow.js] Workflow 编排,import `judge_outcome.js`
|
|
40
|
+
- `run_e2e.py` — 编排入口(驱动→拉 wire→定位→备料→出备料稿+元数据)+ `render_report`
|
|
41
|
+
- `verify_drift.py` — 可选漂移比对(`plan_drift_checks` 纯函数 + 探测)
|
|
42
|
+
- `SYNC.md` — vendored 上游 commit 台账
|
|
43
|
+
|
|
44
|
+
**新建(测试,仓库根 `tests/yzsgo-e2e/`,不随包):**
|
|
45
|
+
- `test_locate.py` / `test_merge_evidence.py` / `test_render_report.py` / `test_preflight.py` / `test_verify_drift.py`
|
|
46
|
+
- `tests/yzsgo-e2e-outcome.test.js`(node --test)
|
|
47
|
+
|
|
48
|
+
**修改:**
|
|
49
|
+
- `README.md` — skill 清单加 `yzsgo-e2e`
|
|
50
|
+
- `tests/service-matrix-alignment.test.js` — 若校验清单则纳入 `yzsgo-e2e`(Task 7 先读它再决定)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Task 1: skill 骨架 + preflight 汇总纯函数
|
|
55
|
+
|
|
56
|
+
**Files:**
|
|
57
|
+
- Create: `.claude/skills/yzsgo-e2e/SKILL.md`(骨架 frontmatter,正文 Task 7 补全)
|
|
58
|
+
- Create: `.claude/skills/yzsgo-e2e/preflight.py`
|
|
59
|
+
- Create: `.claude/skills/yzsgo-e2e/SYNC.md`(空台账,Task 2 起填)
|
|
60
|
+
- Test: `tests/yzsgo-e2e/test_preflight.py`
|
|
61
|
+
|
|
62
|
+
**Interfaces:**
|
|
63
|
+
- Produces: `summarize_preflight(checks: list[dict]) -> dict`,`checks` 每项 `{"name": str, "ok": bool, "hint": str}`,返回 `{"ok": bool, "missing": list[str], "report": str}`。`ok = all(c["ok"])`;`missing = [c["name"] for c in checks if not c["ok"]]`;`report` 为多行文本,每行 `✅/❌ <name>`,`❌` 行附 `hint`。
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 1: 写失败测试**
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# tests/yzsgo-e2e/test_preflight.py
|
|
69
|
+
import os, sys, unittest
|
|
70
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
71
|
+
from preflight import summarize_preflight
|
|
72
|
+
|
|
73
|
+
class TestPreflight(unittest.TestCase):
|
|
74
|
+
def test_all_ok(self):
|
|
75
|
+
r = summarize_preflight([{"name": "chrome-9222", "ok": True, "hint": "x"}])
|
|
76
|
+
self.assertTrue(r["ok"]); self.assertEqual(r["missing"], [])
|
|
77
|
+
self.assertIn("✅ chrome-9222", r["report"])
|
|
78
|
+
|
|
79
|
+
def test_missing_lists_and_hints(self):
|
|
80
|
+
r = summarize_preflight([
|
|
81
|
+
{"name": "chrome-9222", "ok": True, "hint": "x"},
|
|
82
|
+
{"name": "buildbox-pw", "ok": False, "hint": "创建 ~/.buildbox_pw"},
|
|
83
|
+
])
|
|
84
|
+
self.assertFalse(r["ok"]); self.assertEqual(r["missing"], ["buildbox-pw"])
|
|
85
|
+
self.assertIn("❌ buildbox-pw", r["report"]); self.assertIn("创建 ~/.buildbox_pw", r["report"])
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
92
|
+
|
|
93
|
+
Run: `python3 -m unittest tests.yzsgo-e2e.test_preflight -v`(在仓库根,或 `python3 tests/yzsgo-e2e/test_preflight.py`)
|
|
94
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'preflight'`
|
|
95
|
+
|
|
96
|
+
- [ ] **Step 3: 写 preflight.py**
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
#!/usr/bin/env python3
|
|
100
|
+
"""yzsgo-e2e 环境自检。探测函数有副作用(读端口/文件),汇总用纯函数 summarize_preflight。
|
|
101
|
+
缺项只指路(不替用户做一次性登录/充值),准备步骤见 store-skills 的 setting-up-yzsgo-test-env。"""
|
|
102
|
+
import os, shutil, socket, sys
|
|
103
|
+
|
|
104
|
+
def _port_open(port: int) -> bool:
|
|
105
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
106
|
+
s.settimeout(1.0)
|
|
107
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
108
|
+
|
|
109
|
+
def probe(env: str = "cn-prod") -> list[dict]:
|
|
110
|
+
return [
|
|
111
|
+
{"name": "chrome-9222", "ok": _port_open(9222),
|
|
112
|
+
"hint": '起调试端口 Chrome:open -na "Google Chrome" --args --remote-debugging-port=9222 '
|
|
113
|
+
'--user-data-dir=/tmp/yzsgo-chrome https://www.yzsgo.com(手动登测试账号)'},
|
|
114
|
+
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")),
|
|
115
|
+
"hint": "拉 wire 需 buildbox 口令文件 ~/.buildbox_pw(见 setting-up-yzsgo-test-env)"},
|
|
116
|
+
{"name": "sshpass", "ok": shutil.which("sshpass") is not None,
|
|
117
|
+
"hint": "brew install hudochenkov/sshpass/sshpass"},
|
|
118
|
+
{"name": "playwright", "ok": _has_playwright(),
|
|
119
|
+
"hint": "pip install playwright && playwright install chromium"},
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def _has_playwright() -> bool:
|
|
123
|
+
try:
|
|
124
|
+
import playwright # noqa: F401
|
|
125
|
+
return True
|
|
126
|
+
except Exception:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def summarize_preflight(checks: list) -> dict:
|
|
130
|
+
ok = all(c["ok"] for c in checks)
|
|
131
|
+
missing = [c["name"] for c in checks if not c["ok"]]
|
|
132
|
+
rows = []
|
|
133
|
+
for c in checks:
|
|
134
|
+
mark = "✅" if c["ok"] else "❌"
|
|
135
|
+
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['hint']}"))
|
|
136
|
+
return {"ok": ok, "missing": missing, "report": "\n".join(rows)}
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
env = sys.argv[1] if len(sys.argv) > 1 else "cn-prod"
|
|
140
|
+
r = summarize_preflight(probe(env))
|
|
141
|
+
print(r["report"])
|
|
142
|
+
sys.exit(0 if r["ok"] else 1)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
同时建 `SKILL.md` 骨架(正文 Task 7 补):
|
|
146
|
+
|
|
147
|
+
```markdown
|
|
148
|
+
---
|
|
149
|
+
name: "yzsgo-e2e"
|
|
150
|
+
description: "当用户请求端到端测试鸭嘴兽、e2e 测 yzsgo 对话、用浏览器真机测鸭嘴兽整个流程、驱动对话再拉 wire 核对前后端、测网关/agent 端到端有没有问题时,使用此技能。playwright attach 调试端口 Chrome 驱 www.yzsgo.com 对话 → 拉 gateway wire → conversation-iq 语义管线判定 → confirmed 自动提 issue。"
|
|
151
|
+
allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
<!-- 正文见 Task 7 -->
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
建空 `SYNC.md`:
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
# vendored 文件上游台账
|
|
161
|
+
|
|
162
|
+
| 本文件 | 上游 repo · 路径 | commit | 同步日期 |
|
|
163
|
+
|---|---|---|---|
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
167
|
+
|
|
168
|
+
Run: `python3 tests/yzsgo-e2e/test_preflight.py`
|
|
169
|
+
Expected: PASS(2 tests)
|
|
170
|
+
|
|
171
|
+
- [ ] **Step 5: 提交**
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git add .claude/skills/yzsgo-e2e/{SKILL.md,preflight.py,SYNC.md} tests/yzsgo-e2e/test_preflight.py
|
|
175
|
+
git commit -m "feat(yzsgo-e2e): skill 骨架 + preflight 环境自检"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Task 2: vendor chat_driver.py + pull_wire.py + SYNC 台账
|
|
181
|
+
|
|
182
|
+
**Files:**
|
|
183
|
+
- Create: `.claude/skills/yzsgo-e2e/chat_driver.py`(拷贝)
|
|
184
|
+
- Create: `.claude/skills/yzsgo-e2e/pull_wire.py`(拷贝)
|
|
185
|
+
- Modify: `.claude/skills/yzsgo-e2e/SYNC.md`
|
|
186
|
+
- Test: `tests/yzsgo-e2e/test_imports.py`
|
|
187
|
+
|
|
188
|
+
**Interfaces:**
|
|
189
|
+
- Produces: `chat_driver.ChatDriver`(含 `attach/new_conversation/send/wait_reply/send_and_wait/answer_question/ensure_idle/preflight` 方法,签名同上游);`pull_wire.segment/render_conversation/first_user_text/make_deref/pull/render_all`(同上游)。
|
|
190
|
+
|
|
191
|
+
- [ ] **Step 1: 写失败测试(import smoke)**
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# tests/yzsgo-e2e/test_imports.py
|
|
195
|
+
import os, sys, unittest
|
|
196
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
197
|
+
|
|
198
|
+
class TestImports(unittest.TestCase):
|
|
199
|
+
def test_pull_wire_symbols(self):
|
|
200
|
+
import pull_wire
|
|
201
|
+
for fn in ("segment", "render_conversation", "first_user_text", "make_deref"):
|
|
202
|
+
self.assertTrue(hasattr(pull_wire, fn), fn)
|
|
203
|
+
|
|
204
|
+
def test_chat_driver_class(self):
|
|
205
|
+
import chat_driver # 需环境已装 playwright
|
|
206
|
+
self.assertTrue(hasattr(chat_driver, "ChatDriver"))
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
unittest.main()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
213
|
+
|
|
214
|
+
Run: `python3 tests/yzsgo-e2e/test_imports.py`
|
|
215
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'pull_wire'`
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 3: 拷贝上游文件并记台账**
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
SS=~/optima-store-skills/.claude/skills
|
|
221
|
+
cp "$SS/operating-yzsgo-chat/chat_driver.py" .claude/skills/yzsgo-e2e/chat_driver.py
|
|
222
|
+
cp "$SS/pulling-yzsgo-session-wire/pull_wire.py" .claude/skills/yzsgo-e2e/pull_wire.py
|
|
223
|
+
# 记录上游 commit
|
|
224
|
+
git -C ~/optima-store-skills rev-parse --short HEAD
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
把上一行输出的 commit 填入 `SYNC.md`(示例,实际填真实 hash 与当日日期):
|
|
228
|
+
|
|
229
|
+
```markdown
|
|
230
|
+
| chat_driver.py | optima-store-skills · .claude/skills/operating-yzsgo-chat/chat_driver.py | <hash> | 2026-08-31 |
|
|
231
|
+
| pull_wire.py | optima-store-skills · .claude/skills/pulling-yzsgo-session-wire/pull_wire.py | <hash> | 2026-08-31 |
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
> 注:原样 vendored,不裁 `ensure_installed` 等 marketplace 方法(保持 diff 干净、漂移比对简单;裁剪列为后续可选)。
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
237
|
+
|
|
238
|
+
Run: `.venv/bin/python tests/yzsgo-e2e/test_imports.py`(venv 需先 `pip install playwright`;若无 playwright,`test_chat_driver_class` 会 skip 不了则先只验 `test_pull_wire_symbols`)
|
|
239
|
+
Expected: `test_pull_wire_symbols` PASS;playwright 在场时 `test_chat_driver_class` PASS
|
|
240
|
+
|
|
241
|
+
- [ ] **Step 5: 提交**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
git add .claude/skills/yzsgo-e2e/{chat_driver.py,pull_wire.py,SYNC.md} tests/yzsgo-e2e/test_imports.py
|
|
245
|
+
git commit -m "feat(yzsgo-e2e): vendor chat_driver + pull_wire(记 SYNC 台账)"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Task 3: 对话索引 emit + locate_conversation 定位纯函数
|
|
251
|
+
|
|
252
|
+
**Files:**
|
|
253
|
+
- Modify: `.claude/skills/yzsgo-e2e/pull_wire.py`(追加 `emit_conversation_index` + `locate_conversation`)
|
|
254
|
+
- Test: `tests/yzsgo-e2e/test_locate.py`
|
|
255
|
+
|
|
256
|
+
**Interfaces:**
|
|
257
|
+
- Consumes: `pull_wire.segment`, `pull_wire.first_user_text`, `pull_wire.make_deref`(Task 2)。
|
|
258
|
+
- Produces:
|
|
259
|
+
- `emit_conversation_index(wire_root: str) -> list[dict]`,每项 `{"gidx": int, "sid": str, "ts": str, "prompt": str}`(gidx 从 1 递增,跨 session 连续,与 `render_all` 编号一致;ts = 对话首 req 的 `ts`;prompt = `first_user_text`)。
|
|
260
|
+
- `locate_conversation(index: list[dict], started_ts: str, first_message: str) -> dict | None`:在 `index` 里找本次对话。匹配规则:① 候选 = `first_message.strip()[:40]` 与 `item["prompt"]` 互相包含(任一方 startswith 另一方前 40 字);② 候选按 `ts` 升序,取第一个 `ts >= started_ts` 的;③ 无 ts 满足则取候选里 ts 最大的;④ 无候选返回 `None`。
|
|
261
|
+
|
|
262
|
+
- [ ] **Step 1: 写失败测试**
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# tests/yzsgo-e2e/test_locate.py
|
|
266
|
+
import os, sys, unittest
|
|
267
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
268
|
+
from pull_wire import locate_conversation
|
|
269
|
+
|
|
270
|
+
IDX = [
|
|
271
|
+
{"gidx": 1, "sid": "s1", "ts": "2026-08-31T10:00:00Z", "prompt": "我想做电商帮我看看"},
|
|
272
|
+
{"gidx": 2, "sid": "s1", "ts": "2026-08-31T14:03:10Z", "prompt": "我想做电商帮我看看行情"},
|
|
273
|
+
{"gidx": 3, "sid": "s1", "ts": "2026-08-31T14:05:00Z", "prompt": "换个话题聊聊物流"},
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
class TestLocate(unittest.TestCase):
|
|
277
|
+
def test_picks_first_after_started_among_matches(self):
|
|
278
|
+
r = locate_conversation(IDX, "2026-08-31T14:00:00Z", "我想做电商帮我看看")
|
|
279
|
+
self.assertEqual(r["gidx"], 2) # gidx1 在 started 之前,被排除
|
|
280
|
+
|
|
281
|
+
def test_no_match_returns_none(self):
|
|
282
|
+
self.assertIsNone(locate_conversation(IDX, "2026-08-31T14:00:00Z", "完全不相干的话"))
|
|
283
|
+
|
|
284
|
+
def test_fallback_latest_when_none_after_started(self):
|
|
285
|
+
r = locate_conversation(IDX, "2026-08-31T23:00:00Z", "我想做电商帮我看看")
|
|
286
|
+
self.assertEqual(r["gidx"], 2) # 无 ts>=started 的候选 → 候选里 ts 最大
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
unittest.main()
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
293
|
+
|
|
294
|
+
Run: `python3 tests/yzsgo-e2e/test_locate.py`
|
|
295
|
+
Expected: FAIL — `ImportError: cannot import name 'locate_conversation'`
|
|
296
|
+
|
|
297
|
+
- [ ] **Step 3: 追加实现到 pull_wire.py 末尾**
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
# ── yzsgo-e2e 增补:结构化对话索引 + 本次对话定位 ──
|
|
301
|
+
|
|
302
|
+
def emit_conversation_index(wire_root):
|
|
303
|
+
"""遍历 wire_root 下各 session,切分对话,产出与 render_all 同序的结构化索引。"""
|
|
304
|
+
import os as _os
|
|
305
|
+
out = []
|
|
306
|
+
gidx = 0
|
|
307
|
+
sessions = sorted(d for d in _os.listdir(wire_root)
|
|
308
|
+
if _os.path.isdir(_os.path.join(wire_root, d)))
|
|
309
|
+
for sid in sessions:
|
|
310
|
+
sdir = _os.path.join(wire_root, sid)
|
|
311
|
+
recf = _os.path.join(sdir, "records.jsonl")
|
|
312
|
+
if not _os.path.exists(recf):
|
|
313
|
+
continue
|
|
314
|
+
deref = make_deref(sdir)
|
|
315
|
+
recs = [json.loads(l) for l in open(recf, encoding="utf-8") if l.strip()]
|
|
316
|
+
reqs = [r for r in recs if r.get("kind") == "request"]
|
|
317
|
+
for conv in segment(reqs, deref):
|
|
318
|
+
gidx += 1
|
|
319
|
+
out.append({"gidx": gidx, "sid": sid,
|
|
320
|
+
"ts": conv[0].get("ts", ""),
|
|
321
|
+
"prompt": first_user_text(conv[0], deref)})
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def locate_conversation(index, started_ts, first_message):
|
|
326
|
+
"""按 (started_ts, first_message) 定位本次对话;禁用 ls -t。规则见 plan Task 3 Interfaces。"""
|
|
327
|
+
key = (first_message or "").strip()[:40]
|
|
328
|
+
cands = []
|
|
329
|
+
for it in index:
|
|
330
|
+
p = (it.get("prompt") or "").strip()
|
|
331
|
+
if not key:
|
|
332
|
+
continue
|
|
333
|
+
if p[:40].startswith(key) or key.startswith(p[:40]):
|
|
334
|
+
cands.append(it)
|
|
335
|
+
if not cands:
|
|
336
|
+
return None
|
|
337
|
+
cands.sort(key=lambda x: x.get("ts", ""))
|
|
338
|
+
after = [c for c in cands if c.get("ts", "") >= started_ts]
|
|
339
|
+
if after:
|
|
340
|
+
return after[0]
|
|
341
|
+
return cands[-1]
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
345
|
+
|
|
346
|
+
Run: `python3 tests/yzsgo-e2e/test_locate.py`
|
|
347
|
+
Expected: PASS(3 tests)
|
|
348
|
+
|
|
349
|
+
- [ ] **Step 5: 提交**
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
git add .claude/skills/yzsgo-e2e/pull_wire.py tests/yzsgo-e2e/test_locate.py
|
|
353
|
+
git commit -m "feat(yzsgo-e2e): 对话索引 emit + 按时间戳/prompt 定位本次对话"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Task 4: prep_conversation.py(改编备料 + 浏览器证据合并)
|
|
359
|
+
|
|
360
|
+
**Files:**
|
|
361
|
+
- Create: `.claude/skills/yzsgo-e2e/prep_conversation.py`
|
|
362
|
+
- Modify: `.claude/skills/yzsgo-e2e/SYNC.md`(登记改编来源)
|
|
363
|
+
- Test: `tests/yzsgo-e2e/test_merge_evidence.py`
|
|
364
|
+
|
|
365
|
+
**Interfaces:**
|
|
366
|
+
- Consumes: `pull_wire.render_conversation`(用其渲染 wire 侧备料稿)。
|
|
367
|
+
- Produces: `merge_browser_evidence(wire_md: str, browser_turns: list[dict]) -> str`。`browser_turns` 每项 `{"sent": str, "state": str, "transcript": str, "tool_trace": Any, "timed_out": bool}`(`transcript`/`state` 来自 `chat_driver.wait_reply`)。返回 = `wire_md` + 追加一节 `## 前端所见(浏览器侧证据;与上面 wire 对照)`,逐轮列出 `sent/state/timed_out/transcript`,并在节首写一行提示要求判定者核对前后端一致性。
|
|
368
|
+
|
|
369
|
+
- [ ] **Step 1: 写失败测试**
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# tests/yzsgo-e2e/test_merge_evidence.py
|
|
373
|
+
import os, sys, unittest
|
|
374
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
375
|
+
from prep_conversation import merge_browser_evidence
|
|
376
|
+
|
|
377
|
+
class TestMerge(unittest.TestCase):
|
|
378
|
+
def test_appends_browser_section_with_turns(self):
|
|
379
|
+
wire = "# 对话 #2\n## 事实卡\n- LLM 轮数 3\n"
|
|
380
|
+
turns = [
|
|
381
|
+
{"sent": "你好", "state": "done", "transcript": "你好!我是鸭嘴兽…", "tool_trace": [], "timed_out": False},
|
|
382
|
+
{"sent": "看看行情", "state": "service_error", "transcript": "", "tool_trace": [], "timed_out": False},
|
|
383
|
+
]
|
|
384
|
+
out = merge_browser_evidence(wire, turns)
|
|
385
|
+
self.assertIn("# 对话 #2", out) # 保留 wire 原文
|
|
386
|
+
self.assertIn("## 前端所见", out) # 追加浏览器节
|
|
387
|
+
self.assertIn("你好!我是鸭嘴兽", out) # 前端渲染内容进证据
|
|
388
|
+
self.assertIn("service_error", out) # 前端异常态可见
|
|
389
|
+
self.assertIn("对照", out) # 有前后端对照提示
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
unittest.main()
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
396
|
+
|
|
397
|
+
Run: `python3 tests/yzsgo-e2e/test_merge_evidence.py`
|
|
398
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'prep_conversation'`
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 3: 写 prep_conversation.py**
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
#!/usr/bin/env python3
|
|
404
|
+
"""yzsgo-e2e 备料层:wire 侧备料稿(复用 pull_wire.render_conversation)+ 合并浏览器侧证据。
|
|
405
|
+
改编自 optima-gateway conversation-iq/prep_session.py(备料思路)与 pull_wire(渲染)。见 SYNC.md。"""
|
|
406
|
+
import json
|
|
407
|
+
|
|
408
|
+
def merge_browser_evidence(wire_md: str, browser_turns: list) -> str:
|
|
409
|
+
lines = [wire_md.rstrip(), "",
|
|
410
|
+
"## 前端所见(浏览器侧证据;与上面 wire 对照)",
|
|
411
|
+
"> 判定要求:逐轮核对**前端渲染的**与**wire 里 agent 真实产出的**是否一致——"
|
|
412
|
+
"前端丢内容/半截/报错但 wire 成功(或反之)即为缺陷。", ""]
|
|
413
|
+
for i, t in enumerate(browser_turns):
|
|
414
|
+
lines.append(f"### 轮 #{i} 发送: {t.get('sent','')!r}")
|
|
415
|
+
lines.append(f"- state: {t.get('state','')} timed_out: {t.get('timed_out', False)}")
|
|
416
|
+
tt = t.get("tool_trace")
|
|
417
|
+
if tt:
|
|
418
|
+
lines.append(f"- tool_trace: {json.dumps(tt, ensure_ascii=False)[:1500]}")
|
|
419
|
+
lines.append("- 前端整轮渲染:")
|
|
420
|
+
lines.append("```")
|
|
421
|
+
lines.append((t.get("transcript") or "(空)")[:6000])
|
|
422
|
+
lines.append("```")
|
|
423
|
+
return "\n".join(lines)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
在 `SYNC.md` 追加:
|
|
427
|
+
|
|
428
|
+
```markdown
|
|
429
|
+
| prep_conversation.py | optima-gateway · .claude/skills/conversation-iq/prep_session.py(改编:+浏览器证据合并) | <hash> | 2026-08-31 |
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
(`<hash>` 用 `git -C ~/optima-gateway rev-parse --short HEAD` 的输出。)
|
|
433
|
+
|
|
434
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
435
|
+
|
|
436
|
+
Run: `python3 tests/yzsgo-e2e/test_merge_evidence.py`
|
|
437
|
+
Expected: PASS(1 test)
|
|
438
|
+
|
|
439
|
+
- [ ] **Step 5: 提交**
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
git add .claude/skills/yzsgo-e2e/prep_conversation.py .claude/skills/yzsgo-e2e/SYNC.md tests/yzsgo-e2e/test_merge_evidence.py
|
|
443
|
+
git commit -m "feat(yzsgo-e2e): 备料层——合并浏览器侧证据做前后端对照"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Task 5: judge_outcome.js 三态裁决纯函数 + judge_workflow.js
|
|
449
|
+
|
|
450
|
+
**Files:**
|
|
451
|
+
- Create: `.claude/skills/yzsgo-e2e/judge_outcome.js`
|
|
452
|
+
- Create: `.claude/skills/yzsgo-e2e/judge_workflow.js`
|
|
453
|
+
- Modify: `.claude/skills/yzsgo-e2e/SYNC.md`
|
|
454
|
+
- Test: `tests/yzsgo-e2e-outcome.test.js`
|
|
455
|
+
|
|
456
|
+
**Interfaces:**
|
|
457
|
+
- Produces: `judge_outcome.js` 导出 `decideOutcome(votes)`,`votes` 为数组,元素 `{refuted: boolean}` 或 `null`(agent 失败)。逻辑(同 conversation-iq):`good = votes.filter(Boolean)`;`n = good.length`;`refuted = good.filter(v => v.refuted).length`;`n===0 → 'needs_review'`;`refuted===n → 'rejected'`;`refuted > n/2 → 'needs_review'`;否则 `'confirmed'`。
|
|
458
|
+
- `judge_workflow.js` 是 Workflow 脚本(`export const meta` + `agent/parallel/pipeline` 全局),import `decideOutcome`;judge prompt 增「前端所见 vs wire 一致性」维度;`args.knownIssues` 由 Claude 运行时用 `gh` 拉入。
|
|
459
|
+
|
|
460
|
+
- [ ] **Step 1: 写失败测试**
|
|
461
|
+
|
|
462
|
+
```javascript
|
|
463
|
+
// tests/yzsgo-e2e-outcome.test.js
|
|
464
|
+
import { test } from "node:test";
|
|
465
|
+
import assert from "node:assert";
|
|
466
|
+
import { decideOutcome } from "../.claude/skills/yzsgo-e2e/judge_outcome.js";
|
|
467
|
+
|
|
468
|
+
test("多数不反驳 → confirmed", () => {
|
|
469
|
+
assert.equal(decideOutcome([{refuted:false},{refuted:false},{refuted:true}]), "confirmed");
|
|
470
|
+
});
|
|
471
|
+
test("全票反驳 → rejected", () => {
|
|
472
|
+
assert.equal(decideOutcome([{refuted:true},{refuted:true},{refuted:true}]), "rejected");
|
|
473
|
+
});
|
|
474
|
+
test("过半但非全票反驳 → needs_review (split-vote)", () => {
|
|
475
|
+
assert.equal(decideOutcome([{refuted:true},{refuted:true},{refuted:false}]), "needs_review");
|
|
476
|
+
});
|
|
477
|
+
test("全员失败(null) → needs_review", () => {
|
|
478
|
+
assert.equal(decideOutcome([null,null,null]), "needs_review");
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
483
|
+
|
|
484
|
+
Run: `node --test tests/yzsgo-e2e-outcome.test.js`
|
|
485
|
+
Expected: FAIL — 无法 import `judge_outcome.js`
|
|
486
|
+
|
|
487
|
+
- [ ] **Step 3: 写 judge_outcome.js + judge_workflow.js**
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
// .claude/skills/yzsgo-e2e/judge_outcome.js
|
|
491
|
+
export function decideOutcome(votes) {
|
|
492
|
+
const good = (votes || []).filter(Boolean);
|
|
493
|
+
const n = good.length;
|
|
494
|
+
const refuted = good.filter((v) => v.refuted).length;
|
|
495
|
+
if (n === 0) return "needs_review";
|
|
496
|
+
if (refuted === n) return "rejected";
|
|
497
|
+
if (refuted > n / 2) return "needs_review";
|
|
498
|
+
return "confirmed";
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```javascript
|
|
503
|
+
// .claude/skills/yzsgo-e2e/judge_workflow.js
|
|
504
|
+
// 改编自 optima-gateway conversation-iq/workflow.js(见 SYNC.md)。
|
|
505
|
+
// 输入 args: { base, sids, knownIssues }(备料稿目录 / 待判对话文件名列表 / gh 实时拉的开着 issue 文本)。
|
|
506
|
+
import { decideOutcome } from "./judge_outcome.js";
|
|
507
|
+
|
|
508
|
+
export const meta = {
|
|
509
|
+
name: "yzsgo-e2e-judge",
|
|
510
|
+
description: "读 e2e 备料稿(wire + 前端证据)逐对话判缺陷,对抗验证出三态",
|
|
511
|
+
phases: [
|
|
512
|
+
{ title: "Judge", detail: "每对话一个 agent 读备料稿判缺陷(含前后端一致性)" },
|
|
513
|
+
{ title: "Verify", detail: "每条 novel finding 3 个 skeptic 读 runtime 源码反驳,三态裁决" },
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const A = typeof args === "string" ? JSON.parse(args) : args || {};
|
|
518
|
+
|
|
519
|
+
const STABLE_NORMAL = `## 正常现象(不是缺陷,别报):
|
|
520
|
+
- compaction 摘要调用、abort、max_tokens 截断本身;
|
|
521
|
+
- 前端 new_conversation 后主区只有本轮(预期)。`;
|
|
522
|
+
|
|
523
|
+
const JUDGE_SCHEMA = {
|
|
524
|
+
type: "object",
|
|
525
|
+
properties: {
|
|
526
|
+
impression: { type: "string" },
|
|
527
|
+
findings: {
|
|
528
|
+
type: "array",
|
|
529
|
+
items: {
|
|
530
|
+
type: "object",
|
|
531
|
+
properties: {
|
|
532
|
+
what: { type: "string" },
|
|
533
|
+
evidence: { type: "string" },
|
|
534
|
+
knownIssue: { type: "string" },
|
|
535
|
+
},
|
|
536
|
+
required: ["what", "evidence"],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
required: ["impression", "findings"],
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const VERDICT_SCHEMA = {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: { refuted: { type: "boolean" }, reason: { type: "string" } },
|
|
546
|
+
required: ["refuted", "reason"],
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
function judgePrompt(path) {
|
|
550
|
+
return `读备料稿 ${path}(含 wire transcript + 「前端所见」证据)。判这次端到端对话有没有网关/agent 缺陷。
|
|
551
|
+
重点核对:① wire 里 agent 真实产出 vs 前端渲染是否一致(丢内容/半截/前端报错但 wire 成功、或反之);
|
|
552
|
+
② 悬空 tool_use / max_tokens 截断 / error / abort 是否造成用户可感问题;③ 回答是否编数据/答非所问/活没干完。
|
|
553
|
+
${STABLE_NORMAL}
|
|
554
|
+
已知开着的 issue(命中就在 knownIssue 里标 #号,仅打标签、不要因此不报):
|
|
555
|
+
${A.knownIssues || "(无)"}
|
|
556
|
+
默认健康:证据不足别硬报。输出 impression + findings。`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function verifyPrompt(f, path) {
|
|
560
|
+
return `有人在 ${path} 报了缺陷:「${f.what}」,证据:${f.evidence}。
|
|
561
|
+
你是 skeptic:读**真实 runtime 源码**(optima-gateway / agent-runtime)核对机制,尽力反驳。默认判假(refuted=true),
|
|
562
|
+
只有确凿证明该缺陷真实存在才 refuted=false。输出 refuted + reason。`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const results = await pipeline(
|
|
566
|
+
A.sids,
|
|
567
|
+
(sid) =>
|
|
568
|
+
agent(judgePrompt(`${A.base}/${sid}.md`), {
|
|
569
|
+
label: `judge:${sid}`,
|
|
570
|
+
phase: "Judge",
|
|
571
|
+
schema: JUDGE_SCHEMA,
|
|
572
|
+
}).then((judge) => ({ sid, judge })),
|
|
573
|
+
async (prev) => {
|
|
574
|
+
if (!prev || !prev.judge) return { sid: prev?.sid, confirmed: [], needsReview: [], rejected: [] };
|
|
575
|
+
const path = `${A.base}/${prev.sid}.md`;
|
|
576
|
+
const novel = prev.judge.findings || [];
|
|
577
|
+
if (novel.length === 0)
|
|
578
|
+
return { sid: prev.sid, impression: prev.judge.impression, confirmed: [], needsReview: [], rejected: [] };
|
|
579
|
+
const verified = await parallel(
|
|
580
|
+
novel.map((f) => async () => {
|
|
581
|
+
const votes = await parallel(
|
|
582
|
+
[0, 1, 2].map(() => () => agent(verifyPrompt(f, path), { label: `verify:${prev.sid}`, phase: "Verify", schema: VERDICT_SCHEMA }))
|
|
583
|
+
);
|
|
584
|
+
return { finding: f, outcome: decideOutcome(votes) };
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
const by = (o) => verified.filter(Boolean).filter((v) => v.outcome === o);
|
|
588
|
+
return { sid: prev.sid, impression: prev.judge.impression, confirmed: by("confirmed"), needsReview: by("needs_review"), rejected: by("rejected") };
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
return { results: results.filter(Boolean) };
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
`SYNC.md` 追加:
|
|
596
|
+
|
|
597
|
+
```markdown
|
|
598
|
+
| judge_workflow.js | optima-gateway · .claude/skills/conversation-iq/workflow.js(改编:+前后端一致性维度) | <hash> | 2026-08-31 |
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
602
|
+
|
|
603
|
+
Run: `node --test tests/yzsgo-e2e-outcome.test.js`
|
|
604
|
+
Expected: PASS(4 tests)
|
|
605
|
+
|
|
606
|
+
- [ ] **Step 5: 提交**
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
git add .claude/skills/yzsgo-e2e/judge_outcome.js .claude/skills/yzsgo-e2e/judge_workflow.js .claude/skills/yzsgo-e2e/SYNC.md tests/yzsgo-e2e-outcome.test.js
|
|
610
|
+
git commit -m "feat(yzsgo-e2e): judge 三态裁决纯函数 + Workflow 编排(前后端一致性维度)"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Task 6: run_e2e.py 编排入口 + render_report 纯函数
|
|
616
|
+
|
|
617
|
+
**Files:**
|
|
618
|
+
- Create: `.claude/skills/yzsgo-e2e/run_e2e.py`
|
|
619
|
+
- Test: `tests/yzsgo-e2e/test_render_report.py`
|
|
620
|
+
|
|
621
|
+
**Interfaces:**
|
|
622
|
+
- Consumes: `preflight.{probe,summarize_preflight}`、`chat_driver.ChatDriver`、`pull_wire.{pull,emit_conversation_index,locate_conversation,render_conversation}`、`prep_conversation.merge_browser_evidence`。
|
|
623
|
+
- Produces:
|
|
624
|
+
- `render_report(result: dict) -> str`:`result = {"env","started_ts","first_message","confirmed":[...],"needs_review":[...],"blocked": str|None,"coverage": str}`;`confirmed`/`needs_review` 每项 `{"what","evidence"}`。输出 markdown:标题 + 「本次端到端问题」表(confirmed 标 `confirmed`、needs_review 标 `needs_review`)+ `## 覆盖边界` + `## 判断修正`;`blocked` 非空时顶部标 `> ⚠️ blocked: <原因>(环境问题,非缺陷,未提 issue)`。
|
|
625
|
+
- `main()`:argparse `--env/--message(可多次)/--answer(可多次 k=v)/--expect/--out/--issue-repo`(默认 `Optima-Chat/optima-gateway`);串起四段,落备料稿 + 元数据 JSON(含 `started_ts/first_message/located_gidx`)到 `--out`,打印备料稿路径供 Claude 跑 `judge_workflow.js`。**run_e2e 不在脚本里跑 LLM**(judge 由 Claude 用 Workflow 跑);提 issue 由 Claude 依 SKILL.md 规则用 `gh` 做。
|
|
626
|
+
|
|
627
|
+
- [ ] **Step 1: 写失败测试**
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
# tests/yzsgo-e2e/test_render_report.py
|
|
631
|
+
import os, sys, unittest
|
|
632
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
633
|
+
from run_e2e import render_report
|
|
634
|
+
|
|
635
|
+
class TestReport(unittest.TestCase):
|
|
636
|
+
def test_confirmed_and_needs_review_rows(self):
|
|
637
|
+
md = render_report({
|
|
638
|
+
"env": "cn-prod", "started_ts": "2026-08-31T14:00:00Z", "first_message": "你好",
|
|
639
|
+
"confirmed": [{"what": "前端丢了工具卡", "evidence": "wire 有 tool_result,前端未渲染"}],
|
|
640
|
+
"needs_review": [{"what": "疑似截断", "evidence": "max_tokens=1"}],
|
|
641
|
+
"blocked": None, "coverage": "单次对话;skeptic 读源码",
|
|
642
|
+
})
|
|
643
|
+
self.assertIn("前端丢了工具卡", md)
|
|
644
|
+
self.assertIn("confirmed", md); self.assertIn("needs_review", md)
|
|
645
|
+
self.assertIn("## 覆盖边界", md); self.assertIn("## 判断修正", md)
|
|
646
|
+
self.assertNotIn("blocked:", md)
|
|
647
|
+
|
|
648
|
+
def test_blocked_banner(self):
|
|
649
|
+
md = render_report({"env": "cn-prod", "started_ts": "t", "first_message": "x",
|
|
650
|
+
"confirmed": [], "needs_review": [], "blocked": "积分不足", "coverage": "-"})
|
|
651
|
+
self.assertIn("blocked: 积分不足", md)
|
|
652
|
+
|
|
653
|
+
if __name__ == "__main__":
|
|
654
|
+
unittest.main()
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
658
|
+
|
|
659
|
+
Run: `python3 tests/yzsgo-e2e/test_render_report.py`
|
|
660
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'run_e2e'`
|
|
661
|
+
|
|
662
|
+
- [ ] **Step 3: 写 run_e2e.py**
|
|
663
|
+
|
|
664
|
+
```python
|
|
665
|
+
#!/usr/bin/env python3
|
|
666
|
+
"""yzsgo-e2e 编排:驱动对话 → 拉 wire → 定位本次对话 → 备料(含浏览器证据)→ 出备料稿+元数据。
|
|
667
|
+
judge 由 Claude 用 judge_workflow.js 跑;提 issue 由 Claude 依 SKILL.md 用 gh 做。用户不敲本脚本。"""
|
|
668
|
+
import argparse, json, os, sys
|
|
669
|
+
from datetime import datetime, timezone
|
|
670
|
+
|
|
671
|
+
def render_report(result: dict) -> str:
|
|
672
|
+
L = [f"# yzsgo-e2e 报告 · {result['env']} · {result['started_ts']}", ""]
|
|
673
|
+
if result.get("blocked"):
|
|
674
|
+
L.append(f"> ⚠️ blocked: {result['blocked']}(环境问题,非缺陷,未提 issue)")
|
|
675
|
+
L.append("")
|
|
676
|
+
L += ["## 本次端到端问题(按严重度)", "", "| 状态 | 问题 | 证据 |", "|---|---|---|"]
|
|
677
|
+
for f in result.get("confirmed", []):
|
|
678
|
+
L.append(f"| confirmed | {f['what']} | {f['evidence']} |")
|
|
679
|
+
for f in result.get("needs_review", []):
|
|
680
|
+
L.append(f"| needs_review | {f['what']} | {f['evidence']} |")
|
|
681
|
+
if not result.get("confirmed") and not result.get("needs_review"):
|
|
682
|
+
L.append("| — | 无 confirmed/needs_review | — |")
|
|
683
|
+
L += ["", "## 覆盖边界", result.get("coverage", "-"),
|
|
684
|
+
"", "## 判断修正", "(如判定过程中修正过结论,如实记此;无则写'无')"]
|
|
685
|
+
return "\n".join(L)
|
|
686
|
+
|
|
687
|
+
def _utc_now() -> str:
|
|
688
|
+
return datetime.now(timezone.utc).isoformat()
|
|
689
|
+
|
|
690
|
+
def _parse_answers(pairs):
|
|
691
|
+
out = []
|
|
692
|
+
for p in pairs or []:
|
|
693
|
+
if "=" in p:
|
|
694
|
+
k, v = p.split("=", 1)
|
|
695
|
+
out.append({"match": k, "answer": v})
|
|
696
|
+
return out
|
|
697
|
+
|
|
698
|
+
def main():
|
|
699
|
+
ap = argparse.ArgumentParser()
|
|
700
|
+
ap.add_argument("--env", default="cn-prod", choices=["cn-prod", "cn-stage"])
|
|
701
|
+
ap.add_argument("--message", action="append", required=True, help="逐轮发送的消息(可多次)")
|
|
702
|
+
ap.add_argument("--answer", action="append", help="反问预设答案 关键词=答案(可多次)")
|
|
703
|
+
ap.add_argument("--expect", default="", help="关注点,喂给判定层")
|
|
704
|
+
ap.add_argument("--user", required=True, help="测试账号 userId(拉 wire 用)")
|
|
705
|
+
ap.add_argument("--out", default="e2e-out")
|
|
706
|
+
ap.add_argument("--issue-repo", default="Optima-Chat/optima-gateway")
|
|
707
|
+
args = ap.parse_args()
|
|
708
|
+
|
|
709
|
+
if args.env == "cn-stage":
|
|
710
|
+
print("[warn] cn-stage 的 wire 取法未验证(spec §8 待核实);仅 cn-prod 全链路已打通。", file=sys.stderr)
|
|
711
|
+
|
|
712
|
+
os.makedirs(args.out, exist_ok=True)
|
|
713
|
+
import preflight, chat_driver, pull_wire, prep_conversation
|
|
714
|
+
pf = preflight.summarize_preflight(preflight.probe(args.env))
|
|
715
|
+
if not pf["ok"]:
|
|
716
|
+
print(pf["report"]); print("\n[preflight 未通过,先按提示准备环境]"); sys.exit(2)
|
|
717
|
+
|
|
718
|
+
started_ts = _utc_now()
|
|
719
|
+
answers = _parse_answers(args.answer)
|
|
720
|
+
d = chat_driver.ChatDriver().attach()
|
|
721
|
+
d.new_conversation()
|
|
722
|
+
turns = []
|
|
723
|
+
for msg in args.message:
|
|
724
|
+
r = d.send_and_wait(msg, answers=answers)
|
|
725
|
+
turns.append({"sent": msg, "state": r.get("state"), "transcript": r.get("transcript", ""),
|
|
726
|
+
"tool_trace": r.get("tool_trace"), "timed_out": r.get("timed_out", False)})
|
|
727
|
+
d.close()
|
|
728
|
+
|
|
729
|
+
wire_root = pull_wire.pull(args.user, since_days=1, out=args.out)
|
|
730
|
+
meta = {"env": args.env, "started_ts": started_ts, "first_message": args.message[0],
|
|
731
|
+
"expect": args.expect, "issue_repo": args.issue_repo, "turns": turns}
|
|
732
|
+
if wire_root:
|
|
733
|
+
index = pull_wire.emit_conversation_index(wire_root)
|
|
734
|
+
hit = pull_wire.locate_conversation(index, started_ts, args.message[0])
|
|
735
|
+
meta["located"] = hit
|
|
736
|
+
if hit:
|
|
737
|
+
sdir = os.path.join(wire_root, hit["sid"])
|
|
738
|
+
deref = pull_wire.make_deref(sdir)
|
|
739
|
+
recs = [json.loads(l) for l in open(os.path.join(sdir, "records.jsonl"), encoding="utf-8") if l.strip()]
|
|
740
|
+
reqs = [x for x in recs if x.get("kind") == "request"]
|
|
741
|
+
resps = {x.get("callId"): x for x in recs if x.get("kind") in ("response", "error")}
|
|
742
|
+
convs = pull_wire.segment(reqs, deref)
|
|
743
|
+
conv = convs[hit["gidx"] - 1] if hit["gidx"] - 1 < len(convs) else convs[-1]
|
|
744
|
+
_, _, _, _, wire_md = pull_wire.render_conversation(conv, resps, deref, hit["gidx"])
|
|
745
|
+
prepped = prep_conversation.merge_browser_evidence(wire_md, turns)
|
|
746
|
+
open(os.path.join(args.out, "prepped.md"), "w", encoding="utf-8").write(prepped)
|
|
747
|
+
open(os.path.join(args.out, "meta.json"), "w", encoding="utf-8").write(json.dumps(meta, ensure_ascii=False, indent=2))
|
|
748
|
+
print(f"[done] 备料稿 → {args.out}/prepped.md 元数据 → {args.out}/meta.json")
|
|
749
|
+
print("下一步:Claude 用 judge_workflow.js 判定,confirmed 提 issue 到", args.issue_repo)
|
|
750
|
+
|
|
751
|
+
if __name__ == "__main__":
|
|
752
|
+
main()
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
756
|
+
|
|
757
|
+
Run: `python3 tests/yzsgo-e2e/test_render_report.py`
|
|
758
|
+
Expected: PASS(2 tests)
|
|
759
|
+
|
|
760
|
+
- [ ] **Step 5: 提交**
|
|
761
|
+
|
|
762
|
+
```bash
|
|
763
|
+
git add .claude/skills/yzsgo-e2e/run_e2e.py tests/yzsgo-e2e/test_render_report.py
|
|
764
|
+
git commit -m "feat(yzsgo-e2e): run_e2e 编排入口 + 报告渲染"
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## Task 7: SKILL.md 正文 + README/service-matrix 对齐 + .codex 镜像
|
|
770
|
+
|
|
771
|
+
**Files:**
|
|
772
|
+
- Modify: `.claude/skills/yzsgo-e2e/SKILL.md`(补正文)
|
|
773
|
+
- Modify: `README.md`(skill 清单加 yzsgo-e2e)
|
|
774
|
+
- Read then maybe Modify: `tests/service-matrix-alignment.test.js`
|
|
775
|
+
- Create: `.codex/skills/yzsgo-e2e/`(镜像整个目录)
|
|
776
|
+
- Test: `node --test tests/service-matrix-alignment.test.js`(及既有 `npm test`)
|
|
777
|
+
|
|
778
|
+
**Interfaces:**
|
|
779
|
+
- Produces: 完整可触发的 skill;双端镜像一致。
|
|
780
|
+
|
|
781
|
+
- [ ] **Step 1: 先读校验测试,确认它校验什么**
|
|
782
|
+
|
|
783
|
+
Run: `sed -n '1,60p' tests/service-matrix-alignment.test.js`
|
|
784
|
+
判断它是否校验「README 清单 ⇄ `.claude/skills/` 目录 ⇄ `.codex/skills/`」一致。这决定 Step 3 要同步哪些清单。若它是失败测试(新增 skill 未登记会红),本步即「确认失败」。
|
|
785
|
+
|
|
786
|
+
- [ ] **Step 2: 写 SKILL.md 正文**
|
|
787
|
+
|
|
788
|
+
在 frontmatter 下补正文,含这些必备节(照 spec §4–§9):
|
|
789
|
+
|
|
790
|
+
```markdown
|
|
791
|
+
# yzsgo-e2e — 鸭嘴兽通用端到端测试
|
|
792
|
+
|
|
793
|
+
用浏览器真机驱动鸭嘴兽完成一段对话,同时拿浏览器侧结果 + 拉 gateway wire,判前后端一致性与网关缺陷,confirmed 自动提 issue。
|
|
794
|
+
|
|
795
|
+
## 何时用
|
|
796
|
+
- 「端到端测一下鸭嘴兽」「e2e 测这段对话」「用浏览器真机测整个流程有没有问题」
|
|
797
|
+
|
|
798
|
+
## 用户说人话 → 我怎么映射(用户不敲 CLI)
|
|
799
|
+
- 用户给的对话内容 → 逐轮 `--message`(按序)。
|
|
800
|
+
- 用户提到「遇到它问 X 就答 Y / 测某个反问」→ `--answer "X=Y"`。
|
|
801
|
+
- 用户说「重点看有没有 Z」→ `--expect "Z"`。
|
|
802
|
+
|
|
803
|
+
## 四段流程
|
|
804
|
+
1. **preflight**:`python3 preflight.py <env>`;缺项按提示准备(一次性登录/充值见 store-skills `setting-up-yzsgo-test-env`,我不替做)。
|
|
805
|
+
2. **驱动 + 拉 wire + 备料**:`python3 run_e2e.py --env <env> --user <测试账号userId> --message ... [--answer k=v] [--expect ...] --out <dir>` → 出 `prepped.md` + `meta.json`。
|
|
806
|
+
3. **判定**:`gh issue list --repo Optima-Chat/optima-gateway --state open --json number,title` 拉已知 issue 文本,`Workflow({scriptPath:"<skill>/judge_workflow.js", args:{base:"<dir>", sids:["prepped"], knownIssues:"<文本>"}})`。
|
|
807
|
+
4. **报告 + 提 issue**:出报告(`render_report`)。**confirmed 直接 `gh issue create --repo Optima-Chat/optima-gateway --label needs-triage`**(证据=前端输出+wire transcript+repro 对话),提错可改可删;**needs_review 不自动提**,报告单列待人工;**环境类标 blocked 不提**。
|
|
808
|
+
|
|
809
|
+
## 诚实边界(照搬 conversation-iq)
|
|
810
|
+
- 「0 confirmed」≠「没问题」;needs_review 绝不静默毙。
|
|
811
|
+
- 对抗验证必须让 skeptic 读**真实 runtime 源码**,默认判假。
|
|
812
|
+
- 报告固定含「覆盖边界」+「判断修正」两节。
|
|
813
|
+
|
|
814
|
+
## 前后端对照(本 skill 独有)
|
|
815
|
+
判定要核对**前端渲染的** vs **wire 里 agent 真实产出的**:前端丢内容/半截/报错但 wire 成功(或反之)= 缺陷。
|
|
816
|
+
|
|
817
|
+
## vendor 同步纪律
|
|
818
|
+
`chat_driver.py`/`pull_wire.py` 上游权威 = optima-store-skills;`prep_conversation.py`/`judge_workflow.js` 改编自 optima-gateway conversation-iq。见 `SYNC.md`。驱动异常先怀疑前端 DOM 漂移 → 去上游同步。可跑 `python3 verify_drift.py` 比对。
|
|
819
|
+
|
|
820
|
+
## 两环境
|
|
821
|
+
`--env cn-prod`(已打通)/ `cn-stage`(wire 取法待核实,脚本会告警)。
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
- [ ] **Step 3: README 加清单 + 镜像 .codex**
|
|
825
|
+
|
|
826
|
+
在 `README.md` 的 skill 列表(`## 📋 任务场景` 段)按字母序插入一行:
|
|
827
|
+
|
|
828
|
+
```markdown
|
|
829
|
+
- **yzsgo-e2e** - 鸭嘴兽通用端到端测试:浏览器真机驱对话 + 拉 wire + 语义判定 + 提 issue(cn-prod;cn-stage 待核实)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
镜像目录:
|
|
833
|
+
|
|
834
|
+
```bash
|
|
835
|
+
rm -rf .codex/skills/yzsgo-e2e
|
|
836
|
+
cp -R .claude/skills/yzsgo-e2e .codex/skills/yzsgo-e2e
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
若 Step 1 发现 `service-matrix-alignment.test.js` 另有清单来源(如某个 matrix 常量/文件),按其规则补登 `yzsgo-e2e`。
|
|
840
|
+
|
|
841
|
+
- [ ] **Step 4: 跑校验测试通过**
|
|
842
|
+
|
|
843
|
+
Run: `npm test`(含 `node --test tests/*.test.js`,覆盖 service-matrix 与 outcome 测试)
|
|
844
|
+
Expected: PASS(service-matrix 不再因缺 yzsgo-e2e 而红;outcome 测试通过)
|
|
845
|
+
|
|
846
|
+
- [ ] **Step 5: 提交**
|
|
847
|
+
|
|
848
|
+
```bash
|
|
849
|
+
git add .claude/skills/yzsgo-e2e/SKILL.md README.md .codex/skills/yzsgo-e2e tests/service-matrix-alignment.test.js
|
|
850
|
+
git commit -m "feat(yzsgo-e2e): SKILL.md 正文 + README/service-matrix 对齐 + .codex 镜像"
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## Task 8: verify_drift.py 漂移比对 + 真机 smoke 文档
|
|
856
|
+
|
|
857
|
+
**Files:**
|
|
858
|
+
- Create: `.claude/skills/yzsgo-e2e/verify_drift.py`
|
|
859
|
+
- Modify: `.claude/skills/yzsgo-e2e/SKILL.md`(补 smoke 步骤)
|
|
860
|
+
- Modify: `.codex/skills/yzsgo-e2e/`(同步)
|
|
861
|
+
- Test: `tests/yzsgo-e2e/test_verify_drift.py`
|
|
862
|
+
|
|
863
|
+
**Interfaces:**
|
|
864
|
+
- Produces: `plan_drift_checks(present: dict) -> list[dict]`。`present` = `{"store-skills": bool, "gateway": bool}`。返回每个 vendored 文件一项 `{"file","upstream","checkable": bool}`:上游 repo 在场→`checkable=True`,不在场→`checkable=False`(跳过,不报错)。
|
|
865
|
+
|
|
866
|
+
- [ ] **Step 1: 写失败测试**
|
|
867
|
+
|
|
868
|
+
```python
|
|
869
|
+
# tests/yzsgo-e2e/test_verify_drift.py
|
|
870
|
+
import os, sys, unittest
|
|
871
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".claude", "skills", "yzsgo-e2e"))
|
|
872
|
+
from verify_drift import plan_drift_checks
|
|
873
|
+
|
|
874
|
+
class TestDrift(unittest.TestCase):
|
|
875
|
+
def test_absent_repo_skipped(self):
|
|
876
|
+
checks = plan_drift_checks({"store-skills": False, "gateway": False})
|
|
877
|
+
self.assertTrue(all(not c["checkable"] for c in checks))
|
|
878
|
+
|
|
879
|
+
def test_present_repo_checkable(self):
|
|
880
|
+
checks = plan_drift_checks({"store-skills": True, "gateway": True})
|
|
881
|
+
files = {c["file"] for c in checks if c["checkable"]}
|
|
882
|
+
self.assertIn("chat_driver.py", files) # 来自 store-skills
|
|
883
|
+
self.assertIn("judge_workflow.js", files) # 来自 gateway
|
|
884
|
+
|
|
885
|
+
if __name__ == "__main__":
|
|
886
|
+
unittest.main()
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
- [ ] **Step 2: 跑测试确认失败**
|
|
890
|
+
|
|
891
|
+
Run: `python3 tests/yzsgo-e2e/test_verify_drift.py`
|
|
892
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'verify_drift'`
|
|
893
|
+
|
|
894
|
+
- [ ] **Step 3: 写 verify_drift.py**
|
|
895
|
+
|
|
896
|
+
```python
|
|
897
|
+
#!/usr/bin/env python3
|
|
898
|
+
"""比对 vendored 文件与上游是否漂移。上游 repo 不在本机则跳过(不破坏自包含)。"""
|
|
899
|
+
import os, subprocess, sys
|
|
900
|
+
|
|
901
|
+
VENDORED = [
|
|
902
|
+
{"file": "chat_driver.py", "repo": "store-skills",
|
|
903
|
+
"upstream": "~/optima-store-skills/.claude/skills/operating-yzsgo-chat/chat_driver.py"},
|
|
904
|
+
{"file": "pull_wire.py", "repo": "store-skills",
|
|
905
|
+
"upstream": "~/optima-store-skills/.claude/skills/pulling-yzsgo-session-wire/pull_wire.py"},
|
|
906
|
+
{"file": "prep_conversation.py", "repo": "gateway",
|
|
907
|
+
"upstream": "~/optima-gateway/.claude/skills/conversation-iq/prep_session.py"},
|
|
908
|
+
{"file": "judge_workflow.js", "repo": "gateway",
|
|
909
|
+
"upstream": "~/optima-gateway/.claude/skills/conversation-iq/workflow.js"},
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
def plan_drift_checks(present: dict) -> list:
|
|
913
|
+
out = []
|
|
914
|
+
for v in VENDORED:
|
|
915
|
+
out.append({"file": v["file"], "upstream": v["upstream"],
|
|
916
|
+
"checkable": bool(present.get(v["repo"], False))})
|
|
917
|
+
return out
|
|
918
|
+
|
|
919
|
+
def _present() -> dict:
|
|
920
|
+
return {"store-skills": os.path.isdir(os.path.expanduser("~/optima-store-skills")),
|
|
921
|
+
"gateway": os.path.isdir(os.path.expanduser("~/optima-gateway"))}
|
|
922
|
+
|
|
923
|
+
if __name__ == "__main__":
|
|
924
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
925
|
+
for c in plan_drift_checks(_present()):
|
|
926
|
+
if not c["checkable"]:
|
|
927
|
+
print(f"⏭ {c['file']}:上游不在本机,跳过"); continue
|
|
928
|
+
up = os.path.expanduser(c["upstream"])
|
|
929
|
+
r = subprocess.run(["diff", "-q", os.path.join(here, c["file"]), up], capture_output=True, text=True)
|
|
930
|
+
print(("✅ " if r.returncode == 0 else "⚠️ 漂移 ") + f"{c['file']} vs {c['upstream']}")
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
> 注:`prep_conversation.py`/`judge_workflow.js` 是**改编**(非全等),`diff` 必然有差异——`⚠️` 对它们是「提醒去看上游有没有新变更」而非「必须一致」。SKILL.md 里说明这一点。
|
|
934
|
+
|
|
935
|
+
- [ ] **Step 4: 跑测试确认通过**
|
|
936
|
+
|
|
937
|
+
Run: `python3 tests/yzsgo-e2e/test_verify_drift.py`
|
|
938
|
+
Expected: PASS(2 tests)
|
|
939
|
+
|
|
940
|
+
- [ ] **Step 5: 补 SKILL.md smoke 步骤,同步 .codex,提交**
|
|
941
|
+
|
|
942
|
+
在 SKILL.md 末尾加:
|
|
943
|
+
|
|
944
|
+
```markdown
|
|
945
|
+
## 真机 smoke(改完先跑这个验管道通)
|
|
946
|
+
python3 run_e2e.py --env cn-prod --user <测试账号userId> --message "你好" --out /tmp/yzsgo-smoke
|
|
947
|
+
# 期望:/tmp/yzsgo-smoke/prepped.md 含「前端所见」节 + wire transcript;meta.json 的 located 命中本次对话。
|
|
948
|
+
# 只验四段接线通,不追判定质量。
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
同步镜像并提交:
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
cp -R .claude/skills/yzsgo-e2e/. .codex/skills/yzsgo-e2e/
|
|
955
|
+
git add .claude/skills/yzsgo-e2e/verify_drift.py .claude/skills/yzsgo-e2e/SKILL.md .codex/skills/yzsgo-e2e tests/yzsgo-e2e/test_verify_drift.py
|
|
956
|
+
git commit -m "feat(yzsgo-e2e): 漂移比对 + 真机 smoke 文档"
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## Self-Review(作者自查,已随计划完成)
|
|
962
|
+
|
|
963
|
+
**Spec 覆盖:** §3 目录结构→Task1-8 全建;§4 输入映射→Task6 argparse + Task7 SKILL.md;§5 四段→Task2/3/4/6(驱动/拉wire/定位/备料)+Task5(判定);§6 前后端对照→Task4 merge + Task5 judgePrompt;§7 报告/提issue→Task6 render_report + Task7 SKILL.md 规则;§8 两环境→Task6 `--env` + cn-stage 告警;§9 vendor 纪律→Task2 SYNC + Task8 verify_drift;§10 双端/测试→Task7 镜像+校验、各任务 TDD。
|
|
964
|
+
|
|
965
|
+
**Placeholder 扫描:** 无 TBD/TODO;每个新函数含真实测试+实现;vendored 用 cp 明确动作;`<hash>` 处标明用 `git rev-parse` 取真实值(非 placeholder,是运行期取值)。
|
|
966
|
+
|
|
967
|
+
**类型一致:** `wait_reply` 返回键(state/transcript/tool_trace/timed_out)在 Task6 turns 与 Task4 merge 输入一致;`decideOutcome` 输入 `{refuted}` 与 workflow votes 一致;`locate_conversation` 返回 `{gidx,sid,ts,prompt}` 与 Task6 `hit["sid"]/hit["gidx"]` 用法一致;`render_report` 字段与 test 一致。
|
|
968
|
+
|
|
969
|
+
**开放项(执行期处理,非阻塞):** cn-stage wire 取法(Task6 已告警占位);前端是否暴露 sessionId(暴露则可替代时间戳定位,Task3 可增强);chat_driver 是否裁 marketplace 方法(Task2 注为后续可选)。
|