Skip to content

test(export): byte-stable golden snapshots over a synthetic state - #174

Merged
2000game merged 6 commits into
mainfrom
export-tf-golden
Sep 17, 2026
Merged

2000game merged 6 commits into
mainfrom
export-tf-golden

Conversation

@2000game

Copy link
Copy Markdown
Member

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 💬

`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
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
Base automatically changed from export-tf-renderer to main September 17, 2026 14:26
@2000game
2000game marked this pull request as ready for review September 17, 2026 14:29
@2000game
2000game merged commit ba74230 into main Sep 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant