diff --git a/packages/lint/src/project.test.ts b/packages/lint/src/project.test.ts index a2705d4ced..08b201bbf1 100644 --- a/packages/lint/src/project.test.ts +++ b/packages/lint/src/project.test.ts @@ -235,3 +235,36 @@ describe("template shell style sources", () => { expect(findings.some((finding) => finding.code === "texture_mask_asset_not_found")).toBe(true); }); }); + +describe("duplicate_audio_track", () => { + async function duplicateAudioFindings(audioMarkup: string): Promise { + const project = makeProject(` +
+ ${audioMarkup} +
+ + `); + const { results } = await lintProject(project); + return results + .flatMap((entry) => entry.result.findings) + .filter((finding) => finding.code === "duplicate_audio_track"); + } + + it("does not infer overlap when same-track clips omit data-duration", async () => { + const findings = await duplicateAudioFindings(` + + + `); + + expect(findings).toHaveLength(0); + }); + + it("still reports overlap when explicit clip durations overlap", async () => { + const findings = await duplicateAudioFindings(` + + + `); + + expect(findings).toHaveLength(1); + }); +}); diff --git a/packages/lint/src/project.ts b/packages/lint/src/project.ts index a23226542f..84d41ad726 100644 --- a/packages/lint/src/project.ts +++ b/packages/lint/src/project.ts @@ -503,11 +503,11 @@ function lintDuplicateAudioTracks(htmlSources: HtmlSource[]): HyperframeLintFind const startStr = extractAttr(tag, "data-start"); const durStr = extractAttr(tag, "data-duration"); const src = extractAttr(tag, "src") ?? "unknown"; - if (!trackStr || !startStr) continue; + if (!trackStr || !startStr || !durStr) continue; const trackIndex = parseInt(trackStr, 10); const start = parseFloat(startStr); - const duration = durStr ? parseFloat(durStr) : Infinity; + const duration = parseFloat(durStr); const key = `${src}:${start}:${duration}:${trackIndex}`; if (seen.has(key)) continue; seen.add(key); diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts index 1419c545ee..66df582595 100644 --- a/packages/lint/src/rules/core.test.ts +++ b/packages/lint/src/rules/core.test.ts @@ -464,6 +464,17 @@ body { expect(finding).toBeUndefined(); }); + it("does not treat a head tag literal inside a template script comment as document head", async () => { + const html = compositionWithBodyPrefix( + "", + ``, + ); + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "head_leaked_text"); + + expect(finding).toBeUndefined(); + }); + it("does not report CSS-looking educational text inside the composition root", async () => { const html = compositionWithBodyPrefix( "", diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts index f4b6b8a300..60a7218b2a 100644 --- a/packages/lint/src/rules/core.ts +++ b/packages/lint/src/rules/core.ts @@ -107,12 +107,14 @@ function findLeakedTextInHeadContent(headContent: string): string | null { } function findLeakedTextInHead(rawSource: string): string | null { - const headMatches = [...rawSource.matchAll(HEAD_CONTENT_PATTERN)]; - for (const match of headMatches) { - const leakedText = findLeakedTextInHeadContent(match[1] ?? ""); - if (leakedText) return leakedText; - } - return null; + const headPattern = new RegExp(HEAD_CONTENT_PATTERN.source, "i"); + const match = headPattern.exec(rawSource); + if (!match) return null; + + const bodyIndex = rawSource.search(/= 0 && match.index > bodyIndex) return null; + + return findLeakedTextInHeadContent(match[1] ?? ""); } function findLeakedTextBetweenHeadAndBody(rawSource: string): string | null {