Schema.Struct 上的 verifyRoundTrip 为什么失败?

new TestSchema.Asserts(Schema.Struct({ a: Schema.String })).verifyRoundTrip({ seed: 1 }) 这么简单的 Schema,在 effect 4.0.5 上报的是 Shrunk input: {"a":""}、Failure: returned false。

**原因不在你的 Schema,而在比较方式:verifyRoundTrip 用 Node 的 isDeepStrictEqual 比较,而 Struct 的生成器产出的对象原型是 null,解码结果是普通对象,原型不同就判为不等。**另一个原因是 Schema.Number 的生成器会产出 -0,往返后变成 0。

verifyRoundTrip 是 TestSchema.Asserts 的一个方法,做「编码再解码、与原值比较」的往返检查,整体用法见怎么用 TestSchema.Asserts 测 Effect Schema?。下面讲这两点怎么排查出来的、哪些情况不受影响,以及怎么绕开。

为什么相同字段的对象会被判为不等?

这是我实际排查出来的两点:

  • Struct 的 Arbitrary 产出的对象原型是 null(Object.getPrototypeOf 得到 null),而解码结果是普通对象;isDeepStrictEqual 会比较原型,所以即使字段完全相同也判为不等。
  • Schema.Number 的生成器会产出 -0,NumberFromString 往返后变成 0,同样被严格相等拒绝。

哪些 Schema 不受影响?

Schema.Class 的实例不受影响,verifyRoundTrip 对 Payment 这样的 Class 能通过;纯 string 这类原始 Schema 也能通过。所以出问题的范围比较窄:Struct,以及会产出 -0 的数值往返。

这是 bug 吗?

这是 4.0.5 的现状,我没有去查它是不是已被确认为缺陷,所以不替它下结论。如果你用的是别的版本,先用最小例子(上面那个 Schema.Struct({ a: Schema.String }))确认现象是否还在。

要往返测试 Struct,怎么绕开?

自己用 checkEffect 加 Schema.toEquivalence,比较规则由 Schema 决定,不依赖原型:

import { Arbitrary, Effect, Schema } from "effect"

const Wire = Schema.Struct({
  id: Schema.Int,
  at: Schema.DateTimeUtcFromString,
  n: Schema.NumberFromString
})

const roundTrip = Effect.gen(function* () {
  const eq = Schema.toEquivalence(Wire)
  const result = yield* Arbitrary.checkEffect(
    Arbitrary.schema(Wire),
    (w) =>
      Schema.encodeEffect(Wire)(w).pipe(
        Effect.flatMap(Schema.decodeUnknownEffect(Wire)),
        Effect.map((back) => eq(back, w))
      ),
    { runs: 200, seed: 1 }
  )
  console.log(result._tag) // Passed
})

Effect.runPromise(roundTrip)

checkEffect 返回的是数据,不会自己让测试失败,所以放进 vitest 时要判断 _tag,或者用 formatCheckFailure 格式化后再断言。checkEffect 的用法见 Effect v4 不用 fast-check,属性测试怎么写?。

版本是 effect 4.0.5。Struct 往返失败的现象、Object.getPrototypeOf 得到 null、-0 变 0 这两点排查,以及最后一段绕法代码得到 Passed,都在这个版本上跑过。是否已被确认为缺陷、其它版本是否仍然如此,没有验证。