Scenario: Send a chat message and verify response
Steps
- Type "explain the code" in the chat input
- Press Enter to submit
- Verify there is a chat response
这 3 个步骤就是整个场景的"骨架"。值得强调的是:这些步骤不是可直接执行的脚本,而是**给 Copilot CLI 理解的自然语言描述**——真正的可执行脚本由生成阶段(Phase 1: Generate)编译产生,并固化在 `scenarios/generated/01-chat-response.commands.json` 中,与 `.scenario.md` 一起提交进 git(见 [e2e/README.md](https://gitcode.com/GitHub_Trending/vscode6/vscode/blob/2e12155dfa05dfe3cd24e6624cb1489ac8a62907/src/vs/sessions/test/e2e/README.md?utm_source=gitcode_repo_files))。
## 三步走:从自然语言步骤到确定性指令
要把自然语言步骤变成测试,需要理解该体系的两阶段设计(详见 [README.md](https://gitcode.com/GitHub_Trending/vscode6/vscode/blob/2e12155dfa05dfe3cd24e6624cb1489ac8a62907/src/vs/sessions/test/e2e/README.md?utm_source=gitcode_repo_files) 的 "How It Works"):
1. **Phase 1: Generate(使用 LLM,运行一次,较慢)** —— 运行 `npm run generate`,脚本为每个 `.scenario.md` 启动 Sessions Web 服务器并在 `playwright-cli` 中打开页面;随后抓取当前页面的可访问性树(accessibility tree)快照,把"自然语言步骤 + 页面快照"一起发给 Copilot CLI,由它返回精确的 `playwright-cli` 命令(例如 `click e43`、`type "hello"`),再执行这些命令推进 UI 状态,进入下一步骤的循环;最后把编译出的命令写入 `scenarios/generated/` 下的同名 `.commands.json`。
2. **Phase 2: Test(无 LLM,快速、确定性)** —— 运行 `npm test`,运行器读取每个 `.commands.json` 并按顺序机械回放 `playwright-cli` 命令,不加任何 LLM 调用、正则匹配或图标剥离,只做顺序命令与断言。
`01-chat-response` 编译后生成的 `.commands.json` 真实内容如下(见 [01-chat-response.commands.json](https://gitcode.com/GitHub_Trending/vscode6/vscode/blob/2e12155dfa05dfe3cd24e6624cb1489ac8a62907/src/vs/sessions/test/e2e/scenarios/generated/01-chat-response.commands.json?utm_source=gitcode_repo_files)):
```json
{
"scenario": "Scenario: Send a chat message and verify response",
"generatedAt": "2026-03-06T04:23:25.470Z",
"steps": [
{
"description": "Type \"explain the code\" in the chat input",
"commands": [
"click textbox \"Chat input\"",
"type \"explain the code\""
]
},
{
"description": "Press Enter to submit",
"commands": [
"click textbox \"Chat input\"",
"press Enter"
]
},
{
"description": "Verify there is a chat response",
"commands": [
"# ASSERT_VISIBLE: This project has a simple structure with a main entry point and utility functions."
]
}
]
}
把 .scenario.md 与这份编译产物逐行对照,可以观察到两个关键事实:
- 第 1、2 个自然语言步骤被展开为具体的 UI 定位命令。注意编译产物中保留了语义化的角色选择器
textbox "Chat input"(而非click e43这类硬编码的 accessibility ref),其原因是 test 阶段运行时可以通过实时快照把语义选择器解析成当前的 ref(详见 test.cjs 的resolveSemanticCommand),从而避免 UI 渲染顺序变化导致 ref 失配。 - 第 3 步"验证存在聊天响应"被编译成了一条注释形式的断言,其中内嵌的断言文本与 Mock 代理针对 "explain the code" 这一关键词返回的固定文案完全一致(见下文源码佐证)。断言以
#注释承载,说明它并不是要下发给playwright-cli执行的命令,而是测试运行器需要特殊解析的信号。
description 字段同时被复用为运行日志中步骤的显示名称,即 README 示例输出中的 step 3: Verify there is a chat response。
断言背后的真相:ASSERT_VISIBLE 如何证明"有响应"
场景第 3 步要回答的问题是:提交后聊天区域真的出现了一条模型响应。这一验证既不通过 DOM 选择器,也不通过截图像素比对,而是回归到了 可访问性树快照的文本包含判断。
从 test.cjs 的实现可以看到,当运行器遇到 # ASSERT_VISIBLE: 前缀的命令时,会进入一个带重试的轮询断言逻辑:
if (cmd.startsWith('# ASSERT_VISIBLE:')) {
const text = cmd.slice('# ASSERT_VISIBLE:'.length).trim();
return pollAssertion(() => {
const snap = getSnapshot();
if (!snap.stdout) { return { ok: false, message: 'Failed to get snapshot for assertion' }; }
if (!snap.stdout.toLowerCase().includes(text.toLowerCase())) {
return { ok: false, message: `Expected "${text}" to be visible in snapshot` };
}
return { ok: true };
});
}
其中 pollAssertion(test.cjs)以 ASSERT_TIMEOUT_MS = 10_000 毫秒为总超时、ASSERT_POLL_MS = 500 毫秒为轮询间隔,反复调用 getSnapshot() 抓取最新可访问性树,直到快照中出现目标文本——也就是说,断言天然容忍了"模型响应需要一定时间渲染"的异步性,只要 10 秒内文本出现在可访问性树中即判定通过。整段匹配为大小写不敏感的子串包含判断,不要求精确行匹配。
除 ASSERT_VISIBLE 之外,运行器还支持 ASSERT_DISABLED 与 ASSERT_ENABLED 两种断言(分别检查按钮是否带 [disabled] 属性,见 test.cjs)。断言文本会被 normalizeLabel()(test.cjs)剔除 Codicon 私用区字符([\uE000-\uF8FF])并转为小写再比较,因此在按钮类断言的快照比对中不受图标字符干扰。
模拟响应从何而来:Mock 代理的关键词匹配实现
"explain the code" 为什么会得到一句关于项目结构的固定文案?答案在测试工作台的 Mock 聊天代理实现里。src/vs/sessions/test/web.test.ts 中的 getMockResponseWithEdits()(web.test.ts)按用户消息的关键词返回预置响应:
if (/build|compile|create/i.test(message)) {
return {
text: 'I\'ll help you build the project. Here are the changes:',
fileEdits: [ /* index.ts、build.ts、package.json 的编辑 */ ],
};
}
if (/fix|bug/i.test(message)) {
/* 返回包含 utils.ts 输入校验编辑的响应 */
}
if (/explain|describe/i.test(message)) {
return {
text: 'This project has a simple structure with a main entry point and utility functions.',
};
}
return {
text: 'I understand your request. Let me work on that.\n\n1. Review the codebase\n2. Make changes\n3. Run tests',
};
对照可见:01-chat-response 中输入的消息 "explain the code" 命中了第三条分支(正则 /explain|describe/i),返回的正是断言文本 This project has a simple structure with a main entry point and utility functions.。该场景刻意选用"纯文本、无文件编辑"的响应分支,把关注点收敛在聊天响应能否出现并渲染上;而需要连带验证文件变更时,则应输入 "build the project"(命中 /build/ 分支,携带 textEdit 编辑,见 web.test.ts)或 "fix the bug"(命中 /fix/ 分支)——这正是 02、03、05 等场景使用这些关键词的原因。
从源码结构看,Mock 是这套测试中唯一注入 canned data 的入口:Mock 代理负责响应 LLM 的"上游"职责,而其下游——包括 ChatModel 通过 acceptResponseProgress() 路由代理进度、ChatEditingService 消费 textEdit、ChangesViewPane 渲染变更树、diff 编辑器打开真实差异——全部走真实服务实现。因此,01-chat-response 虽然断言的是预置文案,但它验证的是真实工作台下聊天提交、代理调用与响应渲染的完整真实链路。
场景运行的全貌:Mock 面与真实面的分工
为了在 CI 中无外部依赖地跑通上述链路,README 中明确列出了"最小 Mock 集"与"其余全部真实"的边界:
| 服务 | Mock 方式 | 原因 |
|---|---|---|
IChatEntitlementService |
返回 ChatEntitlement.Free |
CI 中不存在真实 Copilot 账号 |
IDefaultAccountService |
返回一个假签入账号 | 隐藏 "Sign In" 按钮 |
IGitService |
立即 resolve | Web 测试中无真实 git 扩展 |
聊天代理(copilotcli 等) |
关键词匹配的固定响应 | 无真实 LLM 后端 |
mock-fs:// FileSystemProvider |
InMemoryFileSystemProvider |
必须在任何服务解析工作区文件前可用 |
| GitHub 认证 | 总是签入的 mock provider | 无真实 OAuth 流程 |
| PR 命令(Create/Open/Merge) | 仅打日志并显示 info 消息 | 无真实 GitHub API |
而 ChatEditingService、ChatModel、ChangesViewPane、diff 编辑器、上下文键与菜单动作等均运行真实实现(见 README.md 的 "What's Mocked" / "What's Real" 两节)。
README 用一张数据流图概括了消息从输入到渲染的传播路径,其面向本场景的简化形态为:
User types message → Chat Widget → ChatService
→ Mock Agent invoke() 命中关键词分支 → 返回固定响应文本
→ ChatModel 路由进度 → 聊天视图渲染 markdown 响应
→ (可访问性树快照包含该文本)
→ test.cjs 的 ASSERT_VISIBLE 轮询断言通过
在本地复现与调试本场景
运行前提(详见 README.md 的 Prerequisites / Running 两节):
# 1. 先编译 VS Code(仓库根目录)
npm install && npm run compile
# 2. 安装 E2E 依赖
cd src/vs/sessions/test/e2e && npm install
# 3. UI 变化或首次运行前,重新编译场景(需要 Copilot CLI,慢)
npm run generate
# 4. 运行确定性测试(无需 LLM)
npm test
只看这一个场景可以传入文件名过滤。运行器 discoverCommandFiles(filter)(test.cjs)会对文件名做子串匹配,因此也可以直接调用底层脚本:
node test.cjs 01-chat-response
运行器每次会:在随机端口(9100 + Math.floor(Math.random() * 900))启动带 mock 的 Sessions Web 服务器(test.cjs),用 playwright-cli 打开浏览器并访问 http://localhost:<port>/?skip-sessions-welcome,等待工作台渲染后逐个回放场景。每两个场景之间会按下 Escape 并重新 goto 页面以重置状态;单个步骤执行前会 sleep 1 秒让 UI 稳定(test.cjs)。若某一步失败,运行器会通过 screenshot --filename=out/failure-01-chat-response-stepN.png 留下失败现场截图(test.cjs),并最终以 Results: N passed, M failed 汇总并以非零码退出。
测试输出中的关键节点大致为:
Found N compiled scenario(s)
Starting sessions web server on port 9542…
Server ready.
▶ Scenario: Send a chat message and verify response
✅ step 1: Type "explain the code" in the chat input
✅ step 2: Press Enter to submit
✅ step 3: Verify there is a chat response
Results: N passed, 0 failed
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00