首页
/ Visual Studio Code Agent Sessions 全流程 E2E 场景拆解:从聊天改动到变更视图、Diff 编辑器与多会话终端绑定

Visual Studio Code Agent Sessions 全流程 E2E 场景拆解:从聊天改动到变更视图、Diff 编辑器与多会话终端绑定

2026-09-07 14:02:11作者:柯茵沙

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.jsonbuild.tsindex.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.jsonbuild.tsindex.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 e43type "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.tsregisterMockFileSystemProvider)。这是因为 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 状态(hasUndecidedChatEditingResourceContextKeyhasAppliedChatEditsContextKey),而这些 context key 由真实代码观测修改状态后更新——于是“该出现时出现、不该出现时不出现”的判定逻辑本身被真实地测试了。

会话与终端绑定:session-1 / session-2 标签如何切换

05 场景后半段(步骤 12–21)是其他场景没有覆盖的独有亮点:Agent 会话与集成终端的动态绑定

  • 步骤 12–15:点 “Open Terminal” 后,终端面板可见,且其 Tab 标签显示为 bash - session-1。在 mock 环境下,终端后端同样是被替换为桩实现的(见 web.test.tsregisterMockTerminalBackend / createMockTerminalBackendcreateProcess 返回 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 READMEpackage.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.cjs9100 + 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 中的四条约定:

  1. 编号前缀控制顺序:文件名以 NN- 开头并按名字排序执行,05 之所以在最后,是因为它依赖前序场景铺垫的多会话 UI 状态。
  2. 自然语言步骤:动作与断言尽量用 Click button "X"Type "..." in the chat inputPress EnterVerify ... is visible 这类可被 Copilot 理解的口语,但不受限。
  3. 一条动作一个步骤:保持原子性,失败信息才足够清晰。
  4. 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.tsgetMockResponseWithEdits()/EXISTING_MOCK_FILES 与 mock 扩展 extension.jsMOCK_FILES,且所有路径必须位于 /mock-repo/ 之下。

小结

05-full-workflow 是 Agent Sessions E2E 套件中信息量最大的一条链路测试:它以一句 build the project 为起点,一路验证聊天响应、Changes 变更树的多文件改动、真实 Diff 编辑器、Merge/Open Terminal 动作、终端与当前会话的动态绑定以及历史会话往返切换。透过它的手写剧本(.scenario.md)与编译产物(.commands.json),既能看清“Mock 注入点 + 真实服务链路”这一测试架构如何用最小代价换来最大真实度,也能把 Agent Sessions 最核心的产品交互——在对话中完成代码修改并就地审查、合并、运行——完整复现为机器可验证的回归资产。

登录后查看全文
热门项目推荐
相关项目推荐