diff --git a/packages/producer/src/regression-harness.ts b/packages/producer/src/regression-harness.ts index c7fdc7e44..22d5f3062 100644 --- a/packages/producer/src/regression-harness.ts +++ b/packages/producer/src/regression-harness.ts @@ -235,6 +235,14 @@ function discoverTestSuites( return suites; } +function copyFixtureSupportFiles(suite: TestSuite, tempRoot: string): void { + const excluded = new Set(["src", "output", "meta.json", "failures"]); + for (const entry of readdirSync(suite.dir)) { + if (excluded.has(entry)) continue; + cpSync(join(suite.dir, entry), join(tempRoot, entry), { recursive: true }); + } +} + // ── FFmpeg Utilities ───────────────────────────────────────────────────────── function runFfmpeg(args: string[], label: string): { stdout: Buffer; stderr: string } { @@ -582,6 +590,7 @@ async function runTestSuite( logPretty("Rendering video...", "🎬"); const tempSrcDir = join(tempRoot, "src"); + copyFixtureSupportFiles(suite, tempRoot); cpSync(suite.srcDir, tempSrcDir, { recursive: true }); const job = createRenderJob({ diff --git a/packages/producer/src/services/fileServer.test.ts b/packages/producer/src/services/fileServer.test.ts index 9d97becf0..303512881 100644 --- a/packages/producer/src/services/fileServer.test.ts +++ b/packages/producer/src/services/fileServer.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from "bun:test"; -import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import path, { join } from "node:path"; import { tmpdir } from "node:os"; import { + createFileServer, HF_BRIDGE_SCRIPT, HF_EARLY_STUB, injectScriptsAtHeadStart, @@ -151,6 +152,45 @@ describe("isPathInside", () => { }); }); +describe("createFileServer", () => { + it("serves asset files through project-root symlinked directories", async () => { + const workspaceDir = mkdtempSync(join(tmpdir(), "hf-file-server-symlink-assets-")); + const adsDir = join(workspaceDir, "Ads"); + const projectDir = join(adsDir, "annual-upsell-2"); + const sharedDir = join(adsDir, "shared"); + + try { + mkdirSync(projectDir, { recursive: true }); + mkdirSync(sharedDir, { recursive: true }); + writeFileSync(join(projectDir, "index.html"), ""); + writeFileSync( + join(sharedDir, "brand.css"), + ".aisplus-glass { backdrop-filter: blur(28px); }", + ); + symlinkSync("../shared", join(projectDir, "shared")); + + const server = await createFileServer({ + projectDir, + preHeadScripts: [], + headScripts: [], + bodyScripts: [], + }); + + try { + const response = await fetch(`${server.url}/shared/brand.css`); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/css"); + expect(await response.text()).toContain(".aisplus-glass"); + } finally { + server.close(); + } + } finally { + rmSync(workspaceDir, { recursive: true, force: true }); + } + }); +}); + describe("HF_EARLY_STUB + HF_BRIDGE_SCRIPT integration", () => { /** * Simulates the real injection order in a Puppeteer page: diff --git a/packages/producer/src/services/fileServer.ts b/packages/producer/src/services/fileServer.ts index c27216b66..3060ab4c4 100644 --- a/packages/producer/src/services/fileServer.ts +++ b/packages/producer/src/services/fileServer.ts @@ -31,8 +31,8 @@ type IsPathInsideOptions = { /** * Returns true iff `child` is the same as, or nested inside, `parent` after - * symlink-free path normalization. Used to reject path-traversal attempts - * (e.g. GET `/../etc/passwd`) before opening any file. + * path normalization. Used to reject path-traversal attempts (e.g. + * GET `/../etc/passwd`) before opening any file. * * `path.join(root, "..")` normalizes traversal segments and can escape `root` * entirely, so the join return value alone is not a safe guard. Callers must @@ -537,13 +537,14 @@ export function createFileServer(options: FileServerOptions): Promise + + + + + +
+
+
SYMLINK CSS
+
+
+ + diff --git a/packages/producer/tests/render-symlinked-assets/output/output.mp4 b/packages/producer/tests/render-symlinked-assets/output/output.mp4 new file mode 100644 index 000000000..e4db6f4a3 --- /dev/null +++ b/packages/producer/tests/render-symlinked-assets/output/output.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d73f03b86c6c60f8900627df3a71771b47386add8bcbb50ed7b162b81abea976 +size 10433 diff --git a/packages/producer/tests/render-symlinked-assets/shared/brand.css b/packages/producer/tests/render-symlinked-assets/shared/brand.css new file mode 100644 index 000000000..9faa5e347 --- /dev/null +++ b/packages/producer/tests/render-symlinked-assets/shared/brand.css @@ -0,0 +1,18 @@ +.stage { + position: absolute; + inset: 0; + display: grid; + place-items: center; + background: #0b1220; +} + +.card { + width: 260px; + height: 120px; + display: grid; + place-items: center; + border: 4px solid #86efac; + background: #22c55e; + color: #04130a; + font: 700 34px system-ui, sans-serif; +} diff --git a/packages/producer/tests/render-symlinked-assets/src/index.html b/packages/producer/tests/render-symlinked-assets/src/index.html new file mode 100644 index 000000000..1b47764e1 --- /dev/null +++ b/packages/producer/tests/render-symlinked-assets/src/index.html @@ -0,0 +1,19 @@ + + + + + + +
+
+
SYMLINK CSS
+
+
+ + diff --git a/packages/producer/tests/render-symlinked-assets/src/shared b/packages/producer/tests/render-symlinked-assets/src/shared new file mode 120000 index 000000000..8fba6b66a --- /dev/null +++ b/packages/producer/tests/render-symlinked-assets/src/shared @@ -0,0 +1 @@ +../shared \ No newline at end of file