Skip to content

RFC-0045: 上游测试迁移策略 - #281

Merged
eric8810 merged 7 commits into
masterfrom
claude/project-thread-15ah47
Oct 20, 2026
Merged

eric8810 merged 7 commits into
masterfrom
claude/project-thread-15ah47

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

新增 rfc/0045-upstream-test-parity.md(Draft)和它第 2 节用到的统计脚本 scripts/aisdk-tests/inventory.mjs(附单测,挂在 CI 现有的 Node 脚本步骤里)。

按 VERSIONS.json 锁定的 18 个包、各自的 git tag 统计,上游有 445 个测试文件、8810 个用例,按用例名能在 aimux 里找到对应的 1475 个。上游很多功能我们没做,也不打算做,所以不能全迁。本文定迁移策略:

  • 测试跟着代码走:只迁 aimux 已实现模块对应的上游测试文件(逐个判断:已实现 208 个、部分实现 52 个、没实现 40 个、不适用 145 个)。没实现的功能,测试由将来实现它的 PR 一起带进来。
  • 选了文件再选用例:固定五个排除类别。single-step(对齐文档 Q1、D25)和 telemetry 在 ai 包里按 API 名自动判定;ts-only、registered、sub-feature 每条手写一行理由。
  • 证据形式:请求、响应、流事件、错误分类优先用差分 fixture;纯转换函数逐条搬。
  • 位置和文件状态:按 RFC-0046 §5 的位置和 §4.4 的 layout.toml,本文在条目上加 tests 表和可选的 version。出处标记写完整 describe 链,每层 describe 对应一个 Rust mod。
  • 不设全局清零目标,也不设只升不降的限制:完成标准是选中的每个文件迁过一遍。

估计要补约 3000 到 3500 个用例,core 的 generate-text 和 stream-text 占 250 到 350 个。

测试体系概览(master 5743a40a)

CI 怎么跑测试:

.github/workflows/ci.yml
  Detect code changes                              ci.yml:71    纯文档 PR 跳过下面的代码 job
  Rust core (fmt + clippy + test)                  ci.yml:207
    Ops contract tests                             ci.yml:231   contract-tests/ops:错误信封、流、manifest
    cargo test --workspace                         ci.yml:233
      ├─ crate 内单元测试(#[cfg(test)])            例:aimux-core/src/generate/tests.rs,mock 模型 ScriptedModel :17
      ├─ aimux-core/tests/                         搬来的上游单测,例:util_test.rs 的 fix-json 用例
      └─ aimux-providers/tests/(130 个测试二进制)
           ├─ *_factory_test.rs                    上游录制回放(差分)
           ├─ <厂商>_test.rs                        搬来的上游单测
           └─ replay_test.rs + cassettes/          cassette 回放(32 个厂商目录)
  Generator drift checks                           ci.yml:702
    node --test scripts/fetch-aisdk.test.mjs …     ci.yml:729   AI SDK 锁定版本脚本
  Node / Python / aimux-ffi / Java+Kotlin / Swift / Flutter    各绑定 job:共享契约向量、C ABI

其中和上游对照的有两条路,cassette 回放对照的是 aimux 自己录下的历史响应,不是上游。

差分 fixture:先在 Node 里跑真实的 @ai-sdk/*,再在 Rust 里用同一份 JSON 回放比对。目前 29 个,覆盖 openai、openai-compatible、anthropic、google、provider-utils。

node scripts/aisdk-fixtures/record.mjs openai
  recordCase({ pkg, version, c })                  scripts/aisdk-fixtures/record.mjs:161
    c.build(mock)                                  cases/openai.mjs:56    provider(mock).chat('gpt-4o')
    runOperation('generateText', model, input)     record.mjs:128         generateText({ model, _internal })
    → fixtures/aisdk/openai/chat-basic.json        记下 URL / headers / body 和结果

cargo test -p aimux-providers --test openai_factory_test
  chat_basic()                                     aimux-providers/tests/openai_factory_test.rs:524
    replay_chat("chat-basic")                      :503
      Fixture::load()                              :212   读同一份 JSON
      Fixture::mock()                              :237   用录下的响应造 MockFetch(tests/common/mock_fetch.rs:54)
      model.do_generate(&fixture.call_options())   :515
      Fixture::assert_request()                    :265   URL / headers / body 与上游逐字段比
      assert_generate_result()                     :426   结果与上游比

手搬的上游单测:上游用例和 aimux 测试并排。

上游 packages/openai/src/responses/openai-responses-language-model.test.ts(@ai-sdk/openai@4.0.83)
  describe('OpenAIResponsesLanguageModel')
    createTestServer({ 'https://api.openai.com/v1/responses': {} })   :240
    describe('doGenerate') › describe('code interpreter tool')
      it('should send request body with include and tool')            :3713(同名还有 :3795 :4035 :4087 :4190,只差中间那层 describe)
        model.doGenerate(...)
        expect(server.calls[0].requestBodyJson).toMatchInlineSnapshot(...)

aimux aimux-providers/tests/groq_test.rs
  // groq-prepare-tools.test.ts                                       :705   出处写在分节注释里
  mod prepare_tools
    /// TS: "should correctly prepare function tools"                :711

体系里目前没有一步把"上游有哪些用例"和"aimux 有哪些测试"对起来,所以上面这些测试全绿,并不说明上游用例覆盖到了哪里。

本 PR 的改动(细节)

本 PR 加 RFC 和统计脚本。RFC 在上面的体系里再加四样东西,下面是它规定的调用路径。

0. 统计上游用例(本 PR 的代码,RFC §2):

node scripts/aisdk-tests/inventory.mjs --repo <vercel/ai blobless clone> [--json out.json]
  main()
    读 fixtures/aisdk/VERSIONS.json                      18 个包
    git ls-tree -r <包>@<版本> -- packages/<目录>          *.test.ts / *.test.tsx,不含 *.test-d.ts
    git cat-file --batch                                 一次取全部文件内容
    listCases(src)                                       小 lexer:跳过注释、字符串、模板、正则
      describe(...) 入栈 → it / test(含 skip/only/todo/each)记 { line, chain }
  → 按包打印文件数、用例数:445 个文件、8810 个用例
node --test scripts/aisdk-tests/inventory.test.mjs     ci.yml:729(并进现有步骤)

1. 选哪些上游用例(RFC §3.1、§3.2):

layout.toml(RFC-0046 §4.4 建立,每个上游源文件一条)
  [[openai.files]]
  upstream = "src/responses/openai-responses-language-model.ts"
  status   = "mapped"           → 对应的 .test.ts 进入迁移;missing / not-applicable 不迁
  version  = "…"                ← 只在模块按更高版本对齐时写,现在只有 decide(ai@7.0.128)
  [openai.files.tests]          ← 一律是表,空表 = 选中、没有手写排除
  registered  = [{ case = "… › …", ref = "<对齐文档 §5 那一行>", reason = "…" }]
  sub-feature = [{ case = "… › …", issue = 275, reason = "…" }]
  ts-only     = [{ case = "… › …", reason = "…" }]

check.mjs 对 ai 包的文件自动判定(不写进 layout.toml)
  describe 链 + 用例体出现 stopWhen / prepareStep / needsApproval / 带 execute 的工具 … → single-step
  出现 telemetry / on…Start / on…End / on…Finish 回调                                   → telemetry

2. 搬来的测试(RFC §4、§5;位置按 RFC-0046 §5):

aimux-providers/src/openai/tests/responses/responses_language_model.rs   一个上游测试文件一个 Rust 文件;src/openai/mod.rs 末尾 #[cfg(test)] mod tests;
  (测内部函数和走公开 API 的用例在同一个文件里,跟上游一致;都编进 lib 的测试程序)
  mod openai_responses_language_model › mod do_generate › mod code_interpreter_tool
    /// upstream: @ai-sdk/openai src/responses/openai-responses-language-model.test.ts › OpenAIResponsesLanguageModel › doGenerate › code interpreter tool › should send request body with include and tool
    should_send_request_body_with_include_and_tool()
      MockFetch::new(...)                     共用 mock fetch 放 src/test_support/(由 tests/common/ 移入)
      model.do_generate(...)
      assert_eq!(seen[0].json_body(), json!({ "include": [...], ... }))   上游 inline snapshot 原样写成 json!
  mod … › mod image_generation_tool
    should_send_request_body_with_include_and_tool()   同名函数在不同 mod 里,能编译,标记也各自对上

完整链也相同的 7 处,标记加 #2、#3,函数名加 _2、_3。行为和上游不一致时,每处单独开一个 fix PR(带一个在 master 上失败的测试,登记到 #275 的 provider 偏差清单),测试 PR 叠在上面、只含测试和 layout.toml;刻意的差异登记后记进 registered。

3. CI 校验(RFC §7,新增 job):

scripts/aisdk-tests/check.mjs
  inventory.mjs 的用例清单做底(条目有 version 时取那个 tag)
  读 layout.toml:status = mapped / elsewhere 的文件及其 tests 表
  扫 Rust 的 `/// upstream:` 标记、cases/*.mjs 的 upstream 字段
    → 标记指向不存在的用例、一个用例被标记两次、version 低于锁定版本:失败
    → 类别不在五类之内,手写排除缺 reason,registered 缺 ref,sub-feature 缺 issue:失败
    → 每个文件 已迁 / 自动排除 / 手写排除 / 未处理 写进 job summary,自动排除的用例单列(只报告,不判失败)
  check.mjs diff <旧版本> <新版本>:升级上游时列出新增用例

4. core 差分(RFC §6,新增):

node scripts/aisdk-fixtures/record.mjs ai-core
  cases/ai-core.mjs:MockLanguageModelV4 按脚本返回(只录单步场景)
    streamText → 完整事件流;generateText → 结果
  → fixtures/aisdk/ai-core/*.json

cargo test -p aimux-core
  ScriptedModel(三份合成一份,和 mock fetch 一样放 crate 内、test-only feature 打开)按同一脚本返回
    stream_text()                           aimux-core/src/generate.rs:1563
    TextStreamPart 序列与上游逐个比

厂商侧的差分 fixture 照现有 *_factory_test.rs 的路径,补到还没有 fixture 的 11 个厂商包:amazon-bedrock、anthropic-aws、azure、cohere、deepseek、google-vertex、groq、huggingface、mistral、open-responses、xai。

Type of change

  • Docs / RFC
  • Tooling(统计脚本及其单测)

Checklist

  • Updated relevant docs (rfc/)
  • Commit messages follow Conventional Commits
  • node --test scripts/fetch-aisdk.test.mjs scripts/aisdk-fixtures/versions.test.mjs scripts/aisdk-tests/inventory.test.mjs 本地通过(15 个)

Notes for reviewers

🤖 Generated with Claude Code

https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

有几处需要改。

  1. §4 的标记示例 › doGenerate › should send request body with include and tool 在 @ai-sdk/openai@4.0.83 里对应 5 个用例(:3713、:3795、:4035、:4087、:4190)。不写中间那层 describe,这 5 个用例就分不开。同一个文件里的同名 Rust 函数也会编译失败。建议每层 describe 对应一个 mod。
  2. VERSIONS.json 锁定的 ai@7.0.127 只有 src/evaluate/,src/decide/ 从 7.0.128 才有。按锁定版本取测试,拿到的会是 evaluate.test.ts。§7 第 1 条又要求每个标记指向锁定版本里真实存在的用例,所以在现在的锁定版本下,decide 的测试没法打标记。
  3. tests 字段在 §3.1 是字符串 "port",在 §3.2 是表,需要统一成一种。
  4. 完成标准只靠分类就能满足,而排除又不写理由。建议 single-step 和 telemetry 按 API 名自动判定,其余三类每条带一行理由。
  5. §1 写不设只升不降的限制,§7.4 又要求测试数量不减少,两处冲突。
  6. §6 的清单漏了 huggingface,但 §9 要求每个对齐的厂商包都有差分 fixture。

另外,§2 的 409 个文件和 7,784 个用例能否附上统计脚本和统计范围?我用正则数行首的 it( 和 test(,范围是 18 个锁定 tag 下 packages/<包>/ 的全部 *.test.ts(x),得到约 445 个文件、约 8,800 个用例。

…y script

- Markers carry the full describe chain; each describe level is a Rust
  mod, and cases with identical chains get #2, #3.
- A module aligned to a newer upstream than VERSIONS.json (decide at
  ai@7.0.128) sets `version` on its layout.toml entry.
- `tests` is always a table; single-step and telemetry are decided by
  API name in the ai package, the other categories need a reason.
- Drop the test-count check from section 7 (it belongs to RFC-0046).
- Fixture list covers all 15 vendor packages, huggingface included.
- scripts/aisdk-tests/inventory.mjs counts the cases at the pinned tags:
  445 files, 8810 cases; section 2 is recomputed from it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

Copy link
Copy Markdown
Contributor Author

六条都改了,统计脚本也附上了,见 a18f124。

意见 怎么改的 位置
1. 标记示例分不开 5 个同名用例,同名 Rust 函数也编译不过 标记写完整 describe 链,每层 describe 对应一个 mod,示例改成两个同名函数分别放在 code_interpreter_tool、image_generation_tool 两个 mod 里。锁定版本里有 7 处完整链也相同,按出现顺序加 #2、#3,函数名加 _2、_3 §4
2. 锁定的 ai@7.0.127 没有 src/decide/,decide 的测试打不了标记 layout.toml 条目加可选的 version,只在对齐文档按更高版本对齐时写。decide.ts 写 version = "7.0.128",迁 decide.test.ts 的 20 个用例;deprecated-aliases.test.ts 和 7.0.127 的 src/evaluate/ 按对齐文档 §3.1(D31)记 not-applicable。check.mjs 要求 version 不低于锁定版本,锁定版本升上去以后删掉 §3.1,§7 第 1 条
3. tests 一处是字符串一处是表 一律是表,mapped、elsewhere 必须有,空表表示选中、没有手写排除 §3.1,§3.2
4. 完成标准只靠分类就能满足,排除不写理由 照你的建议:single-step、telemetry 在 ai 包里按 API 名自动判定,名单列在 §3.2;onError、onChunk、onAbort 不在名单里,因为它们测的是错误和中止行为本身。ts-only、registered、sub-feature 每条写 reason,registered 另写对齐文档 §5 那一行,sub-feature 另写 issue。规则漏掉的多步用例也手写并带理由。CI 的进度报告里单列自动排除的用例 §1,§3.2,§7,§9
5. §1 说不设只升不降,§7.4 又要求测试数不减少 删掉 §7.4。测试数前后相等是 RFC-0046 搬移 PR 的检查,不属于本 RFC;§1 写明 CI 不比较前后的数量 §1,§7
6. §6 漏了 huggingface 厂商包改成按 VERSIONS.json 定义:去掉 ai、provider、provider-utils 剩 15 个,已有 fixture 4 个,其余 11 个全部列出,huggingface 在内;§9 引用同一份清单 §6,§9
统计脚本和统计范围 新增 scripts/aisdk-tests/inventory.mjs,附单测,挂进 CI 现有的 Node 脚本步骤(ci.yml:729)。范围和你的一样:18 个锁定 tag 下 packages/<包>/ 的全部 *.test.ts(x),不含 *.test-d.ts;用小 lexer 跳过注释、字符串和正则,所以 .test('https://…') 这种正则方法调用不会算进去,it.each 只算一次。结果是 445 个文件、8810 个用例,和你的数一致 §2

初稿的 409 个文件、7784 个用例对不上,是因为取了 ai@7.0.127 这一个提交。那个提交里的厂商包比锁定版本新(google-vertex 是 5.0.101,锁定 5.0.98;amazon-bedrock 是 5.0.105,锁定 5.0.100),而且没算 ai/ 的 ui、agent、realtime 等目录。§2 的现状表已按锁定 tag 重算:

  • 选中 260 个文件、6335 个用例,其中 1431 个已有对应;
  • core 的两个文件共 746 个用例,自动规则判出 310 个 single-step、87 个 telemetry,要补的在 250 到 350 个之间。

Generated by Claude Code

cunninghamcard-bit and others added 2 commits October 20, 2026 03:56
Internal-function tests go in foo/tests.rs (`mod tests;`), public-API
tests in per-vendor directories under tests/, built as tests/it/main.rs;
the shared mock fetch sits in the crate behind a test-only feature.
Replaces the foo_test.rs and src/test_support/ wording, matching
RFC-0044 and RFC-0046.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Public-API tests sit in tests/it/<vendor>/, helpers in src/test_support/
behind the test-support feature; layout.toml is RFC-0046 §4.4 and test
placement §5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
One Rust file per upstream test file, named after it, holding both the
internal-function and the public-API cases, with fixtures and snapshots
beside it. No foo/tests.rs directories and no tests/it/ program; the
crate-level tests/ keeps only cross-vendor tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Test PRs carry only tests and layout.toml and stack on the fix PR,
following one PR one direction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

评审:RFC-0045 上游测试迁移策略 —— 批准

策略本身与既有规范体系一致且闭环完整,核验通过的关键点:

  1. "测试跟着代码走" 与 RFC-0036"设计齐备不等于实现完成"的纪律一致;未实现功能的测试不预迁、随实现 PR 带入——正确。
  2. 版本纪律:测试源按 VERSIONS.json 锁定 tag 取(发布包不带测试的细节处理正确);decide@7.0.128 的版本例外机制与 #255 已登记的 Decision 行为参照完全一致,且 check.mjs 强制 version ≥ 锁定版本、升级后删字段——闭环。
  3. 排除类别(自动 single-step/telemetry + 手写 ts-only/registered/sub-feature 各带理由/ref/issue)与对齐文档 Q1/D25/§5/§9.1 的既有决定一一对应,不允许 #[ignore]、修复与测试 PR 分离并叠放——与 ts-rust-port 纪律和当前 PR 波(#282–#291 fix、#295–#309 test)的实际形态吻合。
  4. 数字内部自洽:445=208+52+40+145,8810 各行可加总,1431=1327+104;core 缺口的 250–350 估算路径(746−26−310−87−多步漏网)可复核。inventory.mjs 按 tag 读 blobless clone 的实现与描述相符,单元测试已接入 CI(ci.yml 改动仅加一行)。
  5. 完成标准可判定:按文件×用例的全覆盖判定 + CI 校验(标记指向真实用例、唯一性、排除带理由)+ job summary 进度——不是数量清零目标,可执行。

两个观察项(不阻塞):

  • 落地顺序:本 RFC 的机制面(layout.toml 的 status/tests/version 字段、§4/§5 的位置规则)依赖 RFC-0046(#294,Draft)与 RFC-0044(#279,Draft)。策略先行合并可接受(批 0 才接线),但请确认 0044/0046 会在批 0 执行前落地,避免 layout.toml 机制悬空。
  • 445/8810 及 3000–3500 的统计数字无法在不克隆 vercel/ai 的情况下独立复核;鉴于其仅为估算基数、非门禁,接受声明值。check.mjs 在批 0 上线前的标记纪律属约定执行,已如实声明。

批准。三个 RFC(0044/0045/0046/0047)建议作为一组尽快齐落地。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
@eric8810
eric8810 merged commit 798a2cd into master Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

谢谢评审。两个观察项的情况:

eric 的观察 现状 / 本次改动 位置
「落地顺序:请确认 0044/0046 会在批 0 执行前落地,避免 layout.toml 机制悬空」 layout.toml 不靠 RFC-0046 合入才有:已开的五个厂商测试 PR 各自建这个文件、只写自己那家的表(内容相同的部分逐字一致,后合的只需合并厂商表),check.mjs 接线时文件已在。RFC-0046(#294)和 RFC-0044(#279)由包拆分那边推进,每家的搬移 PR 叠在对应测试 PR 上(例:Hugging Face #304 叠在 #303 上)。落地先后由你定。 #295 #296 #299 #301 #303 的 aimux-providers/layout.toml
「445/8810 及 3000–3500 的统计数字无法在不克隆 vercel/ai 的情况下独立复核」 同意,只作估算基数。复核只需一条命令:node scripts/aisdk-tests/inventory.mjs --repo <vercel/ai blobless clone>。 scripts/aisdk-tests/inventory.mjs

另外顺手改了两处与现行做法不一致的文字(提交 2ee55148 和之前的 d2e8d79e):行为修复不放进测试 PR,每处单独一个 fix PR,测试 PR 叠在上面;fix PR 登记在 #275 的 provider 偏差清单。PR 描述里第 2 段的位置示例也改成了 src/<vendor>/tests/ 的布局。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants