Impeccable Framework Fixtures 深度指南:跨框架 Live 模式测试矩阵的设计与实战
Impeccable 的 live 模式需要在 Vite、Next.js、Astro、SvelteKit、Nuxt、TanStack 等不同框架约定下完成脚本注入、元素包裹(wrap)、变更接受(accept)与 CSP 探测,这是一份非常容易因框架差异而失真的能力。本文以仓库中 tests/framework-fixtures/README.md 为骨架,结合 tests/framework-fixtures.test.mjs、tests/lib/engine-bin.mjs、tests/live-e2e.test.mjs 及全部 34 个 fixture 实例,完整讲解 fixture 的目录布局、fixture.json 全量 schema、运行时 E2E 七步流程、appDir 根解析与各场景探针的配置方法。读完本文,你将能够读懂、扩展并运行 Impeccable 的跨框架测试矩阵,为新的框架形态编写自己的 fixture。
一、Fixture 是什么:一套可复现的"框架形状"
Framework fixture 是代表不同框架工程约定的微型项目树。它不追求还原真实项目全貌,而是精准复刻那些会影响 live 模式工作的关键结构——index.html 壳、app/layout.tsx、src/app.html、svelte.config.js 里的 CSP 指令、monorepo 的共享配置包等。
测试运行时,harness 会把每个 fixture 拷贝进一个临时 git 仓库,然后驱动引擎二进制(engine binary)的 live-inject、live-wrap、live-accept 与 detect-csp 动词,覆盖两条测试路径:
- 静态 sweep:仅做注入、包裹、CSP 探测三项检查,由 tests/framework-fixtures.test.mjs 执行,命令为
node --test tests/framework-fixtures.test.mjs; - 运行时 E2E:真正安装依赖、启动框架 dev server、用 Playwright 驱动浏览器验证 live 握手,由 tests/live-e2e.test.mjs 执行,命令为
bun run test:live-e2e。只有fixture.json中带runtime块的 fixture 才进入此路径。
引擎二进制的定位逻辑见 tests/lib/engine-bin.mjs:依次检查环境变量 IMPECCABLE_BIN、skill/scripts/bin/<os>-<arch>/impeccable[.exe](由 bun run fetch:engine 写入)、target/release/impeccable[.exe](由 cargo build --release -p impeccable 写入)。找不到二进制时返回 null,测试套件会整体 skip,而不是在无引擎的机器上报错——这是所有 sweep 默认"跳过而不是失败"的机制来源。
二、目录布局与 fixture.json 全量 Schema
2.1 标准三段式布局
<fixture>/
files/ project tree the test copies into tmp
gitignore.txt becomes .gitignore in tmp (so we can commit the real files here)
fixture.json config + expected results the test consumes
files/:要被拷贝进临时仓库的项目树;gitignore.txt:在 tmp 中变成.gitignore,让仓库本体可以真实提交dist/、node_modules/等通常被忽略的内容(例如multipage-with-generator/的dist/就靠它 gitignore);fixture.json:测试消费的配置与期望结果,下文逐字段展开。
2.2 顶层字段:静态验证部分
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | 人类可读的标签,测试会断言其非空 |
config |
object | 写入 .impeccable/live/config.json 的注入配置(见 2.3) |
sourceFiles |
string[] | is-generated 应判为"源文件"(false)的路径列表 |
generatedFiles |
string[] | is-generated 应判为"生成文件"(true)的路径列表 |
wrapCases |
object[] | 包裹断言列表(见 2.4) |
csp |
object | CSP 探测期望(见 2.5),可省略 |
runtime |
object | 运行时 E2E 配置(见第三节),可省略 |
2.3 config:live 注入配置
对应引擎二进制读取的 .impeccable/live/config.json,以 vite-react/fixture.json 为例:
{
"config": {
"files": ["index.html"],
"insertBefore": "</body>",
"commentSyntax": "html"
}
}
files:需要注入 live 脚本的文件列表;insertBefore:注入锚点,如</body>或 TanStack Start 的<Scripts;commentSyntax:注入标记的注释语法,html(HTML 注释)或jsx(JSX 注释,Next.js 场景)。
不同框架的注入目标差异体现在各 fixture 中:nextjs-app/ 把 app/layout.tsx 作为 JSX 注入目标,astro/ 把 src/layouts/Layout.astro 作为 HTML 注释目标,sveltekit/ 注入 src/app.html 壳,tanstack-start/ 则因为没有静态 index.html,把注入目标指向 src/routes/__root.tsx 并在 insertBefore 使用 <Scripts。
2.4 wrapCases:包裹落点断言
wrapCases 定义 live-wrap 的调用参数与期望落点:
"wrapCases": [
{
"name": "wraps hero title in source JSX",
"args": { "classes": "hero-title", "tag": "h1" },
"expectedFile": "src/App.jsx"
}
]
字段说明:
name:描述;args.classes/args.tag/args.elementId:对应live-wrap的--classes、--tag、--element-id参数;expectedFile:包裹应落在哪个文件(相对 fixture 根);expectedSourceFile:可选,断言来源文件(Svelte 组件预览场景下与expectedFile分离,后者指向 manifest);expectedPreviewMode:可选,如svelte-component;expectsError:可选错误码,如element_not_in_source、element_not_found。
在 tests/framework-fixtures.test.mjs 的 live-wrap 用例中,harness 会为每个 case 追加 --id wraptest<N> --count 3 等旗标,并解析引擎 JSON 输出中的 file、sourceFile、previewMode 字段逐一断言。multipage-with-generator/ 正是靠 wrapCases 里的 expectsError 来验证 is-generated 守卫 与 element_not_in_source 回退路径:dist/ 是生成目录,live-wrap 不会把修改写进生成文件。
2.5 csp:CSP 形状与修补目标
"csp": {
"shape": "shared-helper | inline-headers | middleware | meta-tag | null",
"signals": ["diagnostic hints — paths where CSP was detected"],
"patchTarget": "which file the agent should modify",
"expectedAfter": "filename of the reference post-patch output inside this fixture"
}
shape:CSP 形状枚举。null表示没有 CSP(大多数非 CSP fixture 不写csp块即等价于期望null);signals:CSP 被检测到的诊断提示路径,格式为文件:符号/键名;patchTarget:agent 应修改的文件;expectedAfter:参考修补后输出的文件名,与fixture.json同级存放(不在files/内),仅作人工/agent 审查参考,测试不会自动应用补丁。
仓库内的真实形状分布:
append-arrays(数组合并):nextjs-turborepo/fixture.json 共享createBaseNextConfig助手(signals指向packages/shared/src/next-config/index.ts:buildCSPConfig等);sveltekit-csp/fixture.json 的svelte.config.js:kit.csp.directives;append-string(字符串拼接):nextjs-inline-csp/fixture.json 的next.config.js字面量 CSP;nuxt-csp/fixture.json 的nuxt.config.tsrouteRules 字面量 header;middleware(Next.js 中间件):README 明确指出该名字同时覆盖 Next.jsmiddleware.*与 Next.js 16 的proxy.*约定。nextjs-proxy-csp/即使用proxy.ts,且 tests/framework-fixtures.test.mjs 末尾专门有一个用例验证proxy.ts的放置规则:app 根与src/根下的proxy.ts被识别为middleware形状,而lib/network/proxy.ts、apps/web/lib/proxy.ts这类同名助手不被误判。
以 nextjs-turborepo/expected-after-patch.ts 为例,可以看到 append-arrays 修补后应有的形态:通过 createBaseNextConfig 传入 additionalScriptSrc / additionalConnectSrc,并用 NODE_ENV === "development" 守卫追加 http://localhost:8400 这一开发期允许项,非开发环境为空数组。
三、运行时 E2E:runtime 块与七步握手流程
3.1 完整 Schema 与默认值
runtime 块可选。没有它的 fixture 只跑静态检查;带它的 fixture 额外跑 tests/live-e2e.test.mjs 中的 E2E 套件,完整字段如下(README 原文照录):
"runtime": {
"styling": "plain-css | tailwind-v4 | styled-components | ...",
"appDir": "website",
"install": ["npm", "install"],
"devCommand": ["npm", "run", "dev"],
"scheme": "http",
"ignoreHTTPSErrors": false,
"readyPattern": "Local:\\s+https?://[^:]+:(\\d+)",
"readyTimeoutMs": 120000,
"pickSelector": "h1.hero-title",
"pickPosition": { "x": 10, "y": 10 },
"variantSequence": [3, 1, 2],
"acceptedSourcePattern": "<ul[^>]*class=\"[^\"]*\\bexpense-list\\b",
"assertSourceContains": ["{#each expenses as expense, i}"],
"stateProbe": {
"textSelector": "[data-testid='open-count']",
"expectedText": "3 offen",
"windowProperty": "__impeccableStatefulMounts",
"expectedWindowValue": 1,
"expectWindowUnchanged": true
},
"paramsScenario": {
"variant": 2,
"rangeLabel": "Lead",
"rangeValue": 1.8,
"stepsLabel": "Density",
"stepsOptionLabel": "Snug",
"expectSourceContains": ["line-height: 1.8"],
"expectSourceMissing": ["letter-spacing: 0.14em"]
},
"componentFailureScenarios": { "variant": 2, "storageLoss": false },
"mode": "insert",
"insert": {
"anchorSelector": "section#features",
"position": "after",
"prompt": "Add a testimonial strip below features",
"expectSelector": ".inserted-strip",
"assertAnchorContains": "feature-grid"
},
"preActions": [
{ "type": "click", "selector": "[data-testid='open-modal']" },
{ "type": "goto", "path": "/about" }
],
"reloadProbe": {
"preActions": [{ "type": "click", "selector": "[data-testid='open-modal']" }],
"expectSelector": "h1.hero-title"
},
"steer": {
"message": "steer-e2e mark hero",
"expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]"
},
"probe": {
"expectLiveInit": true,
"expectConsoleClean": true
}
}
3.2 运行时 E2E 七步流程
带 runtime 块的 fixture 会执行 tests/live-e2e.test.mjs 中的完整 E2E 套件:
- staging:把 fixture 拷入临时仓库;
- 安装真实依赖:执行
runtime.install; - 启动 live 基础设施:运行
impeccable live-server --background,再对它执行impeccable live-inject --port; - 启动 dev server:spawn
runtime.devCommand,用runtime.readyPattern从 stdout 抓取端口(第一个捕获组必须是端口号); - Playwright 打开页面:在
runtime.readyTimeoutMs内断言window.__IMPECCABLE_LIVE_INIT__ === true(浏览器端握手 oracle); - Steer smoke(除非
runtime.steer === false):在全局 Steer 栏提交消息,等 fake agent 回复steer_done,断言栏解锁且data-impeccable-steer标记同时落入源码与 DOM;随后继续 pick → Go → 循环变体 → accept 主流程; - 收尾:Playwright 关闭、dev server 收到 SIGTERM、live-server stop、删除 tmp。
3.3 Steer 冒烟的可选字段
"steer": {
"message": "steer-e2e mark hero",
"sourceFile": "src/routes/About.jsx",
"expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]",
"expectSourceContains": "data-impeccable-steer=\"e2e\"",
"preActions": [{ "type": "click", "selector": "[data-testid='nav-about']" }]
}
- 省略
preActions时,steer smoke 继承runtime.preActions以先揭示隐藏的 hero 再做 DOM 检查; - 先断言源码,再通过"reload + 重试"覆盖 HMR 延迟;
- 设
"steer": false跳过整个冒烟(如 vite8-sveltekit-stateful/fixture.json 就是"steer": false,把资源留给更重的场景探针);设"expectDom": false则只做源码验证。
3.4 runtime.appDir:非根目录应用与根解析
appDir 默认 .,当被服务的应用不是仓库根时使用(例如根目录是 CLI 包、站点在 website/ 下的 monorepo-nested-vite/fixture.json)。设置后 harness 的行为:
- 照常在 tmp 根做
git init与 staging; - 把
.impeccable/live/config.json写到<tmp>/<appDir>/下,fixture.json里所有 fixture 相对路径(steer.sourceFile、manual-edit 的expectedSourceFile等)都相对该 app 目录解析; runtime.install与runtime.devCommand以 app 目录为 cwd 执行;- 从 tmp 根调用
impeccable live(而非直接调live-server/live-inject),从而真正锻炼引擎的根解析能力,而不是假设它正确。
解析后的 boot payload 暴露为 session.liveBoot,tests/live-e2e.test.mjs(L1463-L1478 附近)会断言 roots.appRoot 以 /${appDir} 结尾、roots.appRoot 与 appRoot 相同、roots.contextRoot 等于 tmp 根、projectRoot 等于 appRoot,并检查持久化的 roots.json 与 repo 根指针;同时断言 hasProduct / hasDesign 为 true——证明 PRODUCT.md、DESIGN.md 从嵌套应用被正确读取。
session 对象同时携带两条路径:session.tmp 是仓库根(用于 git 与产物捕获),session.appRoot 是应用;没有 appDir 的 fixture 两者相同。
3.5 Picking、Cycling 与渲染证明
pickSelector+pickPosition:picker 解析光标下的元素,因此"中心被子元素覆盖的容器永远无法被选中"。需要pickPosition(相对元素左上角的像素{x, y})瞄准容器真正拥有的点(如它自己的 padding);variantSequence:默认[2],是按顺序循环的顺序,最后一个条目是最终接受的变体。每个落到的变体都会在 fake-agent 模式下得到一次计算出的font-weight断言——因为 fake agent 以不同字重渲染每个变体(FAKE_VARIANT_FONT_WEIGHTS,见 tests/live-e2e/agent.mjs:300 / 900 / 600)。这把"变体 N 可见"从 bar 标签声明变成了渲染事实,所以[3, 1, 2]这样的序列可以证明三个变体真的都被渲染过(vite8-sveltekit-stateful/fixture.json 正是此序列);acceptedSourcePattern:覆盖默认的接受后源码检查(默认是<h1 class="hero-title">);assertSourceContains列出必须在整个 wrap → accept → carbonize 周期后存活的字符串。在 Svelte 组件预览上,这是证明控制流未被拍平的关键手段:把{#each}头和逐项表达式都列进去(如{#each expenses as expense, i}、{expense.name}、{expense.amount}、href={expense.doc})即可验证循环结构被保留;stateProbe:断言页面状态不丢失。textSelector/expectedText检查渲染状态;windowProperty配合expectedWindowValue与expectWindowUnchanged检查一个"挂载时自增"的计数器,因此静默重挂载页面的脚手架会直接失败。它在 preActions 之后、变体落定之后、(非组件预览下)accept 之后各运行一次。
四、Extra Scenarios:参数旋钮、故障注入与存储丢失
除核心周期外,fixture 通过声明配置来启用场景,每个场景还受 IMPECCABLE_E2E_SCENARIOS 门控:
| 场景名 | 启用字段 | 证明的内容 |
|---|---|---|
params |
runtime.paramsScenario |
在真实 Tune popover 中拨动 range 与 steps 旋钮并接受,断言所选值以字面量烘焙进源码,未选分支被丢弃,且不残留 data-p-* / var(--p-*) |
mount-failure |
runtime.componentFailureScenarios |
破坏已发布的 r<N>/v<variant> 修订文件并步入该变体,断言持久化挂载错误卡片出现、会话存活(bar + localStorage 完好)、variant_mount_failed 事件落入 session journal;然后恢复、Retry、重新到达该变体 |
republish |
runtime.componentFailureScenarios |
重新编写每个变体并再次回复 done,浏览器必须挂载新内容——这正是服务器 revision-dir bump 要保证的 |
storage-loss |
runtime.componentFailureScenarios(除非 storageLoss: false) |
清空 localStorage、reload,断言对比结果仅凭服务器持久化 session 记录即可恢复 |
componentFailureScenarios.variant 选择要破坏/观察哪个变体;storageLoss: false 用于"选中元素只在 preActions 之后才存在"的 fixture——reload 会丢弃该状态,重建又会与预览挂载竞态(vite8-sveltekit-stateful/fixture.json 即如此设置)。
这些场景断言确定性内容,因此 IMPECCABLE_E2E_AGENT=llm 下会整体跳过。
五、常用 E2E 过滤与环境变量
运行时 E2E 常用过滤(直接作用于测试进程):
IMPECCABLE_E2E_ONLY=<fixture>[,<fixture>]:把运行范围限定到指定 fixture 名;IMPECCABLE_E2E_SCENARIOS=core:只跑主"点击 → Go → 循环 → accept"路径;省略或设为all则包含 manual edit、annotation、exit、params 与组件故障注入探针。合法值:core、manual、annotations、exit、missed-done、params、mount-failure、republish、storage-loss;IMPECCABLE_E2E_TEST_TIMEOUT_MS、IMPECCABLE_E2E_INSTALL_TIMEOUT_MS、IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS:收紧 CI 冒烟超时,无需改动 fixture 元数据;- 二进制相关:
IMPECCABLE_BIN可直接指定引擎二进制路径;IMPECCABLE_E2E_AGENT=llm切换到 LLM agent 模式。
六、静态 sweep 的一个关键陷阱:glob 双根解析
tests/framework-fixtures.test.mjs 会把同一 fixture 平铺(flat) staging,并把 live 配置写到 tmp 根。这意味着 config.files 必须同时能从仓库根与应用根解析:
- glob(如
"**/index.html")同时满足两种解析,monorepo-nested-vite/fixture.json 正是靠它工作; - 字面量(如
"index.html")只在应用根有效,会让静态 sweep 报file_not_found。
七、当前 Fixture 全览
| Fixture | Shape |
|---|---|
vite-react/ |
受跟踪的 index.html 壳 + src/App.jsx,注入壳 |
nextjs-app/ |
app/layout.tsx 作为 JSX 注入目标(commentSyntax jsx) |
astro/ |
src/layouts/Layout.astro 注入目标,HTML 注释 |
sveltekit/ |
src/app.html 壳 + src/routes/+page.svelte |
vite8-sveltekit-stateful/ |
Svelte 5 路由:$state、{#if} 分支、{#each} 列表;选列表容器,组件预览脚手架须把循环作为单个 collection prop 携带并从 live DOM 水合条目;同时承载 params、故障注入与状态保持探针 |
nuxt-vite7/ |
Nuxt 4 app/ 结构 + Vue 3 SFC,live 通过生成的 dev-only client plugin 加载 |
tanstack-router-vite/ |
Vite + TanStack Router(基于代码的 SPA),受跟踪 index.html 壳注入(基线 Vite 路径,无 adapter) |
tanstack-start/ |
Vite + TanStack Start(SSR),无静态 index.html,live 修补 __root.tsx 文档以挂载生成式 dev-only React 组件加载 bundle |
multipage-with-generator/ |
src/ 受跟踪、dist/ gitignore,锻炼 is-generated 守卫与 element_not_in_source 回退 |
nextjs-turborepo/ |
monorepo + 共享 CSP 助手(createBaseNextConfig),CSP 形状 append-arrays |
nextjs-inline-csp/ |
next.config.js 字面量 CSP 字符串,形状 append-string |
sveltekit-csp/ |
svelte.config.js 的 kit.csp.directives,形状 append-arrays |
nuxt-csp/ |
nuxt.config.ts routeRules 字面量 CSP header,形状 append-string |
monorepo-nested-vite/ |
仓库根是 CLI 包(无 dev 配置、无 workspaces),被服务的 Vite + React 应用在 website/,锻炼 runtime.appDir 与 live 根解析 |
除上表外,仓库还维护了针对 Vite 8 生态的系列 fixture(vite8-react-*、vite8-https、vite8-sveltekit 等),覆盖 Tailwind v3/v4、Unocss、vanilla-extract、CSS Modules、styled-components、emotion、Radix Dialog、base-path、https、CSP meta 等工程形态,与表内 fixture 共用同一套 schema 与流程。
八、如何新增一个 Fixture
按 README 给出的流程,添加新 fixture 的步骤是:
- 克隆一个现有目录作为起点(选择一个最接近目标框架形态的 fixture,例如要新增 Next.js App Router 场景可克隆
nextjs-app/); - 替换
files/中的文件,让它成为你目标框架的最小可运行工程; - 更新
fixture.json:填写name、config(files / insertBefore / commentSyntax)、sourceFiles/generatedFiles、wrapCases(含expectsError回退用例)、csp(含signals、patchTarget、expectedAfter,如适用); - 如果带
runtime块,还需把 fixture 名字加入 .github/workflows/ci.yml 的 live-e2e 矩阵:live-e2e-full组列表,以及(希望每个 PR 都跑时)一个live-e2e-smoke组。保持两组规模大致相当——它们并行运行,而 job 超时上限为 15 分钟。
新增后可用如下命令验证(前提是已通过 bun run fetch:engine 或 cargo build --release -p impeccable 准备好引擎二进制):
# 静态 sweep:inject / wrap / csp-detect
node --test tests/framework-fixtures.test.mjs
# 仅跑目标 fixture 的运行时 E2E
IMPECCABLE_E2E_ONLY=my-fixture bun run test:live-e2e
九、总结:这套矩阵在验证什么
Fixtures 矩阵的核心价值在于把"live 模式在不同框架下都能工作"这一宏愿拆解成可断言、可复现的最小单元:注入是否落在框架要求的正确文件与语法上(live-inject)、包裹是否路由到真正的源文件而非生成文件(live-wrap + is-generated 守卫)、CSP 是否被按形状识别并可给出修补目标(detect-csp + expectedAfter)、以及完整浏览器周期下握手、变体渲染、状态保持、故障恢复是否真实成立(runtime E2E 与各场景探针)。理解这套 fixture 的 schema 与流程,等于同时理解了 Impeccable live 模式在各主流框架下的全部适配点。
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 StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280