test(core): add T9 CompositionVariable font/image parse spec (before R1)#1246
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
27d97c5 to
801f0cf
Compare
0c2a999 to
ee015a5
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
T9 — the font and image spec tests are clear. One scope note:
The 3rd spec test ("color variable with brandRole") is actually testing ColorVariable gaining a brandRole field, not font/image type support. The PR title/description only mentions font and image. This test failing is correct (brandRole isn't on ColorVariable yet), but it documents a 3rd thing R1 needs to do that isn't mentioned in the PR body. Worth adding a bullet to the PR description: - Extend ColorVariable with optional \brandRole` field`.
The (v as Record<string, unknown>)?.brandRole cast is pragmatic for pre-type-exists specs — fine here, remove once R1 adds the field to the type.
✅ Approve.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Hey Vance — T9 pins the parse contract for font and image variable types plus the brandRole field, all of which gate brand-kit editability. No blockers. Two notes on the cast pattern + an asymmetry in reject-surface coverage.
What I verified
Walked each test against "what could R1 silently break that this catches first":
Red (spec — intentionally failing until R1):
type: "font"parses with name + source — pins R1 extendsCompositionVariableTypeto include"font"andparseCompositionVariablesaccepts the extrasource/default_name/default_sourcefields. Load-bearing for brand-kit font apply.type: "image"withbrandRoleparses and preservesbrandRole— pins both the type extension and the brand-role field passthrough. The brand-role assertion (brandRole: "logo:primary") is the more fragile one — see Concerns.type: "color"withbrandRolepreserves brandRole as extra field — pins that existing types get the samebrandRolepassthrough. This is the test that catches "R1 added brandRole to image/font but not to color" — the asymmetry trap.
Green (baseline):
- Unknown type rejected gracefully — pins that
parseCompositionVariablesis forgiving on unknown types. Critical for downlevel SDK compatibility (an older studio reading a newer composition with atype: "lottie"it doesn't know about should skip, not throw).
Concerns
- The
(v as Record<string, unknown>)?.brandRolecast suppresses TypeScript's type-safety net. If R1 landsbrandRoll(typo'd) orbrand_role(snake-case) orrole(renamed during review), this assertion would still pass against the raw parsed object — but every downstream consumer typed against the properbrandRolewould silently miss the field. Two cheap fixes:- (a) Add a post-R1 cleanup comment:
// TODO: drop the Record cast once ImageVariable type is extended in R1, so the next reader knows the cast is intentional debt with a defined endpoint. - (b) Better — after R1, the field is on the type; a runtime test exercising the typed path (
expect(v.brandRole).toBe(...)once it compiles) is the right shape. Today's cast is fine if the cleanup is tracked.
- (a) Add a post-R1 cleanup comment:
- Reject-surface coverage is asymmetric. The "unknown type rejected gracefully" baseline test covers
type: "widget". But the spec doesn't test:type: "font"without asourcefield — does R1 reject (requiresource)? Or accept with empty source?type: "image"without adefault— what's the fallback?type: "font"withdefault: nullordefault: 123(type-mismatched) — graceful or thrown?- The "accept" surface is more aggressively tested than the "reject" surface. For a parser, both matter equally — a forgiving parser that accepts garbage in
sourceis just as fragile as a strict one that throws on a real composition.
- The
extractCompositionMetadataAPI is being exercised, but the underlyingparseCompositionVariables(mentioned in the PR body as the function being changed) isn't tested directly. Probably right — the public boundary isextractCompositionMetadataand direct-unit testing the internal parser would couple tests to internals. But worth a sentence confirming that intent.
Nits
- The font-variable fixture has six fields (
id,type,label,default,source,default_name,default_source). The assertions only checkidandtype. Other fields (source,default_name,default_source) could be silently dropped by R1's parser and the test would still pass. If those fields are load-bearing for the brand-kit apply path, assert them. (nit) - The image-variable test's
default: ""— is empty-string a valid default for an image variable? If R1 normalizes it tonullor throws on empty, the test catches it on the typed path. Worth confirming the contract for "no default image." (nit) - No test for
type: "color"withoutbrandRole— does it parse normally? Implied by the existing test file (line 639+ of pre-PR), but pinning that "brandRole is optional, not required" is cheap. (nit) - Comment block at the top says "tests 1, 2, 3" red and "test 4" green — numbering goes stale if anyone reorders. The
[spec]prefix convention (used in #1245) is more durable; consider adopting here. (nit, applies stack-wide) brandRole: "logo:primary"andbrandRole: "color:primary"use a colon-namespaced format. Worth a one-line comment naming the format spec (<role-namespace>:<role-name>) so the next reader doesn't reverse-engineer the convention from the fixture. Especially important since brand-kit apply will dispatch on this string. (nit)
Questions
- Is the brandRole namespace closed (
logo:/color:/font:only) or open-ended? Affects whether R1 should validate the prefix or accept arbitrary strings. - The
default_name/default_sourcefields on the font variable — are those load-bearing for the brand-kit apply (e.g. fallback if the brand kit doesn't provide a font), or vestigial / migration cruft? If load-bearing, they should have assertions; if vestigial, consider not including them in the spec fixture. - For
type: "image", isdefault: ""semantically "no image" or "image with no default value, must be provided at apply time"? The contract should be pinned somewhere — either by an assertion or a doc comment.
What I didn't verify
- The HeyGenverse plan doc (HTTP 403 unauthenticated). Trusted the PR body's T9 framing and the brand-kit dependency.
parseCompositionVariables's current rejection behavior — assumed from the PR body thatfontandimageare filtered out today.- Whether
brandRoleis supposed to be optional or required on the new types. The tests only exercise the "with brandRole" path; the "without brandRole" path is uncovered. - Whether the studio editor's brand-kit apply path consumes
brandRoleby string-matching or by typed dispatch — affects whether the namespace-prefix format matters at the contract level.
— Review by Rames D Jusso
801f0cf to
9d4492f
Compare
ee015a5 to
6d3df7d
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Building on Rames's review.
The field passthrough gap Rames flagged is the one I'd prioritize: the font-variable fixture includes source, default_name, and default_source, but the assertions only check id and type. R1 could drop source entirely and this test would still green. If source is how the brand-kit apply path injects the font URL, it needs an assertion now — otherwise the first brand-kit integration test will be the first to catch the regression.
Reject-surface asymmetry: font without source and image without default are the two missing negative-path cases Rames called out. These are spec tests, so they're appropriately punted until R1, but they should be stubs (it.todo(...)) rather than absent — otherwise they're invisible to the R1 author.
The (v as Record<string, unknown>)?.brandRole cast: Rames's cleanup comment suggestion is the minimum bar. Adding // TODO(R1): remove cast once ImageVariable.brandRole is typed keeps the debt tracked. Agree with Rames that the cast is acceptable pre-type — just needs the breadcrumb.
The brandRole without-value case (Rames's nit): type: "color" without brandRole — this is the most likely field to be made accidentally required by R1 if the schema is updated carelessly. One baseline test: "color variable without brandRole parses normally" would catch that regression for free.
6d3df7d to
85f4622
Compare
9d4492f to
33d4e7b
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Follow-up: all concerns addressed.
- ✅ Font spec now asserts
source,default_name,default_source— the three load-bearing fields for brand-kit font apply - ✅
TODO(R1)cleanup comments on both cast sites - ✅ New baseline: "color variable without brandRole parses normally" pins
brandRoleas optional - ✅ Two new
.todostubs for reject-surface: font withoutsource, image withoutdefault
✅ Re-approve.
|
Addressed in latest push:
|
85f4622 to
3a2e237
Compare
33d4e7b to
43ac526
Compare
3a2e237 to
990da47
Compare
43ac526 to
cc70d27
Compare
990da47 to
0d2e471
Compare
cc70d27 to
bbd756a
Compare

T9 — CompositionVariable font/image parse spec (before R1)
Spec tests for net-new variable types that gate brand-kit editability. Written before R1 adds them so the addition is test-driven.
What this gates
R1 needs to:
"font"and"image"toCompositionVariableTypeparseCompositionVariablesto accept and preserve themColorVariablewith optionalbrandRolefieldBrand-kit applies (
setVariableValuewithfont:andlogo:roles) depend on the parser correctly passing these variables through.Test split
Red (spec — intentionally failing until R1):
type: "font"variable is parsed and returned (currently filtered out)type: "image"variable withbrandRoleis parsed and returned (currently filtered out)type: "color"variable preservesbrandRolefield (field not yet onColorVariabletype)Green (baseline — already work, must not regress):
File
packages/core/src/parsers/htmlParser.test.ts— new tests appended todescribe("extractCompositionMetadata")🤖 Generated with Claude Code