首页
/ VS Code Agent Sessions E2E 场景解析:验证 Chat 消息在 Changes 视图中生成真实文件 Diff

VS Code Agent Sessions E2E 场景解析:验证 Chat 消息在 Changes 视图中生成真实文件 Diff

2026-09-07 09:11:40作者:柏廷章Berta

本文围绕 02-chat-with-changes.scenario.md 这一 Agent Sessions 端到端(E2E)测试场景,讲解它如何验证"用户在 Chat 输入框提交一条消息后,Changes 视图真实呈现被修改文件、点击文件能打开真实 Diff 编辑器"的完整链路。读者可以据此理解 VS Code 仓库中会话类 E2E 测试的编写方式、compile-and-replay 架构以及"只 mock 外部后端、其余全部走真实实现"的测试哲学,并学会如何运行、重新生成与扩展这一场景。

场景定位:Agent Sessions 的六条自动化场景之一

src/vs/sessions/test/e2e/scenarios/ 目录下,共维护了六条人类可读的场景规格(*.scenario.md),它们按数字前缀排序并顺序执行:

  • 01-chat-response.scenario.md
  • 02-chat-with-changes.scenario.md(本文主角)
  • 03-session-in-sidebar.scenario.md
  • 04-navigate-sessions.scenario.md
  • 05-full-workflow.scenario.md
  • 以及每个场景编译后生成的 generated/*.commands.json 文件(如 02-chat-with-changes.commands.json

这套测试采用 compile-and-replay(编译并回放) 架构:人类编写自然语言步骤,交给 Copilot CLI 编译成确定性的 playwright-cli 命令序列,回放阶段不再依赖任何 LLM。场景 02 聚焦的正是这套体系中最关键的能力验证——Chat 产出真实文件编辑、Changes 视图如实反映、Diff 编辑器真实打开

被测行为:七个步骤验证一条完整用户路径

02-chat-with-changes.scenario.md 用 7 步串起"提问 → 得到含文件修改的响应 → 在 Changes 列表查看 → 打开 Diff → 关闭"的完整用户路径:

  1. 在 Chat 输入框中输入 build the project
  2. 按 Enter 提交
  3. 验证 Chat 中出现了响应
  4. 验证 Changes 视图显示了被修改的文件
  5. 点击 Changes 列表中的 index.ts
  6. 验证 Diff 编辑器打开了修改后的内容
  7. 按 Escape 关闭 Diff 编辑器

从仓库结构看,场景刻意选取了触发 mock Agent 文件编辑能力的关键词 build:它既能驱动真实 ChatEditingService 计算改动,又能在人工回放时稳定复现。

编译产物:自然语言步骤如何落地为可执行命令

npm run generate 会把这些自然语言步骤交给 Copilot CLI 翻译为语义化命令并写入 .commands.json。编译产物 02-chat-with-changes.commands.json 中每一步对应的命令为:

步骤 编译后的命令 说明
输入文本 click textbox "Chat input"type "build the project" 聚焦输入框并键入关键字
提交 click textbox "Chat input"press Enter 再次聚焦后回车
断言响应 # ASSERT_VISIBLE: I'll help you build the project. Here are the changes: 检查快照文本
断言文件列表 # ASSERT_VISIBLE: index.ts / build.ts / package.json 三个文件逐项断言
打开 Diff click treeitem "index.ts" 在树中点击文件
断言 Diff 内容 # ASSERT_VISIBLE: import { build } from 内容片段断言
关闭 Diff press Escape 恢复 UI 状态

值得注意,编译产物头部附有注释 Uses semantic selectors — regenerate with 'npm run generate' if UI labels change。也就是说命令里保存的是 角色 + 标签(如 textbox "Chat input"treeitem "index.ts"),而不是易失效的引用编号,UI 文案变更后可重新生成。

为什么能产生"真实"Diff:最小化 Mock 架构

普通 E2E 要么依赖真实后端导致不稳定,要么把整条链路全部 mock 掉导致测了个寂寞。这套测试的独特之处在于:运行的是真实 Sessions workbench,只 mock 需要外部后端的最小集合。场景 02 之所以敢断言"真实文件 Diff",正是依赖这一架构设计(详见 src/vs/sessions/test/e2e/README.md)。

被替换为 mock 的只有四类需要外部依赖的服务:

服务 Mock 行为 原因
IChatEntitlementService 恒定返回 ChatEntitlement.Free CI 中没有真实 Copilot 账号
IDefaultAccountService 返回伪造的已登录账号 隐藏"Sign In"按钮
IGitService 立即 resolve Web 测试中无真实 git 扩展,避免 10 秒屏障
Chat agents(copilotcli 等) 基于关键字匹配的罐头响应 + textEdit 进度项 没有真实 LLM 后端
mock-fs:// FileSystemProvider 直接在 workbench 注册 InMemoryFileSystemProvider 必须在任何服务解析工作区文件前可用
GitHub 认证 / PR 命令 恒登录态 / no-op 处理器 无真实 OAuth 与 GitHub API

而下游链路全部走真实实现:ChatEditingServiceChatModelChangesViewPane、Diff 编辑器、上下文键(hasUndecidedChatEditingResourceContextKeyhasAppliedChatEditsContextKey)以及菜单动作(Create PR / Accept / Reject)等。mock Agent 是罐头数据进入系统的唯一入口,其下游全是真实代码路径。

预置内存文件系统:Diff 的"原始内容"来源

要让 ChatEditingService 算出真正的 before/after diff,mock 文件系统里必须预置与编辑目标一致的原文件。在 web.test.tsMOCK_FS_FILES 预置了 4 个文件:

const MOCK_FS_FILES: Record<string, string> = {
    '/mock-repo/src/index.ts': 'export function main() {\n\tconsole.log("Hello from mock repo");\n}\n',
    '/mock-repo/src/utils.ts': 'export function add(a: number, b: number): number {\n\treturn a + b;\n}\n',
    '/mock-repo/package.json': '{\n\t"name": "mock-repo",\n\t"version": "1.0.0"\n}\n',
    '/mock-repo/README.md': '# Mock Repository\n\nThis is a mock repository for E2E testing.\n',
};

registerMockFileSystemProvider()web.test.ts)直接把这些文件写入 mock-fs://mock-repo 这个 authority 下。为什么必须在 workbench 层注册而非 mock 扩展内?因为 SnippetsService、Agentic Prompt 文件定位器、MCP 等服务会在扩展宿主激活之前就去解析工作区内的文件,此时若 provider 尚未注册,会出现 ENOPRO: No file system provider 错误而静默失败。mock 扩展(extension.js)仍会通过扩展 API 注册一个 mock-fs provider 以满足扩展宿主操作,但 workbench 层的注册才是"权威来源"。

关键字驱动的罐头响应:场景数据的注入点

场景 02 中用户输入 build the project,会被 mock Agent 的关键字正则捕获。在 web.test.tsgetMockResponseWithEdits() 中:

if (/build|compile|create/i.test(message)) {
    return {
        text: 'I\'ll help you build the project. Here are the changes:',
        fileEdits: [
            { uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/index.ts' }),   // 修改已有文件
              content: 'import { build } from "./build";\n\nexport function main() {\n\tconsole.log("Hello from mock repo");\n\tbuild();\n}\n' },
            { uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/build.ts' }),  // 新建文件
              content: 'export async function build() {\n\tconsole.log("Building...");\n\tconsole.log("Build complete!");\n}\n' },
            { uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/package.json' }),   // 修改已有文件
              content: '{\n\t"name": "mock-repo",\n\t"version": "1.0.0",\n\t"scripts": {\n\t\t"build": "node src/build.ts"\n\t}\n}\n' },
        ],
    };
}

回复文本 I'll help you build the project. Here are the changes: 正是编译产物第 3 步断言出现的内容,三个 fileEdits 正好对应第 4 步断言的 index.tsbuild.tspackage.json,而 index.ts 新内容首行 import { build } from "./build"; 则是第 6 步 Diff 内容断言 import { build } from 的来源。场景与数据驱动完全闭环。

mock Agent 通过 IChatAgentService.registerDynamicAgent() 注册 copilotclicopilot-cloud-agent 两个动态 agent,其 invoke() 依次产出 markdownContent 进度项与文件编辑进度项(web.test.ts)。

编辑区间策略:已有文件 vs 新建文件

emitFileEdits()web.test.ts)依据目标路径是否存在于 EXISTING_MOCK_FILES 决定 textEdit 的 range:

  • 已有文件(如 index.tspackage.json):使用整文件替换区间 { startLineNumber: 1, startColumn: 1, endLineNumber: 99999, endColumn: 1 },让 ChatEditingService 把旧内容与新内容做真实 diff;
  • 新建文件(如 build.ts):使用"在开头插入"区间 { startLineNumber: 1, ... endLineNumber: 1 },在 Changes 视图中产生一条"file created"条目。

数据流:从输入框到 Diff 编辑器的调用链

场景 02 断言的一切都由下面这条真实管线驱动(见 README.md):

User types message → Chat Widget → ChatService
  → Mock Agent invoke() → progress([{ kind: 'textEdit', uri, edits }])
    → ChatModel.acceptResponseProgress()
      → ChatEditingService observes textEditGroup parts
        → Creates IModifiedFileEntry per file
        → Reads original content from mock-fs:// FileSystemProvider
        → Computes real diff (linesAdded, linesRemoved)
          → ChangesViewPane renders via observable chain
            → Click file → Opens real diff editor

也就是说,用户按下 Enter 后,键盘输入进入真实 ChatService;mock Agent 的罐头 textEdit 进入真实 ChatModel;再由真实 ChatEditingServicechatEditingServiceImpl.ts)观察进度、为每个文件创建带 before/after 内容的修改条目并计算真实增删行数;随后 Changes 视图(changesView.ts)通过 observable 链订阅 IChatEditingService 的状态渲染带 diff 统计的树;点击文件后打开的是真实 Diff 编辑器。这也是为什么该场景的定位是"真实 diff"而非"模拟展示"。

断言策略:断文件与内容,不断行号

README 明确给出一条重要工程建议:不要断言硬编码的行数(如 +23),而是断言文件名与内容片段。原因在于真实 diff 引擎计算的增删行数会随 mock 文件内容演化而变化,而文件名和关键内容片段才是稳定的接口契约。场景 02 正是这一策略的范本:

  • 对响应只断言引导句的开头 I'll help you build the project. Here are the changes:
  • 对文件树只断言三个文件名;
  • 对 Diff 编辑器只断言新内容的特征行 import { build } from

这种断言在回放期会被 test.cjs 以"轮询快照"的方式执行:# ASSERT_VISIBLE: 断言会以 500ms 间隔轮询、最长等待 10 秒(ASSERT_TIMEOUT_MS / ASSERT_POLL_MS,见 test.cjs),以容忍异步渲染。语义命令(如 click treeitem "index.ts")同样会在执行前先用实时无障碍树快照解析成当前页面的 ref,避免元素尚未渲染导致误失败(test.cjs)。

本地运行与重新生成

前置条件

  • 仓库根目录已完成编译:npm install && npm run compile(产物输出到 out/);
  • 安装 e2e 目录依赖:cd src/vs/sessions/test/e2e && npm install
  • 仅在需要 generate 阶段时安装 Copilot CLI 并确认可用:copilot --version

回放阶段(快且确定性)

cd src/vs/sessions/test/e2e
npm test            # 运行全部已编译场景
npm test -- 02-chat # 或按名称过滤:node test.cjs 02-chat-with-changes

test.cjs 会启动 Sessions web 服务器(随机端口 9100–9999)并以 --headed 打开浏览器,进入 /?skip-sessions-welcome 页面;每个场景之间会先按 Escape 并重新 goto,保证状态隔离;任一断言失败都会把失败现场截图保存为 out/failure-*.png。回放全程不调用任何 LLM,只是顺序执行命令与断言。

编译阶段(慢,含 LLM,改动后仅需一次)

cd src/vs/sessions/test/e2e
npm run generate            # 编译全部场景
npm run generate -- 02-chat # 只重编译匹配的场景

generate.cjs 会为每个 .scenario.md 依次:启动服务器 → 打开页面 → 抓取无障碍树快照 → 把"快照 + 自然语言步骤"交给 Copilot CLI(模型参数为 claude-sonnet-4.6)→ 拿到语义命令(系统提示要求使用 click button "Send"type "..."press Enter# ASSERT_VISIBLE: 等角色+标签形式,禁止使用 e43 这类引用编号)→ 执行命令推进 UI 状态进入下一步。编译产物 .commands.json 是提交到 git 的确定性测试计划。当新增 .scenario.md、UI 变化导致引用过期(测试开始失败)、或修改了既有步骤时,需要重新执行 generate。

编写与扩展这一场景的要点

  • 步骤用纯自然语言撰写,文件按 NN-描述.scenario.md 命名以控制顺序;场景之间靠 Escape 分隔。
  • 一个步骤只做一个动作,便于失败信息定位。
  • 按钮标签要与 UI 实际显示完全一致;标签中可能混入 PUA 区的 codicon 图标字符,应只用可读文本部分匹配。
  • 给 mock Agent 添加新的文件编辑时,同步更新三处:新增/修改 extension.js 中的文件存储、web.test.tsEXISTING_MOCK_FILES(两者路径必须一致),并在 getMockResponseWithEdits() 中注册对应关键字;所有路径须位于 /mock-repo/ 之下。
  • 触发编辑类响应的消息关键字可参考 getMockResponseWithEdits() 已实现的分支:build/compile/create 对应三条文件编辑(改 index.ts、新建 build.ts、改 package.json),fix/bug 对应 utils.ts 的校验修复,explain/describe 则只返回解释文本而无编辑。

综上,02-chat-with-changes.scenario.md 不只是 7 行步骤,它是对 Agent Sessions"聊天产出真实文件修改"这一核心能力的端到端契约测试,与 mock 文件系统、关键字驱动 mock Agent、真实编辑服务管线共同构成了仓库中一套可读、可回放、可扩展的验证体系。

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