GenKit 端到端测试体系深度解析:构建 JS 与 Go 测试应用的语言一致性验证系统
【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit
本文以 GenKit 仓库根目录下的 tests/README.md 为核心,完整讲解这套端到端(e2e)测试的运行前提、构建打包流程与底层实现:它如何从本地dist目录安装自行构建的 genkit 包,如何用 Puppeteer 驱动开发者 UI,以及如何用同一套 YAML 行为规格同时验证 JavaScript 与 Go 两个测试应用,从而保证跨语言运行时在 HTTP API 行为上的一致性。
一、e2e 测试在仓库中的定位
GenKit 是一个用 JavaScript、Go、Dart 和 Python 实现同一套抽象的框架,因此“不同语言的运行时暴露的 HTTP 接口行为是否一致”是一个必须被持续验证的问题。仓库根目录下的tests/目录承载了这部分职责,其组织结构如下:
- tests/README.md:运行 e2e 测试的操作说明(本文主线);
- tests/src/flow_server_test.ts:flow server 流式接口测试;
- tests/src/reflection_api_test.ts:reflection API 响应一致性测试;
- tests/src/dev_ui_test.ts:基于 Puppeteer 的开发者 UI 浏览器测试;
- tests/src/utils.ts:拉起被测应用、进程清理、健康检查重试与 JSON 差异比对等基础设施;
- tests/flow_server_tests.yaml、tests/specs/reflection_api.yaml、tests/specs/agent.yaml、tests/specs/generate.yaml:声明式测试用例规格;
- tests/test_js_app/:JavaScript 测试应用(
test_js_app); - go/tests/test_app/main.go:Go 测试应用,与 JS 应用实现相同的 flow 与模型。
从 tests/package.json 的依赖列表可以看到这套测试用到的关键工具链:puppeteer与puppeteer-screen-recorder(浏览器自动化与录屏)、json-diff(JSON 深度比对)、yaml(解析测试规格)、terminate(强制终止子进程),以及tsx(直接以node --import tsx运行 TypeScript 测试源码,无需额外编译步骤)。
二、运行前提与三步准备流程
tests/README.md 给出的运行方式可以概括为一条主线:
pnpm i # 在 tests/ 目录下安装依赖 pnpm test # 运行 e2e 测试但由于测试对象是“本地构建出来的 genkit 包”,在跑测试之前还需要完成以下三步准备。
第 1 步:为 Puppeteer 安装 Chrome 开发版
dev_ui_test用 Puppeteer 驱动真实浏览器,需要先一次性安装浏览器开发构建:
npx puppeteer browsers install chrome这一步只做一次即可,后续测试会复用已安装的浏览器。
第 2 步:构建(build)本地包
e2e 测试安装的是仓库根目录dist/文件夹中的 genkit 包。因此若要针对本地最新源码做测试,必须先完成构建:
- 只构建你正在改动的个别包:进入对应包目录后运行
npm run build,或长时间开发时用build:watch; - 从仓库根目录整体重建(较慢但保证干净):
pnpm build对照根目录 package.json 可以看到,pnpm build实际是pnpm build:js && pnpm build:genkit-tools的组合,其中build:js会进入js/工作区执行pnpm i && pnpm build。进一步查看 js/package.json 的build脚本,可知它按core(core/ai/flow 三个核心包)→genkit→ 各plugins/**→ 各testapps/**的顺序串行/并行构建整个 JS 侧包树。
第 3 步:打包(pack)成 tarball
构建完成后,需要把各包打成.tgz供测试应用通过npm i <path>安装:
- 在多数包目录内可以直接运行
pnpm pack; - 或从仓库根目录一次性打包所有包:
pnpm pack:all根目录package.json中pack:all的定义是:清空并重建dist/目录,然后分别执行pack:tools(genkit-tools 侧)与pack:js(js 工作区侧),最后把所有.tgz再压缩成genkit-dist.zip。pack:js内部按 core、ai、genkit、plugins 分批调用各包的pnpm pack --pack-destination,把产物统一落到根目录dist/下。
一键脚本:test:e2e 与 test:e2e-local
README 提到根目录package.json中有脚本可以串起上述步骤。当前仓库中的实际定义是:
"test:e2e-local": "pnpm build && pnpm pack:all && cd tests && pnpm install && pnpm test", "test:e2e": "cd tests && pnpm install && pnpm test"也就是说,test:e2e-local才是完整的“构建 + 打包 + 运行”一条龙,适用于本地源码有改动的场景;而test:e2e假设dist/中的包已经是你要验证的版本,只负责安装依赖并执行测试。在tests/目录内,pnpm test的实际定义为(见 tests/package.json):
"test": "npm-run-all test:flow_server test:reflection_api", "test:dev_ui_test": "node --import tsx src/dev_ui_test.ts", "test:reflection_api": "node --import tsx src/reflection_api_test.ts", "test:flow_server": "node --import tsx src/flow_server_test.ts"可以看出:pnpm test默认串行运行flow_server与reflection_api两组测试;dev_ui_test是独立脚本(依赖 Puppeteer 与录屏,需要单独触发)。所有测试都以node --import tsx src/xxx.ts的方式直接执行 TypeScript 源码。
三、测试基础设施:utils.ts 的进程编排机制
tests/src/utils.ts 是整套 e2e 测试的“舞台调度器”,包含四个核心函数,理解了它们就理解了测试是如何“无环境依赖”地自举运行的。
runTestsForApp:拉起应用、跑测试、杀进程
export async function runTestsForApp( testAppPath: string, appCmd: string, testFn: (devUiUrl: string) => Promise<void> ) { ... const { url, process } = await genkitStart(testAppPath, appCmd); ... await testFn(url); ... // finally 中通过 terminate 强制结束子进程 }它接收三个参数:测试应用目录、应用启动命令(如go run main.go或node lib/index.js)、以及真正执行断言的测试函数。无论测试成功与否,finally分支都会调用terminate强制回收被测进程,避免端口占用影响下一次运行。
setupNodeTestApp:把 dist 包“装进”隔离的临时测试应用
export async function setupNodeTestApp(testAppPath: string): Promise<string> { const testRoot = path.resolve(os.tmpdir(), `./e2e-run-${Date.now()}`); fs.mkdirSync(testRoot, { recursive: true }); fs.cpSync(testAppPath, testRoot, { recursive: true }); const distDir = path.resolve(process.cwd(), '../dist'); execSync(`npm i --save ${distDir}/*.tgz`, { stdio: 'inherit', cwd: testRoot }); execSync(`npm run build`, { stdio: 'inherit', cwd: testRoot }); return testRoot; }这一步精确解释了 README 中“测试会从根目录dist文件夹安装 genkit 包”的含义:它把 tests/test_js_app/ 复制到系统临时目录下的e2e-run-<时间戳>,在其中执行npm i --save <root>/dist/*.tgz——即把当前本地构建的全部 genkit tarball 作为依赖安装进来——再编译测试应用。因此如果dist/是过期产物,e2e 测试验证的也是过期的构建,这正是必须 build + pack 的原因。
genkitStart:安装本地 CLI 并从 stdout 解析开发者 UI 地址
const distDir = path.resolve(process.cwd(), '../dist'); execSync( `npm i --save ${distDir}/genkit-cli-?.?*.?*.tgz ${distDir}/genkit-ai-tools-common-*.tgz`, { stdio: 'inherit', cwd: cliInstallRoot } ); // ... const appProcess = spawn('npm', ['exec', '--prefix', cliInstallRoot, 'genkit', 'start', '--', ...appCmd.split(' ')], { cwd: testRoot }); appProcess.stdin.write('\n'); // 应对 cookie ack 提示 // 监听 stdout,正则匹配 "Genkit Developer UI:" 后的 URL,30 秒超时它同样在临时目录test-cli-<时间戳>中安装本地构建的genkit-cli与genkit-ai-tools-common两个 tarball,然后用npm exec --prefix ... genkit start -- <appCmd>以“本地 CLI 拉起被测应用”的方式启动服务。就绪信号来自解析子进程 stdout 中的Genkit Developer UI: <url>行(正则/Genkit Developer UI:\w*([^ ]*)/),30 秒内未匹配到则报timeout waiting for genkit start to start。这个细节说明 e2e 测试验证的不只是库本身,还包括CLI(genkit start)+ 开发者 UI + 应用三者联动的完整链路。
diffJSON 与 retriable:容忍语言差异的比对与稳健的健康检查
diffJSON包装json-diff的diffString,比对前先经过normalizeForComparison:递归剔除值为false的布尔、空字符串与归一化后为空的嵌套对象。这样“显式写了false”与“字段缺省”两种序列化习惯不会误判为差异;retriable提供指数无关的简单重试(默认maxRetries=3、delayMs=0,可覆盖),用于应用冷启动期间的健康检查轮询。
四、flow server 测试:流式接口的行为规格
被测的 Go 应用与 streamy flow
flow_server_test.ts 目前针对 Go 测试应用运行:
await runTestsForApp('../go/tests/test_app', 'go run main.go', async () => { await testFlowServer(); });对应的 go/tests/test_app/main.go 定义了一个streamy流式 flow,每轮向回调发送一个{count: i}chunk 并返回汇总文案,然后把所有 flow 挂载到 HTTP mux 上、通过server.Start(ctx, "127.0.0.1:3400", mux)监听3400 端口:
genkit.DefineStreamingFlow(g, "streamy", func(ctx context.Context, count int, cb func(context.Context, chunk) error) (string, error) { i := 0 if cb != nil { for ; i < count; i++ { if err := cb(ctx, chunk{i}); err != nil { return "", err } } } return fmt.Sprintf("done %d, streamed: %d times", count, i), nil })YAML 规格与断言逻辑
测试用例声明在 tests/flow_server_tests.yaml:
app: flow_server tests: - path: streamy post: data: 5 response: message: '{"count":{count}}' result: 'done {count}, streamed: {count} times'其中{count}是模板占位符。flow_server_test.ts 的验证流程分三段:
- 健康检查:以
maxRetries: 30, delayMs: 2000的策略轮询POST http://localhost:3400/streamy,直到返回 200(即最长约 60 秒的启动窗口); - 逐 chunk 断言:通过
genkit/beta/client的streamFlow({ url, input: test.post.data })发起流式调用,每收到一个 chunk 就与message模板(代入当前 chunk 序号)做 JSON 字符串精确比对; - 数量与终值断言:chunk 总数必须等于
post.data(本例为 5),最终response.output必须等于模板代入 5 后的done 5, streamed: 5 times。
这里体现了 e2e 测试的一个设计特点:客户端用的是 JS 侧genkit包的streamFlow,服务端是 Go 实现的 flow server——一次请求同时验证了“JS 客户端协议实现 + Go 服务端流式传输”两端。
五、reflection API 测试:同一规格跑两种语言实现
双应用运行
reflection_api_test.ts 是最能体现“跨语言一致性”的测试,它对同一个YAML 规格先后运行两个被测应用:
const testAppRoot = await setupNodeTestApp('test_js_app'); // JS 应用(从 dist 装包) await runTestsForApp(testAppRoot, 'node lib/index.js', async () => { await testReflectionApi(); }); await runTestsForApp('../go/tests/test_app', 'go run main.go', async () => { await testReflectionApi(); });两个应用都监听3100 端口(genkit start默认开发者 UI/reflection 端口),健康检查轮询GET http://localhost:3100/api/__health(maxRetries: 30, delayMs: 1000)。
规格文件与排除字段的深意
规格定义在 tests/specs/reflection_api.yaml:
app: test_app tests: - path: /api/runAction post: key: /model/customReflector input: messages: [{ role: user, content: [{ text: hello }] }] body: result: finishReason: stop message: role: model content: - text: '{"messages":[{"content":[{"text":"hello"}],"role":"user"}]}' usage: inputCharacters: 5 outputCharacters: 59JS 测试应用 tests/test_js_app/src/index.ts 中定义了一个名为customReflector的“回声”可编程模型:它把收到的ModelRequest直接JSON.stringify后作为模型输出返回;Go 侧 go/tests/test_app/main.go 的echo函数做完全相同的事(json.Marshal(req))。因此runAction的响应里携带的模型输出,实质上是“框架序列化出的 ModelRequest 本身”——比对它就是在逐字段比对两个语言运行时对模型请求的序列化行为。
JS 端代码里有一段耐人寻味的注释(tests/test_js_app/src/index.ts):
// In Go, JSON object properties are output in sorted order. // JSON.stringify uses the order they appear in the program. // So swap the order here to match Go. const m = input.messages[0]; input.messages[0] = { content: m.content, role: m.role };Go 的encoding/json按字母序输出结构体字段(content在role之前),而 JS 的JSON.stringify按属性插入顺序输出,所以 JS 端必须手工把content排到role前面才能与 Go 的输出逐字节一致——这是跨语言一致性测试中“序列化顺序”这类隐蔽差异的鲜活案例。
有选择的字段排除
比对并非全盘逐字,reflection_api_test.ts 显式排除了若干键,且每个排除都带有工程动机注释:
excludeKeys: [ // TODO: Go and JS JSON schema generation is very different. 'outputSchema', 'inputSchema', // FIXME: Go does not set description field 'description', // FIXME: Go does not set telemetry/latencyMs fields. 'telemetry', 'latencyMs', 'usage', ],从源码注释可以推断当前语言间的已知实现差异:Go 端尚未生成 JSON schema(outputSchema/inputSchema)、未设置description字段、未上报telemetry/latencyMs。测试框架的策略是“对已知差异显式豁免,对未知差异零容忍”——一旦某字段出现意外 diff,测试即置process.exitCode = 1并抛错,从而把这些差异锁定在清单内、防止其扩散。
六、开发者 UI 测试:Puppeteer 驱动的浏览器端到端验证
tests/src/dev_ui_test.ts 用runDevUiTest走完整 UI 链路,脚本本身只有 40 行左右,却覆盖了开发者 UI 的核心交互路径:
runDevUiTest('test_js_app', 'node js/index.js', async (page, url) => { await page.goto(url); const basicFlowElemement = await page.waitForSelector('text/testFlow'); basicFlowElemement?.click(); const editor = await page.waitForSelector('#input-editor .monaco-editor'); // 清空编辑器后输入 "hello world" await editor!.type('"hello world"'); const runFlowButton = await page.waitForSelector('button ::-p-text(Run)'); runFlowButton?.click(); await page.waitForSelector('text/Test flow passed'); const inspectFlowButton = await page.waitForSelector('text/View trace'); inspectFlowButton?.click(); await page.waitForSelector('text/testFlow'); });验证链路为:打开开发者 UI → 点击左侧 flow 树中的testFlow→ 在 Monaco 输入编辑器中改写成"hello world"→ 点击 Run → 等待testFlow自身返回的Test flow passed文案(该文案只有在回声模型输出与 JS 端期望字符串完全一致时才会返回,见 tests/test_js_app/src/index.ts 的断言逻辑)→ 再点 View trace 验证 trace 视图能正常渲染。
配套的 tests/src/utils.ts 中runDevUiTest还做了两件值得注意的事:
- 以
slowMo: 50启动 Puppeteer,保证人工录屏可辨识的操作节奏; - 用
PuppeteerScreenRecorder把整个测试过程录屏到./last_recording.mp4——失败时可直接回看 UI 行为,这对纯断言式的 UI 测试是极其实用的调试手段。
该脚本对应 tests/package.json 中独立的test:dev_ui_test命令,需先完成 README 中第 1 步的 Chrome 安装。
七、从 e2e 测试到跨语言行为规格
tests/specs/下的规格文件呈现出由浅入深的三层结构:
| 规格文件 | 消费方 | 作用 |
|---|---|---|
| tests/flow_server_tests.yaml | flow_server_test.ts | 流式 flow 的 chunk 序列与终值断言 |
| tests/specs/reflection_api.yaml | reflection_api_test.ts | reflection HTTP API 响应体比对 |
| tests/specs/agent.yaml | 各语言的 conformance 测试框架 | Agent API 的跨语言行为规格 |
其中 tests/specs/agent.yaml 的文件头注释说明了一个更宏大的设计意图:
# This file describes the behavioral specification for the Agent API. # It is designed to be consumed by conformance test harnesses in any # language (JS, Go, Dart, Python, etc.) to ensure cross-language # compatibility of the Agent abstraction.它还定义了capabilities机制(如resumable-failures、resumable-aborts):任何语言的 conformance harness 遇到自己尚未实现的 capability 就跳过对应用例,而一个未登记的requires名称会在所有harness 中直接失败而非静默跳过——这从机制上区分了“语言尚未实现该能力”与“测试写错了能力名”两种情况。同样地,tests/specs/generate.yaml 为/util/generateaction 定义了覆盖普通调用、流式响应(expectChunks逐 chunk 断言)与工具调用回路(toolRequest/toolResponse消息序列)的用例集,是模型层行为的声明式规格。可以说,tests/目录既是 e2e 测试的载体,也是 GenKit 多语言实现之间“契约”的存放地。
八、实操清单与适用前提
综合 tests/README.md 与上述源码实现,在本仓库上完整跑一遍本地 e2e 验证的最小操作序列为:
# 1. 一次性准备:安装 Puppeteer 所需 Chrome npx puppeteer browsers install chrome # 在 tests/ 目录 # 2. 根目录:构建 + 打包(本地源码有改动时必做) pnpm build pnpm pack:all # 3. 运行测试(二选一) pnpm test:e2e-local # 根目录:自动 build + pack:all + 进 tests 安装并运行 pnpm test:e2e # 根目录:仅安装依赖并运行(要求 dist 已是目标版本) # 或在 tests/ 内: pnpm i && pnpm test # flow_server + reflection_api pnpm test:dev_ui_test # 额外的开发者 UI 浏览器测试适用前提与限制:
- 包管理必须使用pnpm(根
package.json有preinstall: npx only-allow pnpm强制约束),测试脚本以node --import tsx直接执行 TS 源码; - Go 侧测试需要本机可用的 Go 工具链(
go run main.go会现场编译 go/tests/test_app); - 端口占用是硬依赖:flow server 固定用3400,reflection/dev UI 用3100,且每次运行都会通过
terminate清理上一轮的残留进程; - 测试验证的对象是
dist/中的本地构建产物而非 npm 发布版,因此“先 build、再 pack”的顺序不可颠倒; - 从源码注释可见,部分字段(schema、telemetry 等)因语言实现进度不同被显式豁免,这类差异清单是随语言实现演进而收窄的“活清单”。
小结
这套 e2e 体系的价值在于把“验证”拆成了三层可复用资产:声明式规格(YAML 用例,可被 JS 测试直接消费,也被其他语言的 conformance harness 复用)、自举式测试运行时(utils.ts负责从dist/tarball 安装库与 CLI、拉起应用、解析就绪信号、回收进程),以及真实链路验证(HTTP 流式客户端、reflection 响应逐字段 diff、Puppeteer 点击开发者 UI 并录屏)。对希望为多语言框架建立行为一致性保障的读者而言,其“已知差异显式豁免、未知差异零容忍、临时目录隔离安装”的做法,可以直接作为工程模板借鉴。
【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考