test(core): add T1 round-trip idempotence suite for parse/serialize#1240
Conversation
a425e86 to
f473261
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
T1 suite looks solid. A few notes:
- Pinning
compositionId: "test-comp"to suppressDate.now()churn is the right call — good defensive pattern. - The registry-block sweep gracefully degrades to
[]when the dir doesn't exist; CI-safe. maxEndTime+serializehelpers here are duplicated identically in #1245. Not a blocker pre-refactor, but worth a shared test util once the dust settles.
✅ Approve — all baseline cases covered, stability test is a meaningful idempotency guard.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Hey Vance — clean T1 plumbing. The DOM/timing round-trip is exactly the public-contract surface a refactor needs to preserve, and the stability test (serialize(parse(serialize(parse))) === serialize(parse)) is the load-bearing one — it'd catch any non-determinism R1 introduces (id reshuffling, ordering churn, attribute renormalization) on a single failing assertion. No blockers. A couple of notes on coverage shape.
What I verified
Walked each test against "what could R1 silently break that this would catch first":
- Element-count + id preservation (test 1): catches "refactor dropped or renamed an element kind during parse." Solid.
- startTime / duration preservation (test 2): catches "timing-attr emission flipped from
data-start/data-endtodata-startTime/...or got rounded." Solid. - Type preservation across
text/video/img/audio(test 3): catches "one type's serializer regressed silently." Solid. - Double-serialize stability (test 4): the strongest test in this file — non-determinism here means R1 introduced a
Date.now()-shaped instability the waycompositionIdalready does today. Load-bearing. - Empty stage (test 5): edge case, cheap, fine.
- Registry block sampling (10 blocks): the
.slice(0, 10)is alphabetical, so coverage is implicitly "first 10 names." That's fine as a smoke net but mildly arbitrary. (See nit.)
The serialize() helper's compositionId: "test-comp" workaround for the Date.now() default is honest and tracked as R1 — good comment.
Concerns
- GSAP scripts are out of scope (T6), but no test pins they're at minimum passed through unmodified. If R1 accidentally drops or rewrites
<script>content during aparse → generateround-trip on a block that has GSAP, T6 won't catch it until much later in the test plan. A weak smoke assertion — "if input has a<script>block, output contains the same script content (string equality, no semantic check)" — would close a window between this PR and T6 landing. Cheap to add.
Nits
- Registry block sampling is alphabetical-first-10. Two cheaper alternatives that close that window: (a) iterate all blocks (pure addition, low cost — the test runs once per CI), or (b) name the 10 explicitly so a reader sees the surface (
const blockNames = ["animated-...", "card-...", ...]). My read: prefer (a) unless the suite gets slow — round-trip on each block should be milliseconds. (nit) - The element-id preservation assertion uses
parsed.elements.map((e) => e.id)directly. If R1's hf- id scheme renumbers existing ids during the first round-trip (e.g.el-aaa→hf-xxxx), this test would fail — which would be the right signal, but the failure message ("expected['el-aaa','el-bbb'], got['hf-...','hf-...']") might read as a regression rather than a deliberate behavior change. A comment noting that this test is "preserve-as-is for already-id'd elements; T2 covers the auto-id case" would short-circuit the confusion. (nit) - No test that timing-only edits round-trip stably for elements with
data-namecontaining special chars (quotes, ampersands, unicode). HTML escaping is a classic refactor-break shape. Cheap to add one fixture withdata-name="It's & 'wow'". (nit) maxEndTimehelper duplicates logic that probably exists ingenerateHyperframesHtml's caller. If the production callers compute total duration differently, the test's round-trip uses a different shape than real callers — risk of green-test-but-shipping-bug. Worth a quick check that the helper matches the real caller's shape. (nit)
Questions
- Is the plan to extend this file with T6 (GSAP script round-trip) as a separate suite, or split T6 into its own file? Asking because the
serialize()helper is generic enough that T6 could just appenddescribe()blocks here without re-importing — small structural choice but worth pinning. - Does the registry block set churn often? If yes, the alphabetical-first-10 slice could silently lose coverage on a renamed block. If churn is rare, the current shape is fine.
- The
// The compositionId generation instability itself is tracked as R1 (stable hf- ids)comment — is there a linked issue / Linear ticket? If yes, putting the URL in the comment makes the cross-reference clickable from the IDE.
What I didn't verify
- The HeyGenverse plan doc (got HTTP 403 from my unauthenticated fetch). Trusted the PR body's claim that this matches T1 from the plan and that T6 covers the GSAP layer.
- Whether
generateHyperframesHtml'scompositionIddefault really iscomp-${Date.now()}and not something more stable now — trusted the PR body. parseHtml's behavior on malformed input (out of scope for round-trip, but worth a glance if it ever throws on registry blocks).- Whether all 10 sampled registry blocks have non-empty
.elementsarrays — if some round-trip from empty to empty, the assertiontoHaveLength(parsed.elements.length)is trivially true for those.
— Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Second pass, building on Rames's review.
Rames's GSAP script passthrough gap is the one I'd escalate: T6 is the right home for it, but between now and T6 landing, a single smoke assertion — "if input contains a <script> block, output preserves its content verbatim" — would catch the class of R1 bug where generateHyperframesHtml accidentally re-emits a stripped or rewritten script. Cheap to add here (one fixture, no semantic GSAP execution needed).
Rames's point about maxEndTime matching the real caller's shape is also worth resolving before R1: if production computes total duration differently (e.g. uses a composition-level data-duration attribute instead of max(element endTimes)), the test's round-trip is exercising a phantom path and could mask a real bug.
The registry block sampling concern: readdirSync + alphabetical first-10 means renaming any block in the first 10 alphabetically adds it to coverage while a renamed block in positions 11+ silently falls off. Prefer either all blocks or an explicit list.
Everything else Rames flagged (element-id preservation comment, special-char data-name, helper duplication) — seconded, all nits.
f473261 to
212dcf9
Compare
|
Addressed in latest push:
|
miguel-heygen
left a comment
There was a problem hiding this comment.
Follow-up: both concerns addressed.
- ✅ Registry sampling:
.slice(0, 10)removed — all blocks now covered - ✅ GSAP smoke: new test asserts
includeScripts: truepreserves<script>tags through generate; correct scope (existence, not content fidelity — T6 handles the rest)
✅ Re-approve.
Merge activity
|
#1241) ## What Extends `getVariables.test.ts` with T8 from the SDK migration test plan: override-set merge semantics. ## Tests (4 new) - **last-write-wins** — calling `setOverrides` twice; second value wins - **sparse override** — override one key, unmentioned declared defaults survive intact - **batch override (brand kit)** — setting all keys at once via a single override object - **manual override after batch** — replacing one key from a kit batch; others untouched ## Scope note Tests are labeled **"flat-merge, current behaviour"** — `getVariables` does `{...defaults, ...overrides}`. Dotted-key path resolution (`"headline.color"` as `id.prop`) is a future SDK concern; these tests validate the flat-merge contract that exists today, not the future path-resolution semantics. ## Stack Stacked on T1 (#1240). Prerequisite for T11 (#1242), T4 (#1243).

What
Adds
htmlParser.roundtrip.test.ts— T1 from the SDK migration test plan.Tests that
parseHtml → generateHyperframesHtml → parseHtmlis lossless for element structure and timing. Scope is DOM/timing only; GSAP script round-trip is T6 territory.Tests
Inline fixtures (5):
startTime/durationpreservedtext,video,img,audio)serialize(parse(serialize(parse(html)))) === serialize(parse(html)))Registry block sampling (10): first 10 blocks in
registry/blocks/— each asserts element count survives a round-trip.Finding
Stability test surfaced a real bug:
generateHyperframesHtmldefaultscompositionIdto`comp-${Date.now()}`. SinceParsedHtmldoesn't capture this, every re-serialize emits a different id. The test works around it by passing a fixedcompositionId: "test-comp"so structural instability is still detectable. The root cause is tracked as R1 (stable hf- ids).Stack
Prerequisite for: T8 (#1241), T11 (#1242), T4 (#1243)