From ad6b1850922cdc2b76f75e643134fc63b51febc4 Mon Sep 17 00:00:00 2001 From: ShocOne <62835948+ShocOne@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:37:51 +0100 Subject: [PATCH] fix: a probe invents the name it must be able to clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A name must be unique per run and must carry the prefix cleanup matches on, so the invented token now beats a declared example in both synthesis paths. An example is a value the API accepted once: an API that requires a name to be unique refuses it every run after, and the object the first run created under it carries no prefix, so the cleanup pass cannot see it and it stays in somebody's tenant for good. A field an enum, a format or a pattern constrains is left on the ordinary priority — the token satisfies none of those shapes. The plan and the strategized run change together: the live path synthesises from strategize, and a rule applied to one alone would mean the plan and the run disagreed about what they sent. nameBearing loses its duplicate with it, exported from the plan package the run already imports. Co-Authored-By: Claude Opus 5 (1M context) --- internal/audit/plan/synth.go | 17 +++++++++-- internal/audit/plan/synth_test.go | 42 ++++++++++++++++++++++++++ internal/audit/run/adjust_unit_test.go | 17 +++++++++++ internal/audit/run/strategize.go | 30 +++++++++--------- 4 files changed, 87 insertions(+), 19 deletions(-) diff --git a/internal/audit/plan/synth.go b/internal/audit/plan/synth.go index 0856eaf..65e1aff 100644 --- a/internal/audit/plan/synth.go +++ b/internal/audit/plan/synth.go @@ -95,6 +95,17 @@ func (sy synth) value(field string, s *specmodel.Schema, depth int, topLevel boo return nil, false } r := s.Resolved() + // A name must be unique per run and must carry the prefix cleanup matches + // on, so the invented token beats a declared example. An example is a + // value the API accepted once; an API that requires a name to be unique + // refuses it every time after, and an object created under it is invisible + // to the prefix pass and stays in the tenant. An enum, a format or a + // pattern leaves the ordinary priority alone: the token satisfies none of + // those shapes, so a field carrying one is not a field to invent a name + // for. + if NameBearing(field) && len(r.Enum) == 0 && r.Format == "" && r.Pattern == "" { + return sy.nameToken(field), true + } if r.Example != nil { return r.Example, true } @@ -139,7 +150,7 @@ func (sy synth) formatValue(format string) (any, bool) { func (sy synth) typeValue(field string, r *specmodel.Schema, depth int) (any, bool) { switch { case r.Type == "string": - if nameBearing(field) { + if NameBearing(field) { return sy.nameToken(field), true } return "sample-" + field, true @@ -268,9 +279,9 @@ func (sy synth) nameToken(field string) string { return sy.prefix + "-" + RunIDToken + "-" + sy.entity + "-" + field } -// nameBearing reports whether a string field names its object — the +// NameBearing reports whether a string field names its object — the // fields whose synthesized values must carry the cleanup prefix. -func nameBearing(field string) bool { +func NameBearing(field string) bool { lf := strings.ToLower(field) for _, suffix := range []string{"name", "title", "label"} { if lf == suffix || strings.HasSuffix(lf, suffix) { diff --git a/internal/audit/plan/synth_test.go b/internal/audit/plan/synth_test.go index 7df5ad9..9afe188 100644 --- a/internal/audit/plan/synth_test.go +++ b/internal/audit/plan/synth_test.go @@ -381,3 +381,45 @@ components: t.Errorf("minimal body carries the optional self-reference: %#v", body) } } + +func TestUnit_Plan_AnInventedNameOutranksADeclaredExample(t *testing.T) { + spec := resourceSpec( + "name, gated_name, shaped_name, summary", + ` name: + type: string + example: My thing + gated_name: + type: string + example: My thing + enum: [alpha, beta] + shaped_name: + type: string + example: my.thing + pattern: "^[a-z.]+$" + summary: + type: string + example: some words +`) + p := mustDerive(t, loadDoc(t, spec), testConfig(), nil) + body := entityByKey(t, p, "thing").Steps[0].Body + + // The example is a value the API took once; a unique name refuses it + // thereafter, and an object created under it never carries the prefix + // cleanup matches on. + if got := body["name"]; got != "tfpfgen-"+RunIDToken+"-thing-name" { + t.Errorf("name = %#v, want the invented token", got) + } + // A field an enum or a pattern constrains keeps the ordinary priority, in + // which a declared example already outranks both: the token satisfies + // neither shape, so such a field is not one to invent a name for. + if got := body["gated_name"]; got != "My thing" { + t.Errorf("gated_name = %#v, want the declared example", got) + } + if got := body["shaped_name"]; got != "my.thing" { + t.Errorf("shaped_name = %#v, want the declared example", got) + } + // A field that names nothing keeps its example. + if got := body["summary"]; got != "some words" { + t.Errorf("summary = %#v, want the declared example", got) + } +} diff --git a/internal/audit/run/adjust_unit_test.go b/internal/audit/run/adjust_unit_test.go index 3a5b369..700d75e 100644 --- a/internal/audit/run/adjust_unit_test.go +++ b/internal/audit/run/adjust_unit_test.go @@ -195,6 +195,23 @@ func TestUnit_Strategize_SynthesisHelpers(t *testing.T) { if v := synthValue(strategy.SynthHint{Field: "label", Type: "string"}, "ent", "tfpfgen"); v != "tfpfgen--ent-label" { t.Errorf("name-bearing string = %v", v) } + // A name-bearing field takes the invented token over a declared example: + // this is the path a live run synthesises from, and an API that requires a + // unique name refuses the example every run after the first. + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Example: "My thing"}, + "ent", "tfpfgen"); v != "tfpfgen--ent-name" { + t.Errorf("a name-bearing example = %#v, want the invented token", v) + } + // A field an enum, a format or a pattern constrains is not one to invent a + // name for, so the ordinary priority stands. + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Example: "My thing", + Pattern: "^[A-Za-z ]+$"}, "ent", "tfpfgen"); v != "My thing" { + t.Errorf("a constrained name = %#v, want the declared example", v) + } + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Format: "email"}, + "ent", "tfpfgen"); v != "tfpfgen-@example.invalid" { + t.Errorf("a formatted name = %#v, want the format-driven value", v) + } if v := synthValue(strategy.SynthHint{Field: "color", Type: "string"}, "ent", "p"); v != "sample-color" { t.Errorf("plain string = %v", v) } diff --git a/internal/audit/run/strategize.go b/internal/audit/run/strategize.go index 7a3e43f..4710673 100644 --- a/internal/audit/run/strategize.go +++ b/internal/audit/run/strategize.go @@ -21,7 +21,6 @@ import ( "fmt" "math/bits" "strconv" - "strings" "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/audit/observe" "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/audit/plan" @@ -336,7 +335,7 @@ func (r *runner) synthField(ent *entityState, field string) any { return synthValue(h, ent.plan.Entity, r.opts.NamePrefix) } } - if nameBearing(field) { + if plan.NameBearing(field) { return nameToken(r.opts.NamePrefix, ent.plan.Entity, field) } return "sample-" + field @@ -346,6 +345,17 @@ func (r *runner) synthField(ent *entityState, field string) any { // plan package's synthesis: example, then default, then the first enum member, // then a format-driven value, then a type-driven one. func synthValue(h strategy.SynthHint, entity, prefix string) any { + // A name must be unique per run and must carry the prefix cleanup matches + // on, so the invented token beats a declared example. An example is a + // value the API accepted once; an API that requires a name to be unique + // refuses it every time after, and an object created under it is invisible + // to the prefix pass and stays in the tenant. An enum, a format or a + // pattern leaves the ordinary priority alone: the token satisfies none of + // those shapes, so a field carrying one is not a field to invent a name + // for. + if plan.NameBearing(h.Field) && len(h.Enum) == 0 && h.Format == "" && h.Pattern == "" { + return nameToken(prefix, entity, h.Field) + } if h.Example != nil { return h.Example } @@ -370,12 +380,12 @@ func synthValue(h strategy.SynthHint, entity, prefix string) any { case "object": return map[string]any{} case "string": - if nameBearing(h.Field) { + if plan.NameBearing(h.Field) { return nameToken(prefix, entity, h.Field) } return "sample-" + h.Field default: - if nameBearing(h.Field) { + if plan.NameBearing(h.Field) { return nameToken(prefix, entity, h.Field) } return "sample-" + h.Field @@ -540,18 +550,6 @@ func nameToken(prefix, entity, field string) string { return prefix + "-" + plan.RunIDToken + "-" + entity + "-" + field } -// nameBearing reports whether a string field names its object — the fields -// whose synthesised values must carry the cleanup prefix. -func nameBearing(field string) bool { - lf := strings.ToLower(field) - for _, suffix := range []string{"name", "title", "label"} { - if lf == suffix || strings.HasSuffix(lf, suffix) { - return true - } - } - return false -} - // bisectionAllowance is the extra createMaximal attempts worth reserving to // halve the optional set down to one field, plus the retry that confirms it. func bisectionAllowance(optional int) int {