From c0f3da3ad74ae792c45b9e437cfc74a14278ac6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=B9=E8=AE=A9=E4=BB=96=E4=B9=9F=E8=AE=A9?= <63493446+trtyr@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:18:55 +0800 Subject: [PATCH] fix(runs): decode revision spec with protojson to preserve oneof fields DecodeRevisionSpec used encoding/json to unmarshal the persisted project revision spec into the generated proto message. encoding/json does not understand proto oneof fields, so DriverSpec.config (and any other oneof) was silently dropped. A GetProject -> PatchProject round-trip then failed with "driver requires exactly one runtime config" because the returned driver carried only its name. Switch to protojson.Unmarshal with DiscardUnknown so oneof fields decode correctly while the legacy canonical workspace fields (still handled by restoreCanonicalRevisionWorkspaces) are tolerated. --- pkg/runs/preparation.go | 5 +++-- pkg/runs/preparation_driver_oneof_test.go | 25 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 pkg/runs/preparation_driver_oneof_test.go diff --git a/pkg/runs/preparation.go b/pkg/runs/preparation.go index a77c4e8d..b0430490 100644 --- a/pkg/runs/preparation.go +++ b/pkg/runs/preparation.go @@ -2,7 +2,6 @@ package runs import ( "context" - "encoding/json" "fmt" "os" "path/filepath" @@ -15,6 +14,7 @@ import ( "agent-compose/pkg/projects" "agent-compose/pkg/storage/sandboxstore" agentcomposev2 "agent-compose/proto/agentcompose/v2" + "google.golang.org/protobuf/encoding/protojson" ) type PreparationStore interface { @@ -151,7 +151,8 @@ func DecodeRevisionSpec(raw string) (*agentcomposev2.ProjectSpec, error) { if err != nil { return nil, fmt.Errorf("decode project revision spec: %w", err) } - if err := json.Unmarshal(normalizedData, &spec); err != nil { + opts := protojson.UnmarshalOptions{DiscardUnknown: true} + if err := opts.Unmarshal(normalizedData, &spec); err != nil { return nil, fmt.Errorf("decode project revision spec: %w", err) } if err := restoreCanonicalRevisionWorkspaces(data, &spec); err != nil { diff --git a/pkg/runs/preparation_driver_oneof_test.go b/pkg/runs/preparation_driver_oneof_test.go new file mode 100644 index 00000000..b246a74d --- /dev/null +++ b/pkg/runs/preparation_driver_oneof_test.go @@ -0,0 +1,25 @@ +package runs + +import ( + "testing" + + agentcomposev2 "agent-compose/proto/agentcompose/v2" +) + +// DecodeRevisionSpec must preserve oneof fields (e.g. driver runtime config). +// Regression test for the GetProject → PatchProject round-trip where a driver +// decoded via encoding/json lost its config oneof, so PatchProject rejected the +// returned spec with "driver requires exactly one runtime config". +func TestDecodeRevisionSpecPreservesDriverRuntimeConfig(t *testing.T) { + spec, err := DecodeRevisionSpec(`{"name":"demo","agents":[{"name":"worker","driver":{"name":"docker","docker":{}}}]}`) + if err != nil { + t.Fatalf("DecodeRevisionSpec returned error: %v", err) + } + driver := spec.GetAgents()[0].GetDriver() + if driver.GetName() != "docker" { + t.Fatalf("driver name = %q, want docker", driver.GetName()) + } + if _, ok := driver.GetConfig().(*agentcomposev2.DriverSpec_Docker); !ok { + t.Fatalf("driver config = %T, want *agentcomposev2.DriverSpec_Docker", driver.GetConfig()) + } +}