Files
heygen-com__hyperframes/packages/parsers/src/compositionContract.test.ts
Via ac9f463108 fix(lint): recognize compiler-derived data-end as legitimate
`bundleToSingleHtml` compiles `data-duration` into `data-end` (in
`compileTimingAttrs`), then calls `validateHyperframeHtmlContract` against
the compiled HTML. The linter's `deprecated_data_end` rule fired on the
compiler's own consistent output — `<audio data-start="0" data-duration="18"
data-end="18">` — because `diagnoseDerivedEnd` unconditionally emitted a
`deprecated-end` diagnostic whenever both attributes were present, ignoring
whether the derived value matched.

Reporters routed this as "raw-source lint passes with 0 errors and 0
warnings, but `check --strict` still logs StaticGuard noise about
data-end without data-duration." Field cluster: cli-feedback crons 61-68,
n=25+ across darwin/arm64, darwin/x64, linux/x64, win32/x64, and versions
0.7.56 through 0.7.64. L3 reporter cite (ts=1784519869): "bundleToSingleHtml
compiles data-duration into data-end, then validates the compiled HTML and
reports its own generated data-end as deprecated."

Fix: `diagnoseDerivedEnd` now stays silent when the paired `data-end`
matches `data-start + data-duration` (within a 1ns epsilon to absorb
IEEE-754 residuals like `0.1 + 0.2 = 0.30000000000000004`). Truly-legacy
authoring shapes — `data-end` alone with no `data-duration`, or a
`data-end` that disagrees with `data-duration` — still fire
`deprecated_data_end`, with a refined message that names the drift on the
conflicting variant.

Facets covered: (a) validator treats compiler-derived `data-end` as legal
when paired with `data-duration`, and (e) recognizes the compile-time
rewrite site (`bundleToSingleHtml` → `compileHtml` → `compileTimingAttrs`).
Facets (b) stderr gating, (c) terminal JSON verdict, and (d) audio-not-
dropped are unblocked transitively — StaticGuard's `console.warn` is
already gated on `!isValid` and never drops audio; the check command
already emits JSON on every terminal path; so once the false-positive
diagnostic stops firing on compiler output, the noisy stderr line and the
misleading "check appears to fail" reporter framing both go away without
further wiring.

Co-Authored-By: Via <noreply@heygen.com>
2026-07-20 13:26:51 +00:00

187 lines
6.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
COMPOSITION_ATTRIBUTES,
parseStartExpression,
readClipTiming,
writeClipTiming,
} from "./compositionContract";
class Attributes {
readonly values = new Map<string, string>();
constructor(values: Record<string, string>) {
for (const [name, value] of Object.entries(values)) this.values.set(name, value);
}
getAttribute(name: string): string | null {
return this.values.get(name) ?? null;
}
setAttribute(name: string, value: string): void {
this.values.set(name, value);
}
removeAttribute(name: string): void {
this.values.delete(name);
}
}
describe("composition timing contract", () => {
it.each([
["1.25", { kind: "absolute", value: 1.25 }],
["intro", { kind: "reference", refId: "intro", offset: 0 }],
["intro + 2", { kind: "reference", refId: "intro", offset: 2 }],
["intro+2", { kind: "reference", refId: "intro", offset: 2 }],
["intro - .5", { kind: "reference", refId: "intro", offset: -0.5 }],
["intro- 2", { kind: "reference", refId: "intro", offset: -2 }],
["intro-2", { kind: "reference", refId: "intro-2", offset: 0 }],
["intro + nope", null],
["Infinity", { kind: "reference", refId: "Infinity", offset: 0 }],
])("parses start expression %s", (raw, expected) => {
expect(parseStartExpression(raw)).toEqual(expected);
});
it("rejects adversarial start input without regex backtracking", () => {
expect(parseStartExpression(`-+${"0".repeat(100_000)}x`)).toBeNull();
});
it("matches runtime clamping for negative absolute and reference starts", () => {
expect(readClipTiming(new Attributes({ "data-start": "-2", "data-duration": "1" })).start).toBe(
0,
);
expect(
readClipTiming(new Attributes({ "data-start": "intro - 5", "data-duration": "1" }), {
resolveReferenceEnd: () => 2,
}).start,
).toBe(0);
});
it.each([
{
name: "canonical fractional timing",
attrs: { "data-start": "1.25", "data-duration": "2.5", "data-track-index": "3" },
expected: { start: 1.25, duration: 2.5, end: 3.75, trackIndex: 3 },
codes: [],
},
{
name: "resolved reference",
attrs: { "data-start": "intro - .5", "data-duration": "2" },
expected: { start: 3.5, duration: 2, end: 5.5, trackIndex: 0 },
codes: [],
resolve: (id: string) => (id === "intro" ? 4 : null),
},
{
name: "legacy attributes",
attrs: { "data-start": "1", "data-end": "4", "data-layer": "2" },
expected: { start: 1, duration: 3, end: 4, trackIndex: 2 },
codes: ["deprecated-end", "deprecated-layer"],
},
{
name: "canonical values win over conflicting legacy values",
attrs: {
"data-start": "1",
"data-duration": "2",
"data-end": "9",
"data-track-index": "1",
"data-layer": "4",
},
expected: { start: 1, duration: 2, end: 3, trackIndex: 1 },
codes: ["deprecated-end", "conflicting-end", "deprecated-layer", "conflicting-layer"],
},
{
// Regression: compiler-derived `data-end` matching `data-duration` used
// to fire `deprecated-end`, which surfaced as a false-positive
// `deprecated_data_end` from StaticGuard whenever bundler output was
// re-validated. Field cluster: cli-feedback crons 61-68, n=25+ across
// darwin/linux/win32 and versions 0.7.56-0.7.64. Reporter L3 cite
// (ts=1784519869): "bundleToSingleHtml compiles data-duration into
// data-end, then validates the compiled HTML and reports its own
// generated data-end as deprecated." Consistent pairs are silent.
name: "compiler-derived end consistent with canonical duration is silent",
attrs: {
"data-start": "0",
"data-duration": "18",
"data-end": "18",
},
expected: { start: 0, duration: 18, end: 18, trackIndex: 0 },
codes: [],
},
{
name: "compiler-derived end within float epsilon of canonical duration is silent",
// data-start="0.1" + data-duration="0.2" evaluates to 0.30000000000000004
// under IEEE-754. The compiler serializes that residual verbatim, so the
// parsed derived-end and the reader-computed canonical end differ by ~2 ulps.
// A byte-exact comparison would refire the false positive; the epsilon
// gate keeps it silent.
attrs: {
"data-start": "0.1",
"data-duration": "0.2",
"data-end": "0.30000000000000004",
},
expected: { start: 0.1, duration: 0.2 },
codes: [],
},
{
name: "invalid values are diagnosed rather than coerced",
attrs: { "data-start": "wat +", "data-duration": "-1", "data-track-index": "1.5" },
expected: { start: null, duration: null, end: null, trackIndex: 0 },
codes: ["invalid-start", "invalid-duration", "invalid-track-index"],
},
])("reads $name", ({ attrs, expected, codes, resolve }) => {
const result = readClipTiming(new Attributes(attrs), { resolveReferenceEnd: resolve });
expect(result).toMatchObject(expected);
expect(result.diagnostics.map(({ code }) => code)).toEqual(codes);
});
it("canonicalizes legacy timing during a mutation and round-trips semantics", () => {
const attrs = new Attributes({
"data-start": "1",
"data-end": "4",
"data-layer": "2",
});
const written = writeClipTiming(attrs, { start: 2, duration: 4, trackIndex: 5 });
expect(written).toMatchObject({ start: 2, duration: 4, end: 6, trackIndex: 5 });
expect(attrs.values).toEqual(
new Map([
[COMPOSITION_ATTRIBUTES.start, "2"],
[COMPOSITION_ATTRIBUTES.duration, "4"],
[COMPOSITION_ATTRIBUTES.trackIndex, "5"],
]),
);
});
it("preserves a reference expression when writing canonical duration/track fields", () => {
const attrs = new Attributes({
"data-start": "intro + 1",
"data-end": "8",
"data-layer": "2",
});
writeClipTiming(attrs, { duration: 3, trackIndex: 4 });
expect(attrs.getAttribute("data-start")).toBe("intro + 1");
expect(attrs.getAttribute("data-duration")).toBe("3");
expect(attrs.getAttribute("data-track-index")).toBe("4");
expect(attrs.getAttribute("data-end")).toBeNull();
expect(attrs.getAttribute("data-layer")).toBeNull();
});
it("preserves a legacy end when a track-only edit cannot resolve reference duration", () => {
const attrs = new Attributes({
"data-start": "intro + 1",
"data-end": "8",
"data-layer": "2",
});
writeClipTiming(attrs, { trackIndex: 4 });
expect(attrs.getAttribute("data-start")).toBe("intro + 1");
expect(attrs.getAttribute("data-duration")).toBeNull();
expect(attrs.getAttribute("data-end")).toBe("8");
expect(attrs.getAttribute("data-track-index")).toBe("4");
expect(attrs.getAttribute("data-layer")).toBeNull();
});
});