Visual Studio Code Agent Sessions 全流程 E2E 场景拆解:从聊天改动到变更视图、Diff 编辑器与多会话终端绑定
Agent Sessions 是 Visual Studio Code 中面向“代码代理(Copilot CLI / Cloud Agent)会话”的工作台形态,用户在一个会话内既可以与代理对话、查看它生成的文件改动,也可以一键打开终端、合并变更、创建 PR。本指南围绕 src/vs/sessions/test/e2e/scenarios/05-full-workflow.scenario.md 这一条覆盖度最高的端到端场景,逐条拆解其 21 个验证步骤,并结合该测试套件采用的 “compile-and-replay” 架构与 Mock 服务设计,说明每一步在真实 UI 链路和源码层面对应验证了什么。读完本文,你将掌握 Agent Sessions 中聊天文件编辑、Changes 变更视图、Diff 编辑器、会话与终端联动等核心交互的完整工作流,以及如何用可自然语言编写、可确定性回放的 .scenario 测试来守护这一工作流。
场景概览:05-full-workflow 在验证什么
05-full-workflow.scenario.md(场景文件)是 src/vs/sessions/test/e2e/scenarios/ 目录下五个手写场景中的“压轴”用例,其余四个场景分别覆盖单次聊天响应(01-chat-response)、聊天产生真实文件 Diff(02-chat-with-changes)、侧边栏会话列表(03-session-in-sidebar)与多会话历史切换(04-navigate-sessions)。05 则是把这些能力串成一条完整业务链路:
- 在聊天输入框中下达 “build the project” 指令,代理返回带
textEdit的文件编辑流; - 在 Changes 变更视图中断言
package.json、build.ts、index.ts三个文件的增删改状态; - 点击
index.ts打开真实 Diff 编辑器并关闭; - 在变更视图头部看到 Merge 按钮与 Open Terminal 按钮;
- 打开终端后,验证终端 Tab 标签与当前会话一一对应(
bash - session-1); - 创建新会话并发起 “fix the bug”,验证终端标签切到
session-2; - 切回历史会话,验证终端标签随之恢复为
session-1。
该场景同时约束了两种 copilotcli 会话下的 Mock 响应关键词:build/compile/create 触发多文件编辑响应,fix/bug 触发修复类编辑响应,二者的差异正好支撑“切换会话时终端与变更随之切换”的断言设计。
完整 21 步:逐步说明与预期 UI 状态
原场景把全流程编排为 21 个原子步骤。下表逐条给出动作、预期结果以及它们对应的 UI 状态变化:
| 步骤 | 动作 | 预期结果 / 验证点 |
|---|---|---|
| 1–2 | 在 chat input 输入 build the project 并回车 |
请求被提交到 mock agent |
| 3 | 验证聊天区出现响应 | 应出现 I'll help you build the project... 文字回复 |
| 4 | 验证变更视图显示 “CHANGES” | Changes 树出现且带改动徽标 |
| 5–7 | 验证变更列表中可见 package.json、build.ts、index.ts |
两个既有文件被修改 + 一个新文件被创建 |
| 8–9 | 点击 index.ts,验证 Diff 编辑器打开并显示修改内容 |
打开真实 diff editor |
| 10 | 按 Escape 关闭 Diff 编辑器 | 回到变更视图 / 会话界面 |
| 11 | 验证变更视图头部出现 Merge 按钮 | PR 相关动作按真实 context key 出现 |
| 12–14 | 验证 Open Terminal 按钮可见并点击,终端面板显示 | 终端随会话弹出 |
| 15 | 验证终端 Tab 标签为 session-1 |
会话 1 与终端 1 绑定 |
| 16 | 点击 New Session 创建新会话 | 进入会话 2 编辑态 |
| 17–18 | 输入 fix the bug 并回车 |
触发修复类响应 |
| 19 | 验证终端标签变为 session-2 |
新会话重新绑定终端 |
| 20 | 在会话列表中点击回第一个会话 | 加载 build the project 会话历史 |
| 21 | 验证终端标签恢复为 session-1 |
会话与终端绑定正确切换 |
值得注意的是第 8 步的验证方式——点击 index.ts 后编译产物里的断言是 # ASSERT_VISIBLE: index.ts,即依赖 Diff 编辑器标题中出现的文件名而不是某个具体的行号或 +23 这类统计值。这是该测试套件刻意强调的约束:真实 Diff 引擎计算的行数会随 mock 文件内容演化而改变,断言应落在文件名与内容片段上(详见 README)。
手写场景如何变成可确定性回放的命令文件
05-full-workflow.scenario.md 只是“剧本”,实际执行的是同目录 generated/05-full-workflow.commands.json。这套测试采用 compile-and-replay 两阶段架构(架构说明见 e2e README):
- 生成阶段(
npm run generate,使用 LLM,慢):对每个.scenario.md,启动 Sessions Web 服务器并用playwright-cli打开页面,抓取当前页面的 accessibility tree 快照,再把每条自然语言步骤 + 快照发给 Copilot CLI,由它返回精确的playwright-cli命令(如click e43、type "hello"),执行该命令推进 UI 状态后继续下一步,最后把整份命令写入generated/*.commands.json并提交到 git。 - 回放阶段(
npm test,无 LLM,快速确定):test.cjs机械地逐条重放.commands.json中的命令,不做任何解析与匹配。
把 21 步中的几条还原为生成的命令即可看到两类形态。操作类步骤会被编译成具体的引用型命令,例如输入消息编译为:
{
"description": "Type \"build the project\" in the chat input",
"commands": [
"click textbox \"Chat input\"",
"type \"build the project\""
]
}
而断言类步骤则编译成 snapshot/注释断言,例如:
{
"description": "Verify the terminal tab shows \"session-1\" in its label",
"commands": [
"# ASSERT_VISIBLE: bash - session-1"
]
}
步骤 20 的“切回第一个会话”则编译成 click listitem "build the project"——以会话的提示词文本作为列表项标识(完整文件见 05-full-workflow.commands.json)。
需要说明,test.cjs(测试运行器)在回放时并不是盲执行:它内置了语义命令解析与轮询断言两种机制。断言类命令(# ASSERT_VISIBLE: / # ASSERT_DISABLED: / # ASSERT_ENABLED:)会进入 pollAssertion,以 10 秒超时、500ms 间隔反复抓取 a11y 快照,用大小写不敏感的包含匹配判断目标文本是否可见;命令序列形如 click button "Open Terminal" 的语义命令则先取实时快照,把“角色 + 标签”解析为具体的 [ref=e\d+] 后执行。此外每个断言把文本中的 codicon 私有区字符(\uE000-\uF8FF)剥除后再比较,因此步骤 11 中断言 Merge 按钮可见不会受其 codicon 图标干扰。
Mock 架构:只有注入点用假数据,下游全是真实代码
该场景能够“真实地”跑通整条链路,秘诀在于 README 中所述的最小化 Mock 原则:只有必须依赖外部后端(认证、LLM、git)的服务被替换成假实现,其余全部走真实代码路径。Mock 清单如下:
| 服务/组件 | Mock 方式 | 原因 |
|---|---|---|
IChatEntitlementService |
固定返回 ChatEntitlement.Free |
CI 中没有真实 Copilot 账号 |
IDefaultAccountService |
返回假签名账号 | 隐藏侧边栏 “Sign In” 按钮 |
IGitService |
立即 resolve | Web 测试没有真实 git 扩展,省去 10 秒等待 |
Chat agents(copilotcli 等) |
按关键词返回预置响应并带 textEdit |
没有真实 LLM 后端 |
mock-fs:// 文件系统 |
内存态 InMemoryFileSystemProvider |
工作区文件必须可被读取以计算 Diff |
| GitHub 认证 | 恒为已登录的 mock 提供方 | 无真实 OAuth |
| PR 命令 | no-op 处理器,仅打日志 + 信息消息 | 无真实 GitHub API |
真实运行的关键服务则包括:处理 textEdit 并计算真实增删行统计的 ChatEditingService、路由 agent 进度的 ChatModel、读取改动可观察对象并渲染树的 ChangesViewPane、真实 Diff 编辑器、由 ModifiedFileEntryState 观测驱动的 hasUndecidedChatEditingResourceContextKey 等 context key,以及依据真实 context key 出现的 “Create PR / Accept / Reject” 菜单动作。也就是说,mock agent 是罐装数据进入系统的唯一点,一旦 textEdit 进入 ChatModel,之后每一步都由真实实现接管。
关键数据流:一条消息如何变成三个文件改动
步骤 1–7 在源码层的调用链为(实现在 web.test.ts,即 src/vs/sessions/test/web.test.ts):
用户输入 → Chat Widget → ChatService
→ Mock Agent invoke() → progress([{ kind: 'textEdit', uri, edits }])
→ ChatModel.acceptResponseProgress()
→ ChatEditingService 观察 textEditGroup parts
→ 为每个文件创建 IModifiedFileEntry
→ 从 mock-fs:// FileSystemProvider 读取原文
→ 计算真实 diff(linesAdded / linesRemoved)
→ ChangesViewPane 经 observable 链渲染
→ 点击文件 → 打开真实 diff editor
Mock 编辑策略决定了 Diff 的真实性:对已存在文件(如 /mock-repo/src/index.ts、/mock-repo/package.json),编辑使用“整文件替换”范围(第 1 行至第 99999 行),这样 ChatEditingService 拿旧内容与新内容求真实差异;对新文件(如 /mock-repo/src/build.ts),则用“文件开头插入”范围,在变更视图中表现为一条 “file created” 条目。Web 测试源码(web.test.ts)中 emitFileEdits() 正是依据 EXISTING_MOCK_FILES 集合来二选一。而 mock 响应由 getMockResponseWithEdits() 按关键词路由——这正是步骤 1 输入 build the project 会同时命中三个文件、步骤 17 输入 fix the bug 只修改 utils.ts 一个文件的原因。
mock-fs 为什么必须注册在工作台层
一个容易被忽视的细节:mock-fs:// 的 InMemoryFileSystemProvider 并不是在 mock 扩展里注册的,而是在 TestSessionsBrowserMain.createWorkbench() 内直接注册到 IFileService(见 web.test.ts 中 registerMockFileSystemProvider)。这是因为 SnippetsService、AgenticPromptFilesLocator、MCP 等多个工作台服务会在扩展宿主激活之前就尝试解析工作区文件;若只靠扩展 API 注册,这些服务会遭遇 ENOPRO: No file system provider 而静默失败。工作区文件夹 URI 固定为 mock-fs://mock-repo/mock-repo,取 /mock-repo(而非根 /)是为了让 basename(folderUri) 返回 mock-repo,供文件夹选择器显示。
变更视图与 PR 动作:Merge 按钮从哪来
步骤 11 验证的 Merge 按钮来自 mock 扩展的 package.json menus 贡献(扩展目录见 extensions/sessions-e2e-mock)。这些菜单动作由 chatSessionType == copilotcli context 门控,而 chatSessionType context key 由会话 URI scheme 推导(getChatSessionType()),对 mock 会话固定返回 copilotcli。Create PR / Open PR / Merge 的实际处理器被替换为 no-op 扩展处理器:点击只记录日志并弹出 info 消息,对应链路:
Create PR 按钮被点击 → github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR
→ Mock 扩展记录日志并显示 info 消息
之所以这些按钮能在“无真实 GitHub”的环境下按需出现,是因为它们依赖真实 context key 状态(hasUndecidedChatEditingResourceContextKey、hasAppliedChatEditsContextKey),而这些 context key 由真实代码观测修改状态后更新——于是“该出现时出现、不该出现时不出现”的判定逻辑本身被真实地测试了。
会话与终端绑定:session-1 / session-2 标签如何切换
05 场景后半段(步骤 12–21)是其他场景没有覆盖的独有亮点:Agent 会话与集成终端的动态绑定。
- 步骤 12–15:点 “Open Terminal” 后,终端面板可见,且其 Tab 标签显示为
bash - session-1。在 mock 环境下,终端后端同样是被替换为桩实现的(见 web.test.ts 中registerMockTerminalBackend/createMockTerminalBackend,createProcess返回pid: 1的假进程并报告 cwd),因此标签里的bash来自 mock 的getDefaultSystemShell返回/bin/mock-shell所呈现的会话级默认 shell 命名。 - 步骤 16–19:点击 “New Session” 进入会话 2,输入
fix the bug后断言标签变为bash - session-2。这验证了新会话创建时终端会重新初始化并绑定到新会话。 - 步骤 20–21:在会话列表(sessions list)中点回第一个会话(列表项标签即其首条提示
build the project),聊天区加载会话 1 历史的同时,终端标签恢复为bash - session-1。这一步把会话切换与终端还原绑定关系串成了闭环。
从产物断言文本可以看出,会话与终端的对应关系是以 session-N 序号呈现的(见 05-full-workflow.commands.json 第 15/19/21 步);从源码结构看,该绑定由会话管理服务(session-* 的工作树/工作区元数据,如 MockChatAgentContribution.addSessionItem 中为每个会话分配的 /mock-worktrees/session-N 路径)与终端服务协作维护,即一次会话创建对应一个会话级终端实例,切换会话时终端实例随之切换。
如何在本仓库中运行与扩展该场景
前置条件与运行命令
根据 e2e README 与 package.json,运行前需要:
# 1. 编译 VS Code 本体(仓库根目录,需 out/)
npm install && npm run compile
# 2. 安装 e2e 测试依赖
cd src/vs/sessions/test/e2e && npm install
# 3. 仅在需要“生成”阶段时检查 Copilot CLI
copilot --version
运行方式(两阶段):
cd src/vs/sessions/test/e2e
# 首次或 UI 变更后:将 .scenario.md 编译为 .commands.json(慢,走 LLM)
npm run generate
# 日常回归:确定性回放(快,无 LLM)
npm test
npm test 会先随机选择一个 9100–9999 区间的端口(test.cjs 中 9100 + Math.floor(Math.random() * 900)),启动带 --mock 标志的 Sessions Web 服务器,用 playwright-cli open --headed 打开浏览器,依次回放各场景;每个场景之间按 Escape 并重新 goto 基础 URL 复位状态,最后汇总 Results: N passed, 0 failed。失败时还会在 out/failure-<scenario>-step<N>.png 落一张截图便于排查。
编写新场景的约定
新增场景要遵守 README 中的四条约定:
- 编号前缀控制顺序:文件名以
NN-开头并按名字排序执行,05 之所以在最后,是因为它依赖前序场景铺垫的多会话 UI 状态。 - 自然语言步骤:动作与断言尽量用
Click button "X"、Type "..." in the chat input、Press Enter、Verify ... is visible这类可被 Copilot 理解的口语,但不受限。 - 一条动作一个步骤:保持原子性,失败信息才足够清晰。
- md 与 json 成对提交:手写
.scenario.md后运行npm run generate(可用npm run generate -- 05-full只重编单个场景)再npm test验证,最后把.commands.json一并提交。
针对 05 这类“测试真实文件 Diff”的场景,README 特别提醒:不要断言硬编码的行数统计(如 +23),应断言文件名与内容片段,因为真实 Diff 引擎计算出的行数会随 MOCK_FILES(mock 扩展预置文件)内容的演化而改变。若要让 mock agent 支持新的编辑关键词或新文件,需要同步修改 web.test.ts 的 getMockResponseWithEdits()/EXISTING_MOCK_FILES 与 mock 扩展 extension.js 的 MOCK_FILES,且所有路径必须位于 /mock-repo/ 之下。
小结
05-full-workflow 是 Agent Sessions E2E 套件中信息量最大的一条链路测试:它以一句 build the project 为起点,一路验证聊天响应、Changes 变更树的多文件改动、真实 Diff 编辑器、Merge/Open Terminal 动作、终端与当前会话的动态绑定以及历史会话往返切换。透过它的手写剧本(.scenario.md)与编译产物(.commands.json),既能看清“Mock 注入点 + 真实服务链路”这一测试架构如何用最小代价换来最大真实度,也能把 Agent Sessions 最核心的产品交互——在对话中完成代码修改并就地审查、合并、运行——完整复现为机器可验证的回归资产。
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 StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00