mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
ac9f463108
`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>
187 lines
6.8 KiB
TypeScript
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();
|
|
});
|
|
});
|