azure/migrate-json-golden-tests
**WORKFLOW SKILL** — Migrate JSON-based golden file test scenarios (TestGolden) to programmatic Go unit tests in the pipeline package. USE FOR: converting testdata/<GroupName>/*.json scenarios into Go tests that construct types with astmodel and run specific pipeline stages. DO NOT USE FOR: adding new test scenarios from scratch, fixing existing Go unit tests, or code review.
npx skills add https://github.com/Azure/azure-service-operator --skill migrate-json-golden-tests
This skill guides the migration of JSON-based golden file test scenarios (run by TestGolden in golden_files_test.go) into programmatic Go unit tests in the pipeline package. The new tests construct types using astmodel APIs and run specific pipeline stages, providing focused, maintainable unit tests.
v2/tools/generator/internal/codegen/golden_files_test.go):testdata/<GroupName>/ directories define schemasconfig.yaml specifies options (hasArmResources, pipelines)v2/tools/generator/internal/codegen/pipeline/create_arm_types_test.go):astmodel APIs and test helperstest.AssertPackagesGenerateExpectedCode() for golden file comparisonpipeline/testdata/<TestFuncName>/ directoriesThe unit tests are preferred because they are faster, more focused, and don't depend on the JSON schema scanner.
ArmResource) to migrate.v2/tools/generator/internal/codegen/testdata/<GroupName>/..json files in v2/tools/generator/internal/codegen/testdata/<GroupName>/.config.yaml in the same directory to understand test configuration:hasArmResources: true/false — determines which pipeline stages runpipelines: — which pipelines to test (azure, crossplane)resourceDefinitions section)definitions section)required arrays)Tests always go in a *_test.go file in the pipeline package (v2/tools/generator/internal/codegen/pipeline/), typically alongside the pipeline stage being tested.
To determine the specific file:
config.yaml to understand what features the scenarios exercise (e.g., hasArmResources: true suggests ARM type creation is involved).create_arm_types_test.go<stage_name>_test.go fileBefore creating new tests, check the target test file for tests that already cover the same scenarios:
For each JSON scenario that needs a new test, create a Go test function.
| JSON Schema Pattern | astmodel Equivalent |
|---|---|
| "type": "string" | astmodel.StringType |
| "type": "integer" | astmodel.IntType |
| "type": "boolean" | astmodel.BoolType |
| "type": "object" with no properties | astmodel.NewMapType(astmodel.StringType, astmodel.AnyType) |
| {} (empty schema) | astmodel.AnyType |
| "type": "array", "items": X | astmodel.NewArrayType(X) |
| "additionalProperties": X | astmodel.NewMapType(astmodel.StringType, X) |
| "$ref": "#/definitions/Foo" | Reference the TypeName of the Foo definition |
| "enum": [values] | astmodel.NewEnumType(baseType, astmodel.MakeEnumValue(id, \"value"\)...) — value must be backtick-quoted |
| "oneOf": [refs] | Object with optional properties for each variant + astmodel.OneOfFlag.ApplyTo() |
The JSON schema scanner wraps ALL property types in optional and then annotates required ones:
// Required property:
prop := astmodel.NewPropertyDefinition("Name", "name", SomeType).MakeTypeOptional().MakeRequired()
// Optional property:
prop := astmodel.NewPropertyDefinition("Name", "name", SomeType).MakeTypeOptional()
// or equivalently:
prop := astmodel.NewPropertyDefinition("Name", "name", astmodel.NewOptionalType(SomeType))
IMPORTANT: MakeRequired() panics if the property type is not already optional. Always call MakeTypeOptional() first.
For ARM resources (the common case):
spec := test.CreateSpec(test.Pkg2020, "ResourceName", properties...)
status := test.CreateStatus(test.Pkg2020, "ResourceName")
resource := test.CreateARMResource(test.Pkg2020, "ResourceName", spec, status, test.Pkg2020APIVersion)
defs := make(astmodel.TypeDefinitionSet)
defs.AddAll(resource, status, spec, /* other type defs... */ test.Pkg2020APIVersion)
For resources with a nested properties object (common ARM pattern):
propsObj := test.CreateObjectDefinition(pkg, "ResourceNameProperties", prop1, prop2, ...)
propsProp := astmodel.NewPropertyDefinition("Properties", "properties", propsObj.Name()).MakeTypeOptional()
spec := test.CreateSpec(pkg, "ResourceName", test.NameProperty, propsProp)
For resource ownership (parent-child relationships):
resourceBRT, _ := astmodel.AsResourceType(resourceB.Type())
resourceB = resourceB.WithType(resourceBRT.WithOwner(resourceA.Name()))
Determine which pipeline stages to run by examining what the existing tests in the target file use. Look at the other tests already in the file to understand the standard pattern for that stage, then replicate it for your new tests.
For example, create_arm_types_test.go uses this standard set:
state, err := RunTestPipeline(
NewState(defs),
CreateARMTypes(cfg, idFactory, logr.Discard()),
ApplyARMConversionInterface(idFactory, cfg),
SimplifyDefinitions(),
StripUnreferencedTypeDefinitions(),
)
If the scenario exercises additional features, add the relevant stages. Look at existing tests in the same file for examples of how to include stages for specific features:
| Scenario | Additional Stages |
|---|---|
| Resource references | Configure OMC with ReferenceType.Set(config.ReferenceTypeARM) per property, then add ApplyCrossResourceReferencesFromConfig(configuration, logr.Discard()) and TransformCrossResourceReferences(configuration, idFactory) before the core stages. |
| Config maps | Add AddConfigMaps(configuration) before the core stages |
| Secrets | Add AddSecrets(configuration) before the core stages |
| Flattening | Add FlattenProperties(logr.Discard()) after the core stages |
| JSON/Any type fields | Add ReplaceAnyTypeWithJSON() before the core stages |
| OneOf | Standard stages work; the type must have OneOfFlag applied via ApplyObjectTransformation |
For resource references, use standard pipeline stages with OMC configuration — do NOT write custom test helper functions:
omc := config.NewObjectModelConfiguration()
g.Expect(
omc.ModifyProperty(
specProperties.Name(),
someProperty.PropertyName(),
func(pc *config.PropertyConfiguration) error {
pc.ReferenceType.Set(config.ReferenceTypeARM)
return nil
},
),
).To(Succeed())
configuration := config.NewConfiguration()
configuration.ObjectModelConfiguration = omc
state, err := RunTestPipeline(
NewState(defs),
ApplyCrossResourceReferencesFromConfig(configuration, logr.Discard()),
TransformCrossResourceReferences(configuration, idFactory),
CreateARMTypes(omc, idFactory, logr.Discard()),
ApplyARMConversionInterface(idFactory, omc),
SimplifyDefinitions(),
StripUnreferencedTypeDefinitions(),
)
g.Expect(err).ToNot(HaveOccurred())
test.AssertPackagesGenerateExpectedCode(t, state.Definitions())
TestCreateARMTypes_SimpleResourceMapProperties // TestCreateARMTypes_SimpleResourceMapProperties tests that an ARM resource with various map property
// types (maps of objects, maps of maps, maps of arrays, maps of enums, maps of strings) generates
// correct ARM types and conversions.
Run the new tests with -update to create golden files:
cd v2/tools/generator
go test ./internal/codegen/pipeline/ -run "TestName1|TestName2|..." -update -v
Then verify they pass without -update:
go test ./internal/codegen/pipeline/ -run "TestName1|TestName2|..." -v
Also run all existing tests to confirm no regressions:
go test ./internal/codegen/pipeline/ -run "TestCreate" -v
Generate a comparison report between the new pipeline test golden files and the old JSON test golden files. The expected differences fall into two categories:
Expected/benign differences (present in all scenarios due to running fewer pipeline stages):
person (from test.Pkg2020), old uses test (from JSON schema URL)"v2020", old uses the version from JSON schema (e.g., "2020-01-01")Status string field, old may differ// Generated from: comments: Absent in new, present in oldColor_blue = Color(blue), old uses Color_Blue = Color("blue")Potentially meaningful differences to investigate:
interface{} instead of a typed wrapper), look for a transformation stage that converts between representations and add it to the pipeline.Run the comparison:
for scenario in "NewTestName:old_json_name"; do
new_name="${scenario%%:*}"
old_name="${scenario##*:}"
diff "pipeline/testdata/${new_name}/person-v20200101-arm.golden" \
"testdata/<GroupName>/${old_name}_azure_arm.golden"
done
Present the report to the user and get confirmation before proceeding.
Once the user confirms the comparison looks good:
rm -rf v2/tools/generator/internal/codegen/testdata/<GroupName>/
Then verify:
go test ./internal/codegen/ -run "TestGolden" -v -count=1
go test ./internal/codegen/pipeline/ -v -count=1
test (v2/tools/generator/internal/test/)test.Pkg2020, test.Pkg2021, test.Pkg2022 — package referencestest.Pkg2020APIVersion — API version enum definitiontest.NameProperty, test.FullNameProperty, test.FamilyNameProperty, test.KnownAsProperty, test.RestrictedNameProperty — reusable propertiestest.CreateSpec(), test.CreateStatus(), test.CreateARMResource(), test.CreateResource() — resource builderstest.CreateObjectDefinition() — creates an ObjectType with propertiestest.AssertPackagesGenerateExpectedCode() — golden file assertiontest.CreateFolderForTest() — option for subtests needing unique golden file foldersastmodel (v2/tools/generator/internal/astmodel/)astmodel.NewPropertyDefinition(name, jsonName, type) — with .MakeTypeOptional(), .MakeRequired(), .WithDescription()astmodel.NewEnumType(baseType, values...), astmodel.MakeEnumValue(id, value) — enums. The value parameter is a literal Go expression that appears in generated code; for string enums it must include quotes: astmodel.MakeEnumValue("blue", "blue") astmodel.NewArrayType(element) — arraysastmodel.NewMapType(key, value) — mapsastmodel.NewOptionalType(element) — optional wrapperastmodel.OneOfFlag.ApplyTo(objectType) — oneOf flagastmodel.MakeTypeDefinition(name, type) — type definitionastmodel.MakeInternalTypeName(pkg, name) — type nameastmodel.AsResourceType(type) — cast to resource type (for ownership)resourceType.WithOwner(ownerName) — set resource ownershipv2/tools/generator/internal/codegen/pipeline/)RunTestPipeline(state, stages...) — run pipeline stages in sequenceNewState(defs) — create initial stateCreateARMTypes(), ApplyARMConversionInterface() — core ARM stagesSimplifyDefinitions(), StripUnreferencedTypeDefinitions() — cleanup stagesFlattenProperties() — flattening stageReplaceAnyTypeWithJSON() — converts interface{} to v1.JSONApplyCrossResourceReferencesFromConfig(), TransformCrossResourceReferences() — resource reference stagesAddConfigMaps(), AddSecrets() — configmap/secret stagesMakeRequired() panics if the property type is not already optional — always call MakeTypeOptional() first.ApplyCrossResourceReferencesFromConfig and TransformCrossResourceReferences pipeline stages with OMC configuration.ResourceType.WithOwner() which takes InternalTypeName, not a string pointer.MakeEnumValue value parameter must be quoted for string enums — The second argument to MakeEnumValue is a literal Go expression emitted in generated code. For string enums, wrap the value in backtick quotes: astmodel.MakeEnumValue("blue", "blue") . Without quotes, the generated code will reference undefined identifiers (e.g., Color(blue) instead of Color("blue")).Take azure/migrate-json-golden-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.