@@ -2,6 +2,7 @@ package program
22
33import (
44 "reflect"
5+ "strings"
56 "testing"
67
78 "github.com/stretchr/testify/assert"
@@ -18,9 +19,16 @@ import (
1819// reflection-based struct marshaler (marshalInputOptionsImpl, go/pulumi/rpc.go)
1920// silently DROPS any exported struct field that has no `pulumi:"..."` tag —
2021// yaml/json tags alone are not enough. Every field of every type reachable from
21- // an Export call in program.Run must therefore carry a non-empty `pulumi` tag;
22- // this test asserts that mechanically so a new field (or a new descriptor type)
23- // can't reintroduce the silent-empty-export failure mode.
22+ // an Export call in program.Run must therefore carry a non-empty `pulumi` tag.
23+ //
24+ // The tag's NAME matters too, and is asserted here as a second invariant: the
25+ // read side (cmd/inforge's decodeTargets) unmarshals the exported JSON via
26+ // encoding/json, which matches on the `json:` tag — so if `pulumi:"hostDns"`
27+ // (idiomatic Pulumi camelCase) diverges from `json:"host_dns"`, the export
28+ // itself succeeds but every read decodes that field back to its zero value.
29+ // This exact mismatch shipped once (HostDNS et al. exported under "hostDns"
30+ // but decoded looking for "host_dns"), and broke `inforge releases deploy`
31+ // with an empty SSH hostname even after the empty-export bug was fixed.
2432func TestDeployDescriptorsCarryPulumiTags (t * testing.T ) {
2533 types := []any {
2634 service.DeployDescriptor {}, service.DeployTarget {},
@@ -32,8 +40,13 @@ func TestDeployDescriptorsCarryPulumiTags(t *testing.T) {
3240 typ := reflect .TypeOf (v )
3341 for i := 0 ; i < typ .NumField (); i ++ {
3442 f := typ .Field (i )
35- tag := f .Tag .Get ("pulumi" )
36- assert .NotEmpty (t , tag , "%s.%s has no `pulumi:\" ...\" ` struct tag — pulumi.Any() silently drops it from the export" , typ .Name (), f .Name )
43+ pulumiTag := f .Tag .Get ("pulumi" )
44+ assert .NotEmpty (t , pulumiTag , "%s.%s has no `pulumi:\" ...\" ` struct tag — pulumi.Any() silently drops it from the export" , typ .Name (), f .Name )
45+
46+ jsonTag , _ , _ := strings .Cut (f .Tag .Get ("json" ), "," )
47+ assert .Equal (t , jsonTag , pulumiTag ,
48+ "%s.%s: pulumi tag %q must match json tag %q — decodeTargets unmarshals via json, so a mismatch silently zeroes this field on read even though the export itself succeeds" ,
49+ typ .Name (), f .Name , pulumiTag , jsonTag )
3750 }
3851 }
3952}
0 commit comments