test(export): byte-stable golden snapshots over a synthetic state - #174
Merged
Merged
Conversation
`ct export tf` turns ct-state.<env>.json into .tf files plus import blocks, so a migration to the OpenTofu provider needs no re-adoption: curated keys and comments carry across and ids come from state. Rendering is pure (no fs, no network), matching src/coverage/report.ts — the golden test has to run against fixtures without touching disk. Resource types are churchtools_-prefixed: Terraform resolves a resource's provider from the segment before the first underscore, so a bare `campus` type would resolve to a provider named `campus`. Ids are stringified explicitly with no truthiness path anywhere. The Mainz campus is id 0, and dropping it would silently fail to import exactly one resource.
The fixture is synthetic deliberately: this repo is public, so no live campus, Bereich or status name belongs in it. The proof that the export matches a real estate runs in the private config repo, where that state already lives, as part of the migration gate. What the fixture does carry is every shape the renderer can trip on — id 0, a key that is not a valid HCL identifier (!3 Aktiv -> g_3_aktiv), all five resource types, and every camelCase field that maps to a snake_case attribute.
2000game
added this pull request to stack #175
September 16, 2026 09:54
Review of #173 turned up six ways `ct export tf` produced output that looked right and planned wrong. Each is now a failing-then-passing test in tests/export/export-tf.test.ts, which drives runExportTf end to end instead of re-implementing its loop. - Six managed types (group, group-role, security-level, age-group, target-group, relationship-type) were skipped in silence. They are counted, reported as EXPORT_TYPE_UNSUPPORTED warnings and printed after the success line. Warn, not fail: the export is still useful for the five mapped types, and a hard error would make the command unusable on every real estate. - imports.tf was written in raw state order while the .tf files were sorted, so re-keying one resource reshuffled the file. Both now sort on the same HCL address, by bytes — localeCompare disagrees with byte order on `-` vs `_`. - Stale files were never pruned: `--only campus` truncated imports.tf but left group-types.tf managing resources no longer in the exported set. The export now prunes exactly the files it owns and nothing else. - An unknown --only value filtered everything out, overwrote imports.tf with an empty string and exited 0. It is now refused up front. - Two keys could collapse onto one HCL label (`a.b` and `a_b`), producing a duplicate address; assertLabelsUnique names both keys instead. - `${` and `%{` in a CT name were emitted unescaped into a quoted template. Also carries preventDestroy across as `lifecycle { prevent_destroy = true }` — destroy protection lives in state, not in managedFields, so the export dropped it and would have migrated a protected resource into an unprotected one — and emits versions.tf, without which `tofu init` resolves churchtools_* to registry.opentofu.org/hashicorp/churchtools and fails. Comments no longer name a real campus; this repo is public.
The fixture is synthetic deliberately: this repo is public, so no live campus, Bereich or status name belongs in it. The proof that the export matches a real estate runs in the private config repo, where that state already lives, as part of the migration gate. What the fixture does carry is every shape the renderer can trip on — id 0, a key that is not a valid HCL identifier (!3 Aktiv -> g_3_aktiv), all five resource types, and every camelCase field that maps to a snake_case attribute.
The golden re-implemented the export loop and sorted its rows with localeCompare, so the snapshot pinned a string no generated file contained — and missed that imports.tf was written in raw state order. It now runs `runExportTf` into a temp dir and snapshots the files on disk, one snapshot per file, so the ordering it pins is the ordering shipped. The "routes each type to its own file" assertion compared filename SETS, which stayed green if two types swapped files; it now asserts each file's contents. The fixture gains the shapes the renderer trips on and the old one had no cover for: a name carrying an HCL interpolation marker, a destroy-protected resource, and a managed type the provider cannot represent yet (group), which exercises the skip warning.
…-golden # Conflicts: # tests/export/__snapshots__/golden.test.ts.snap # tests/export/golden.test.ts # tests/fixtures/ct-state.tier0.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The fixture is synthetic deliberately: this repo is public, so no live
campus, Bereich or status name belongs in it. The proof that the export
matches a real estate runs in the private config repo, where that state
already lives, as part of the migration gate.
What the fixture does carry is every shape the renderer can trip on —
id 0, a key that is not a valid HCL identifier (!3 Aktiv -> g_3_aktiv),
all five resource types, and every camelCase field that maps to a
snake_case attribute.
Stack created with GitHub Stacks CLI • Give Feedback 💬