package eino import ( "encoding/json" "reflect" "testing" "github.com/cloudwego/eino/schema" ) // board 必须同时实现 json.Marshaler+Unmarshaler,Eino checkpoint 才会走自定义 JSON 路径 // 持久化未导出字段;任一缺失都会让序列化器回退到「只存导出字段」=空 board。 var ( _ json.Marshaler = (*board)(nil) _ json.Unmarshaler = (*board)(nil) ) // TestBoardSnapshotRoundtrip 钉死黑板快照往返:中断点的全部执行态都能无损还原(resume 不丢状态)。 func TestBoardSnapshotRoundtrip(t *testing.T) { orig := &board{ uid: "u42", sid: "s7", query: "介绍杭州西湖", profile: "用户偏好简洁", history: []*schema.Message{schema.UserMessage("上一轮"), schema.AssistantMessage("上一轮回答", nil)}, kb: "u42/travel", refs: []string{"西湖十景…", "苏堤春晓…"}, toolOut: []string{"[wiki] 杭州…"}, sections: []reportSection{ {Heading: "概况", Body: "西湖位于杭州。"}, }, answer: "西湖是…", agentOut: []string{"草稿一", "草稿二"}, // fatalErr 故意置非空:快照应丢弃它(transient),不应影响往返。 fatalErr: errRejected, } data, err := json.Marshal(orig) if err != nil { t.Fatalf("marshal: %v", err) } var got board if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal: %v", err) } // 持久化字段逐一比对。 if got.uid != orig.uid || got.sid != orig.sid || got.query != orig.query || got.profile != orig.profile || got.kb != orig.kb || got.answer != orig.answer { t.Fatalf("标量字段未还原: %+v", got) } if !reflect.DeepEqual(got.refs, orig.refs) || !reflect.DeepEqual(got.toolOut, orig.toolOut) || !reflect.DeepEqual(got.agentOut, orig.agentOut) || !reflect.DeepEqual(got.sections, orig.sections) { t.Fatalf("切片字段未还原: %+v", got) } if len(got.history) != len(orig.history) || got.history[0].Content != "上一轮" { t.Fatalf("history 未还原: %+v", got.history) } // fatalErr 是 transient,快照不持久化 → 还原后必为 nil。 if got.fatalErr != nil { t.Fatalf("fatalErr 不应被持久化,got %v", got.fatalErr) } }