From fb3412c380692f2684cdf3956ddfbdcebbe01dd5 Mon Sep 17 00:00:00 2001 From: "kevin.new" Date: Tue, 16 Jun 2026 21:05:00 +0000 Subject: [PATCH] feat(runway): add domain entities and topic keys (#2) Scaffold the runway domain with entity types for the check and land queues, plus pipeline topic key constants. Runway reuses the shared entity/change and entity/mergestrategy packages rather than defining its own strategy type. Co-Authored-By: Claude Opus 4.6 (1M context) --- runway/core/BUILD.bazel | 8 ++ runway/core/core.go | 20 ++++ runway/core/topickey/BUILD.bazel | 9 ++ runway/core/topickey/topickey.go | 32 ++++++ runway/entity/BUILD.bazel | 31 ++++++ runway/entity/check.go | 87 +++++++++++++++ runway/entity/check_test.go | 152 ++++++++++++++++++++++++++ runway/entity/entity.go | 16 +++ runway/entity/job.go | 123 +++++++++++++++++++++ runway/entity/job_test.go | 176 +++++++++++++++++++++++++++++++ 10 files changed, 654 insertions(+) create mode 100644 runway/core/BUILD.bazel create mode 100644 runway/core/core.go create mode 100644 runway/core/topickey/BUILD.bazel create mode 100644 runway/core/topickey/topickey.go create mode 100644 runway/entity/BUILD.bazel create mode 100644 runway/entity/check.go create mode 100644 runway/entity/check_test.go create mode 100644 runway/entity/entity.go create mode 100644 runway/entity/job.go create mode 100644 runway/entity/job_test.go diff --git a/runway/core/BUILD.bazel b/runway/core/BUILD.bazel new file mode 100644 index 00000000..8968ec3f --- /dev/null +++ b/runway/core/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "core", + srcs = ["core.go"], + importpath = "github.com/uber/submitqueue/runway/core", + visibility = ["//visibility:public"], +) diff --git a/runway/core/core.go b/runway/core/core.go new file mode 100644 index 00000000..f3b872dc --- /dev/null +++ b/runway/core/core.go @@ -0,0 +1,20 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package core groups infrastructure shared across Runway's own services — +// the Runway-scoped analogue of the repo-level core/. Cross-domain +// infrastructure lives in the top-level core/; this package is for plumbing +// private to Runway. Subpackages are added here as shared needs emerge, +// mirroring submitqueue/core. +package core diff --git a/runway/core/topickey/BUILD.bazel b/runway/core/topickey/BUILD.bazel new file mode 100644 index 00000000..9aa38b1a --- /dev/null +++ b/runway/core/topickey/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "topickey", + srcs = ["topickey.go"], + importpath = "github.com/uber/submitqueue/runway/core/topickey", + visibility = ["//visibility:public"], + deps = ["//core/consumer"], +) diff --git a/runway/core/topickey/topickey.go b/runway/core/topickey/topickey.go new file mode 100644 index 00000000..6c177596 --- /dev/null +++ b/runway/core/topickey/topickey.go @@ -0,0 +1,32 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package topickey defines Runway pipeline stage identifiers. +package topickey + +import "github.com/uber/submitqueue/core/consumer" + +// TopicKey is the shared pipeline stage identifier type. +type TopicKey = consumer.TopicKey + +const ( + // TopicKeyCheck is the inbound topic where mergeability check requests arrive from SubmitQueue. + TopicKeyCheck TopicKey = "check" + // TopicKeyLand is the inbound topic where batch land jobs arrive from SubmitQueue. + TopicKeyLand TopicKey = "land" + // TopicKeyCheckResult is the outbound topic where check results are published back to SubmitQueue. + TopicKeyCheckResult TopicKey = "checkresult" + // TopicKeyLandResult is the outbound topic where land results are published back to SubmitQueue. + TopicKeyLandResult TopicKey = "landresult" +) diff --git a/runway/entity/BUILD.bazel b/runway/entity/BUILD.bazel new file mode 100644 index 00000000..c13b0eec --- /dev/null +++ b/runway/entity/BUILD.bazel @@ -0,0 +1,31 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "entity", + srcs = [ + "check.go", + "entity.go", + "job.go", + ], + importpath = "github.com/uber/submitqueue/runway/entity", + visibility = ["//visibility:public"], + deps = [ + "//entity/change", + "//entity/mergestrategy", + ], +) + +go_test( + name = "entity_test", + srcs = [ + "check_test.go", + "job_test.go", + ], + embed = [":entity"], + deps = [ + "//entity/change", + "//entity/mergestrategy", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/runway/entity/check.go b/runway/entity/check.go new file mode 100644 index 00000000..e800b1c3 --- /dev/null +++ b/runway/entity/check.go @@ -0,0 +1,87 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "encoding/json" + + "github.com/uber/submitqueue/entity/change" + "github.com/uber/submitqueue/entity/mergestrategy" +) + +// Check is the inbound message on the runway-check topic. SubmitQueue publishes +// one Check per request to determine whether the request's changes can merge +// cleanly against the target branch. The check is read-only — it does not +// mutate the target branch or any external state. +type Check struct { + // Queue is the SubmitQueue queue name. + Queue string `json:"queue"` + // RequestID is the SubmitQueue request ID. Serves as the idempotency key. + RequestID string `json:"request_id"` + // Repo identifies the repository (e.g., "uber/submitqueue"). + Repo string `json:"repo"` + // TargetBranch is the destination branch (e.g., "main"). + TargetBranch string `json:"target_branch"` + // Changes is the set of code changes to check for mergeability. + Changes []change.Change `json:"changes"` + // Strategy is the landing strategy that would be used to land these changes. + Strategy mergestrategy.MergeStrategy `json:"strategy"` +} + +// ToBytes serializes the Check to JSON bytes for queue message payload. +func (c Check) ToBytes() ([]byte, error) { + return json.Marshal(c) +} + +// CheckFromBytes deserializes a Check from JSON bytes. +func CheckFromBytes(data []byte) (Check, error) { + var c Check + err := json.Unmarshal(data, &c) + return c, err +} + +// MergeabilityResult describes whether a single change can be applied cleanly +// to the target branch. +type MergeabilityResult struct { + // Change is the input change this result corresponds to. + Change change.Change `json:"change"` + // Mergeable is true if the change can be applied cleanly. + Mergeable bool `json:"mergeable"` + // Reason is a human-readable explanation when Mergeable is false; empty when true. + Reason string `json:"reason,omitempty"` +} + +// CheckResult is the outbound message published to the sq-check-result topic. +// It carries per-change mergeability detail back to SubmitQueue. +type CheckResult struct { + // Queue is the SubmitQueue queue name (partition key for the outbound topic). + Queue string `json:"queue"` + // RequestID correlates to Check.RequestID. + RequestID string `json:"request_id"` + // Results is one entry per change in the input Check.Changes. + Results []MergeabilityResult `json:"results"` +} + +// ToBytes serializes the CheckResult to JSON bytes for queue message payload. +func (r CheckResult) ToBytes() ([]byte, error) { + return json.Marshal(r) +} + +// CheckResultFromBytes deserializes a CheckResult from JSON bytes. +func CheckResultFromBytes(data []byte) (CheckResult, error) { + var r CheckResult + err := json.Unmarshal(data, &r) + return r, err +} diff --git a/runway/entity/check_test.go b/runway/entity/check_test.go new file mode 100644 index 00000000..2c23130f --- /dev/null +++ b/runway/entity/check_test.go @@ -0,0 +1,152 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber/submitqueue/entity/change" + "github.com/uber/submitqueue/entity/mergestrategy" +) + +func TestCheck_SerializationRoundTrip(t *testing.T) { + tests := []struct { + name string + check Check + }{ + { + name: "single change single URI", + check: Check{ + Queue: "go-code-main", + RequestID: "go-code-main/42", + Repo: "uber/submitqueue", + TargetBranch: "main", + Changes: []change.Change{{URIs: []string{"github://uber/submitqueue/pull/123/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}}, + Strategy: mergestrategy.MergeStrategyRebase, + }, + }, + { + name: "multiple changes", + check: Check{ + Queue: "queue1", + RequestID: "queue1/100", + Repo: "uber/repo-a", + TargetBranch: "main", + Changes: []change.Change{ + {URIs: []string{"github://uber/repo-a/pull/101/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, + {URIs: []string{"github://uber/repo-a/pull/102/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}, + }, + Strategy: mergestrategy.MergeStrategySquashRebase, + }, + }, + { + name: "stacked diff with multiple URIs", + check: Check{ + Queue: "queue2", + RequestID: "queue2/200", + Repo: "uber/submitqueue", + TargetBranch: "release", + Changes: []change.Change{{URIs: []string{ + "github://uber/submitqueue/pull/10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "github://uber/submitqueue/pull/11/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }}}, + Strategy: mergestrategy.MergeStrategyMerge, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.check.ToBytes() + require.NoError(t, err) + + deserialized, err := CheckFromBytes(data) + require.NoError(t, err) + + assert.Equal(t, tt.check, deserialized) + }) + } +} + +func TestCheckFromBytes_InvalidJSON(t *testing.T) { + _, err := CheckFromBytes([]byte(`{"invalid": json"}`)) + assert.Error(t, err) +} + +func TestCheckFromBytes_EmptyData(t *testing.T) { + c, err := CheckFromBytes([]byte(`{}`)) + require.NoError(t, err) + + assert.Empty(t, c.Queue) + assert.Empty(t, c.RequestID) + assert.Equal(t, mergestrategy.MergeStrategyUnknown, c.Strategy) +} + +func TestCheckResult_SerializationRoundTrip(t *testing.T) { + tests := []struct { + name string + result CheckResult + }{ + { + name: "all mergeable", + result: CheckResult{ + Queue: "go-code-main", + RequestID: "go-code-main/42", + Results: []MergeabilityResult{ + {Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/1/aaaa"}}, Mergeable: true}, + {Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/2/bbbb"}}, Mergeable: true}, + }, + }, + }, + { + name: "some unmergeable", + result: CheckResult{ + Queue: "queue1", + RequestID: "queue1/100", + Results: []MergeabilityResult{ + {Change: change.Change{URIs: []string{"github://uber/repo/pull/1/aaaa"}}, Mergeable: true}, + {Change: change.Change{URIs: []string{"github://uber/repo/pull/2/bbbb"}}, Mergeable: false, Reason: "conflicts with src/main.go"}, + }, + }, + }, + { + name: "empty results", + result: CheckResult{ + Queue: "queue2", + RequestID: "queue2/200", + Results: []MergeabilityResult{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.result.ToBytes() + require.NoError(t, err) + + deserialized, err := CheckResultFromBytes(data) + require.NoError(t, err) + + assert.Equal(t, tt.result, deserialized) + }) + } +} + +func TestCheckResultFromBytes_InvalidJSON(t *testing.T) { + _, err := CheckResultFromBytes([]byte(`not json`)) + assert.Error(t, err) +} diff --git a/runway/entity/entity.go b/runway/entity/entity.go new file mode 100644 index 00000000..3f8abad2 --- /dev/null +++ b/runway/entity/entity.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package entity holds Runway-specific domain types (distinct from shared repo entity/). +package entity diff --git a/runway/entity/job.go b/runway/entity/job.go new file mode 100644 index 00000000..8f194baf --- /dev/null +++ b/runway/entity/job.go @@ -0,0 +1,123 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "encoding/json" + + "github.com/uber/submitqueue/entity/change" + "github.com/uber/submitqueue/entity/mergestrategy" +) + +// ResultStatus defines the possible outcomes of a landing job. +type ResultStatus string + +const ( + // ResultStatusUnknown is the unreachable zero value, set by default when + // the structure is initialized. It should never be seen in the system. + ResultStatusUnknown ResultStatus = "" + // ResultStatusSucceeded means all items in the job were landed successfully. + ResultStatusSucceeded ResultStatus = "succeeded" + // ResultStatusConflict means pre-validation detected a merge conflict. + ResultStatusConflict ResultStatus = "conflict" + // ResultStatusError means an infrastructure failure prevented landing. + ResultStatusError ResultStatus = "error" +) + +// Job is the inbound message on the runway-land topic. SubmitQueue publishes +// one Job per batch to land a scored batch. The job carries the resolved content +// for each request in the batch — runway has no access to SubmitQueue's request +// store, so the message must be self-contained. +type Job struct { + // ID is the unique job identifier (idempotency key). + ID string `json:"id"` + // BatchID is the SubmitQueue batch ID for correlation. + BatchID string `json:"batch_id"` + // Queue is the SubmitQueue queue name. + Queue string `json:"queue"` + // Repo identifies the repository (e.g., "uber/submitqueue"). + Repo string `json:"repo"` + // TargetBranch is the destination branch (e.g., "main"). + TargetBranch string `json:"target_branch"` + // Items is the per-request content, in landing order. + Items []JobItem `json:"items"` +} + +// ToBytes serializes the Job to JSON bytes for queue message payload. +func (j Job) ToBytes() ([]byte, error) { + return json.Marshal(j) +} + +// JobFromBytes deserializes a Job from JSON bytes. +func JobFromBytes(data []byte) (Job, error) { + var j Job + err := json.Unmarshal(data, &j) + return j, err +} + +// JobItem carries one request's resolved content within a Job. +type JobItem struct { + // RequestID is the SubmitQueue request ID for correlation. + RequestID string `json:"request_id"` + // Change is the code change to land. + Change change.Change `json:"change"` + // Strategy is the per-request landing strategy. + Strategy mergestrategy.MergeStrategy `json:"strategy"` +} + +// Outcome describes what happened to a single item within a successful landing. +type Outcome struct { + // RequestID is the SubmitQueue request ID for correlation. + RequestID string `json:"request_id"` + // Change is the input change this outcome corresponds to. + Change change.Change `json:"change"` + // CommitSHAs lists the commits produced on the target branch, in apply order. + // Empty when AlreadyExisted is true. + CommitSHAs []string `json:"commit_shas"` + // AlreadyExisted is true if the change was already present on the target + // branch (no new commits were produced). + AlreadyExisted bool `json:"already_existed"` +} + +// Result is the outbound message published to the sq-land-result topic. +// It carries the landing outcome back to SubmitQueue. +type Result struct { + // JobID correlates to Job.ID. + JobID string `json:"job_id"` + // BatchID is the SubmitQueue batch ID. + BatchID string `json:"batch_id"` + // Queue is the SubmitQueue queue name (partition key for the outbound topic). + Queue string `json:"queue"` + // Status is the overall landing outcome. + Status ResultStatus `json:"status"` + // Outcomes is one entry per item, in landing order. Populated when Status + // is ResultStatusSucceeded. + Outcomes []Outcome `json:"outcomes,omitempty"` + // Error is a human-readable description of the failure. Populated when + // Status is ResultStatusConflict or ResultStatusError. + Error string `json:"error,omitempty"` +} + +// ToBytes serializes the Result to JSON bytes for queue message payload. +func (r Result) ToBytes() ([]byte, error) { + return json.Marshal(r) +} + +// ResultFromBytes deserializes a Result from JSON bytes. +func ResultFromBytes(data []byte) (Result, error) { + var r Result + err := json.Unmarshal(data, &r) + return r, err +} diff --git a/runway/entity/job_test.go b/runway/entity/job_test.go new file mode 100644 index 00000000..dca72c93 --- /dev/null +++ b/runway/entity/job_test.go @@ -0,0 +1,176 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber/submitqueue/entity/change" + "github.com/uber/submitqueue/entity/mergestrategy" +) + +func TestJob_SerializationRoundTrip(t *testing.T) { + tests := []struct { + name string + job Job + }{ + { + name: "single item rebase", + job: Job{ + ID: "job-001", + BatchID: "go-code-main/batch/1", + Queue: "go-code-main", + Repo: "uber/submitqueue", + TargetBranch: "main", + Items: []JobItem{ + { + RequestID: "go-code-main/42", + Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/123/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, + Strategy: mergestrategy.MergeStrategyRebase, + }, + }, + }, + }, + { + name: "multiple items mixed strategies", + job: Job{ + ID: "job-002", + BatchID: "queue1/batch/5", + Queue: "queue1", + Repo: "uber/repo-a", + TargetBranch: "main", + Items: []JobItem{ + { + RequestID: "queue1/10", + Change: change.Change{URIs: []string{"github://uber/repo-a/pull/10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, + Strategy: mergestrategy.MergeStrategyRebase, + }, + { + RequestID: "queue1/11", + Change: change.Change{URIs: []string{"github://uber/repo-a/pull/11/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}, + Strategy: mergestrategy.MergeStrategySquashRebase, + }, + { + RequestID: "queue1/12", + Change: change.Change{URIs: []string{"github://uber/repo-a/pull/12/cccccccccccccccccccccccccccccccccccccccc"}}, + Strategy: mergestrategy.MergeStrategyMerge, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.job.ToBytes() + require.NoError(t, err) + + deserialized, err := JobFromBytes(data) + require.NoError(t, err) + + assert.Equal(t, tt.job, deserialized) + }) + } +} + +func TestJobFromBytes_InvalidJSON(t *testing.T) { + _, err := JobFromBytes([]byte(`{"invalid": json"}`)) + assert.Error(t, err) +} + +func TestJobFromBytes_EmptyData(t *testing.T) { + j, err := JobFromBytes([]byte(`{}`)) + require.NoError(t, err) + + assert.Empty(t, j.ID) + assert.Empty(t, j.BatchID) + assert.Empty(t, j.Queue) + assert.Nil(t, j.Items) +} + +func TestResult_SerializationRoundTrip(t *testing.T) { + tests := []struct { + name string + result Result + }{ + { + name: "succeeded with outcomes", + result: Result{ + JobID: "job-001", + BatchID: "go-code-main/batch/1", + Queue: "go-code-main", + Status: ResultStatusSucceeded, + Outcomes: []Outcome{ + { + RequestID: "go-code-main/42", + Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/123/aaaa"}}, + CommitSHAs: []string{"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, + }, + { + RequestID: "go-code-main/43", + Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/124/bbbb"}}, + AlreadyExisted: true, + }, + }, + }, + }, + { + name: "conflict", + result: Result{ + JobID: "job-002", + BatchID: "queue1/batch/5", + Queue: "queue1", + Status: ResultStatusConflict, + Error: "item queue1/11 conflicts with src/main.go", + }, + }, + { + name: "error", + result: Result{ + JobID: "job-003", + BatchID: "queue2/batch/10", + Queue: "queue2", + Status: ResultStatusError, + Error: "git push failed: remote rejected", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.result.ToBytes() + require.NoError(t, err) + + deserialized, err := ResultFromBytes(data) + require.NoError(t, err) + + assert.Equal(t, tt.result, deserialized) + }) + } +} + +func TestResultFromBytes_InvalidJSON(t *testing.T) { + _, err := ResultFromBytes([]byte(`not json`)) + assert.Error(t, err) +} + +func TestResultStatus_Values(t *testing.T) { + assert.Equal(t, ResultStatus(""), ResultStatusUnknown) + assert.Equal(t, ResultStatus("succeeded"), ResultStatusSucceeded) + assert.Equal(t, ResultStatus("conflict"), ResultStatusConflict) + assert.Equal(t, ResultStatus("error"), ResultStatusError) +}