@@ -68,6 +68,7 @@ function writeValidBundle(dir: string): void {
6868 ) ;
6969 writeShippedSandboxPacks ( dir ) ;
7070 writeShippedSystemSkills ( dir ) ;
71+ writeShippedFonts ( dir ) ;
7172 fs . mkdirSync ( path . join ( dir , "js-sandbox-worker" ) , { recursive : true } ) ;
7273 fs . writeFileSync (
7374 path . join ( dir , "js-sandbox-worker" , "worker-entry.js" ) ,
@@ -98,6 +99,16 @@ const SANDBOX_PACKS_DIR = path.join(
9899 "sandbox-packs"
99100) ;
100101
102+ const FONTS_DIR = path . join (
103+ __dirname ,
104+ ".." ,
105+ ".." ,
106+ ".." ,
107+ "packages" ,
108+ "timeline" ,
109+ "fonts"
110+ ) ;
111+
101112const SYSTEM_SKILLS_DIR = path . join (
102113 __dirname ,
103114 ".." ,
@@ -156,6 +167,23 @@ function writeShippedSystemSkills(dir: string): void {
156167 }
157168}
158169
170+ /**
171+ * Stage every bundled font file the repo ships.
172+ *
173+ * Same reason as the packs and skills above: the script cross-checks the
174+ * staged set against `packages/timeline/fonts/` inside a checkout, so a
175+ * fixture staging none of them is an incomplete bundle. The bytes are a stub —
176+ * what the check reads is presence, and no face is parsed here.
177+ */
178+ function writeShippedFonts ( dir : string ) : void {
179+ const fontDir = path . join ( dir , "fonts" ) ;
180+ fs . mkdirSync ( fontDir , { recursive : true } ) ;
181+ for ( const entry of fs . readdirSync ( FONTS_DIR ) ) {
182+ if ( ! / \. ( t t f | o t f | t x t ) $ / i. test ( entry ) ) continue ;
183+ fs . writeFileSync ( path . join ( fontDir , entry ) , "x" ) ;
184+ }
185+ }
186+
159187function writeStagedPackage (
160188 dir : string ,
161189 name : string ,
@@ -383,6 +411,37 @@ describe("verify-backend-bundle", () => {
383411 expect ( output ) . toContain ( `system skills staged: ${ staged } ` ) ;
384412 } ) ;
385413
414+ // C6, and the check T17 was required to observe failing: the packaged
415+ // backend registers these faces before it draws a title, so an unstaged one
416+ // silently renders every clip in that family in a host font.
417+ it ( "fails when a bundled font file is not staged" , ( ) => {
418+ const fonts = fs . readdirSync ( path . join ( tempDir , "fonts" ) ) . sort ( ) ;
419+ expect ( fonts ) . toContain ( "BebasNeue-Regular.ttf" ) ;
420+ fs . rmSync ( path . join ( tempDir , "fonts" , "BebasNeue-Regular.ttf" ) ) ;
421+ const { status, output } = runVerify ( tempDir ) ;
422+ expect ( status ) . toBe ( 1 ) ;
423+ expect ( output ) . toContain ( "Bundled font file(s) not staged under fonts/" ) ;
424+ expect ( output ) . toContain ( "BebasNeue-Regular.ttf" ) ;
425+ } ) ;
426+
427+ // A face without its licence is a licensing failure, not a cosmetic one
428+ // (C7), so the OFL files are checked with the faces rather than beside them.
429+ it ( "fails when a font licence is not staged" , ( ) => {
430+ fs . rmSync ( path . join ( tempDir , "fonts" , "OFL-Inter.txt" ) ) ;
431+ const { status, output } = runVerify ( tempDir ) ;
432+ expect ( status ) . toBe ( 1 ) ;
433+ expect ( output ) . toContain ( "OFL-Inter.txt" ) ;
434+ } ) ;
435+
436+ it ( "reports the bundled font files it found" , ( ) => {
437+ const { status, output } = runVerify ( tempDir ) ;
438+ expect ( status ) . toBe ( 0 ) ;
439+ // A count, so the check cannot pass by having matched nothing.
440+ const staged = fs . readdirSync ( path . join ( tempDir , "fonts" ) ) . length ;
441+ expect ( staged ) . toBeGreaterThan ( 0 ) ;
442+ expect ( output ) . toContain ( `${ staged } bundled font file(s) staged` ) ;
443+ } ) ;
444+
386445 it ( "fails when server.mjs itself is missing" , ( ) => {
387446 fs . rmSync ( path . join ( tempDir , "server.mjs" ) ) ;
388447 const { status, output } = runVerify ( tempDir ) ;
0 commit comments