package orbit import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "reflect" "strings" "sync/atomic" "testing" "time" ) func TestIntegrationWorkflowDefinitionSDK(t *testing.T) { api, queue, _ := integrationWorkerClient(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() key := workerTestID(t) created, err := api.CreateWorkflowDefinition(ctx, key, CreateWorkflowDefinitionRequest{Name: "workflow." + key, Description: "精确数字发布回归"}) if err != nil { t.Fatal(err) } id := created.Definition.ID typ := WorkflowType{Kind: "OBJECT", Fields: map[string]WorkflowType{"n": {Kind: "NUMBER"}}} task := WorkflowTask{QueueID: queue, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", InputType: typ, Input: WorkflowValue{Kind: "LITERAL", Literal: json.RawMessage(`{"n":9007199254740993}`)}, ResultType: typ, TimeoutMS: 5000, Retry: RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 3000}, RecoverySafety: RecoveryReconcileOnly} in := PublishWorkflowVersionRequest{Version: "v1", Reason: "SDK publication", Specification: WorkflowSpecification{SchemaVersion: 1, TimeoutMS: 30000, FailurePolicy: "REVIEW", InputType: typ, Body: WorkflowBlock{Steps: []WorkflowStep{{Key: "prepare", Kind: "TASK", Task: &task}, {Key: "wait", Kind: "TIMER", Timer: &WorkflowTimer{DelayMS: 1000}}}, OutputType: typ, Output: WorkflowRef("prepare")}}} // 真服务确认发布提交后,代理关闭客户端连接;SDK不得自动换键或默默重试。 target, _ := url.Parse(api.baseURL) proxy := httputil.NewSingleHostReverseProxy(target) var dropped atomic.Bool var calls atomic.Int32 proxy.ModifyResponse = func(r *http.Response) error { calls.Add(1) if dropped.CompareAndSwap(false, true) { raw, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { return err } if r.StatusCode != 201 || !strings.Contains(string(raw), `"deduplicated":false`) { t.Error("publication was not committed before response loss") } return io.ErrUnexpectedEOF } return nil } proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { conn, _, e := w.(http.Hijacker).Hijack() if e == nil { conn.Close() } } gateway := httptest.NewServer(proxy) defer gateway.Close() client, err := NewClient(gateway.URL, Options{Token: api.token}) if err != nil { t.Fatal(err) } if _, err := client.PublishWorkflowVersion(ctx, id, key, in); err == nil || calls.Load() != 1 || !dropped.Load() { t.Fatal("response loss not surfaced", err) } published, err := client.PublishWorkflowVersion(ctx, id, key, in) if err != nil || !published.Deduplicated { t.Fatal("lost publication not recovered", err) } original := published.Version if string(original.Specification.Body.Steps[0].Task.Input.Literal) != `{"n":9007199254740993}` || len(original.ContentDigest) != 64 || original.Analysis.Steps != 2 || original.Analysis.MaxTaskAttempts != 1 { t.Fatal("SDK result changed precision or analysis") } same, err := api.PublishWorkflowVersion(ctx, id, workerTestID(t), in) if err != nil || !same.Deduplicated || !reflect.DeepEqual(same.Version, original) { t.Fatal("same version", err) } in.Specification.Body.Steps[0].Task.Input.Literal = json.RawMessage(`{"n":9007199254740992}`) _, err = api.PublishWorkflowVersion(ctx, id, workerTestID(t), in) var apiErr *APIError if !errors.As(err, &apiErr) || apiErr.Code != "STATE_CONFLICT" { t.Fatal("overwrite accepted", err) } in.Version = "v2" next, err := api.PublishWorkflowVersion(ctx, id, workerTestID(t), in) if err != nil || next.Version.ContentDigest == original.ContentDigest { t.Fatal("v2", err) } stored, err := api.GetWorkflowVersion(ctx, id, "v1") if err != nil || !reflect.DeepEqual(stored, original) { t.Fatal("immutable v1", err) } metadata, err := api.GetWorkflowDefinition(ctx, id) if err != nil || metadata.Revision != 3 { t.Fatal("unexpected version count", err) } // 类型的空块输出支持显式null,不放宽普通任务的null参数约束。 in.Version = "v3" in.Specification.Body = WorkflowBlock{OutputType: WorkflowType{Kind: "NULL"}, Output: WorkflowValue{Kind: "LITERAL", Literal: json.RawMessage(`null`)}} v3, err := api.PublishWorkflowVersion(ctx, id, workerTestID(t), in) if err != nil || v3.Version.Analysis.Queues == nil || len(v3.Version.Analysis.Queues) != 0 { t.Fatal("null block", err) } if _, err := api.GetWorkflowVersion(ctx, id, "../other"); err == nil { t.Fatal("unsafe path accepted") } t.Log("real HTTP publication response dropped after commit; original identity restored precise immutable version, no second revision; all SDK definition routes exercised") }