33// ============================================================================
44
55import { describe , it , expect , beforeEach } from 'vitest'
6- import { EntitySpriteMap } from '../engine/entity-sprite-map'
6+ import { EntitySpriteMap , ALLOWED_AGENT_SPRITES , ALLOWED_ERROR_SPRITES } from '../engine/entity-sprite-map'
77
88describe ( 'EntitySpriteMap' , ( ) => {
99 let map : EntitySpriteMap
@@ -68,7 +68,7 @@ describe('EntitySpriteMap', () => {
6868 describe ( 'resolveTool' , ( ) => {
6969 it ( 'resolves known tool IDs' , ( ) => {
7070 expect ( map . resolveTool ( 'tool_code_edit' ) ) . toBe ( 'shield_town' )
71- expect ( map . resolveTool ( 'tool_testing' ) ) . toBe ( 'monster_golem ' )
71+ expect ( map . resolveTool ( 'tool_testing' ) ) . toBe ( 'mushroom ' )
7272 expect ( map . resolveTool ( 'tool_deploy' ) ) . toBe ( 'shovel' )
7373 expect ( map . resolveTool ( 'tool_terminal' ) ) . toBe ( 'bridge_end_r' )
7474 } )
@@ -98,27 +98,27 @@ describe('EntitySpriteMap', () => {
9898
9999 describe ( 'resolveEvent' , ( ) => {
100100 it ( 'resolves event categories to default sprite (first from array)' , ( ) => {
101- expect ( map . resolveEvent ( 'error' ) ) . toBe ( 'skull ' )
101+ expect ( map . resolveEvent ( 'error' ) ) . toBe ( 'monster_slime ' )
102102 expect ( map . resolveEvent ( 'deployment' ) ) . toBe ( 'shovel' )
103103 expect ( map . resolveEvent ( 'file_change' ) ) . toBe ( 'saw' )
104104 expect ( map . resolveEvent ( 'task' ) ) . toBe ( 'banner_1' )
105105 expect ( map . resolveEvent ( 'comms' ) ) . toBe ( 'target_board' )
106- expect ( map . resolveEvent ( 'combat' ) ) . toBe ( 'monster_boss_1 ' )
106+ expect ( map . resolveEvent ( 'combat' ) ) . toBe ( 'item_sword ' )
107107 } )
108108
109109 it ( 'resolves error severity levels to first variant' , ( ) => {
110- expect ( map . resolveEvent ( 'error' , 'warning' ) ) . toBe ( 'monster_bat ' )
111- expect ( map . resolveEvent ( 'error' , 'error' ) ) . toBe ( 'monster_slime ' )
112- expect ( map . resolveEvent ( 'error' , 'critical' ) ) . toBe ( 'monster_spider ' )
113- expect ( map . resolveEvent ( 'error' , 'outage' ) ) . toBe ( 'monster_rat ' )
110+ expect ( map . resolveEvent ( 'error' , 'warning' ) ) . toBe ( 'monster_slime ' )
111+ expect ( map . resolveEvent ( 'error' , 'error' ) ) . toBe ( 'monster_spider ' )
112+ expect ( map . resolveEvent ( 'error' , 'critical' ) ) . toBe ( 'hero_ranger ' )
113+ expect ( map . resolveEvent ( 'error' , 'outage' ) ) . toBe ( 'item_sword ' )
114114 } )
115115
116116 it ( 'falls back to default when severity not found' , ( ) => {
117- expect ( map . resolveEvent ( 'error' , 'unknown_severity' ) ) . toBe ( 'skull ' )
117+ expect ( map . resolveEvent ( 'error' , 'unknown_severity' ) ) . toBe ( 'monster_slime ' )
118118 } )
119119
120- it ( 'falls back to skull for unknown categories' , ( ) => {
121- expect ( map . resolveEvent ( 'unknown_category' ) ) . toBe ( 'skull ' )
120+ it ( 'falls back to monster_slime for unknown categories' , ( ) => {
121+ expect ( map . resolveEvent ( 'unknown_category' ) ) . toBe ( 'monster_slime ' )
122122 } )
123123 } )
124124
@@ -148,7 +148,7 @@ describe('EntitySpriteMap', () => {
148148 const key = map . resolveEventVariant ( 'error' , 'error' , `monster-${ i } ` )
149149 distribution . set ( key , ( distribution . get ( key ) ?? 0 ) + 1 )
150150 }
151- // error severity pool: ['monster_slime ', 'monster_skeleton ', 'monster_zombie ', 'monster_bat ']
151+ // error severity pool: ['monster_spider ', 'monster_rat ', 'npc_smith ', 'hero_rogue ']
152152 expect ( distribution . size ) . toBe ( 4 )
153153 } )
154154
@@ -157,16 +157,16 @@ describe('EntitySpriteMap', () => {
157157 for ( let i = 0 ; i < 200 ; i ++ ) {
158158 keys . add ( map . resolveEventVariant ( 'error' , 'critical' , `crit-${ i } ` ) )
159159 }
160- // critical pool: ['monster_spider ', 'monster_demon ', 'monster_golem ', 'monster_dragon ']
160+ // critical pool: ['hero_ranger ', 'item_sword ', 'item_axe ', 'item_dagger ']
161161 expect ( keys . size ) . toBe ( 4 )
162- expect ( keys ) . toContain ( 'monster_spider ' )
163- expect ( keys ) . toContain ( 'monster_demon ' )
162+ expect ( keys ) . toContain ( 'hero_ranger ' )
163+ expect ( keys ) . toContain ( 'item_sword ' )
164164 } )
165165
166166 it ( 'falls back to default pool when severity not found' , ( ) => {
167167 const key = map . resolveEventVariant ( 'error' , 'unknown_sev' , 'evt-x' )
168- // default pool: ['skull ', 'monster_skeleton ', 'monster_zombie ']
169- expect ( [ 'skull ' , 'monster_skeleton ' , 'monster_zombie ' ] ) . toContain ( key )
168+ // default pool: ['monster_slime ', 'monster_bat ', 'monster_spider ']
169+ expect ( [ 'monster_slime ' , 'monster_bat ' , 'monster_spider ' ] ) . toContain ( key )
170170 } )
171171
172172 it ( 'works with non-array event configs (string passthrough)' , ( ) => {
@@ -176,12 +176,12 @@ describe('EntitySpriteMap', () => {
176176
177177 it ( 'falls back gracefully without eventId' , ( ) => {
178178 const key = map . resolveEventVariant ( 'error' , 'error' )
179- expect ( key ) . toBe ( 'monster_slime ' ) // first element of error pool
179+ expect ( key ) . toBe ( 'monster_spider ' ) // first element of error pool
180180 } )
181181
182- it ( 'falls back to skull for unknown categories' , ( ) => {
182+ it ( 'falls back to monster_slime for unknown categories' , ( ) => {
183183 const key = map . resolveEventVariant ( 'unknown' , undefined , 'evt-1' )
184- expect ( key ) . toBe ( 'skull ' )
184+ expect ( key ) . toBe ( 'monster_slime ' )
185185 } )
186186
187187 it ( 'same event ID always produces same sprite across fresh instances' , ( ) => {
@@ -221,49 +221,44 @@ describe('EntitySpriteMap', () => {
221221 expect ( Object . keys ( eventMap ) ) . toContain ( 'error' )
222222 expect ( Object . keys ( eventMap ) ) . toContain ( 'deployment' )
223223 // Array values are flattened to their first element
224- expect ( eventMap [ 'error' ] ! [ 'critical' ] ) . toBe ( 'monster_spider ' )
225- expect ( eventMap [ 'error' ] ! [ 'default' ] ) . toBe ( 'skull ' )
224+ expect ( eventMap [ 'error' ] ! [ 'critical' ] ) . toBe ( 'hero_ranger ' )
225+ expect ( eventMap [ 'error' ] ! [ 'default' ] ) . toBe ( 'monster_slime ' )
226226 // Non-array values stay as-is
227227 expect ( eventMap [ 'deployment' ] ! [ 'default' ] ) . toBe ( 'shovel' )
228228 } )
229229
230230 it ( 'getAgentTypeMap returns base sprite for each agent type' , ( ) => {
231231 const typeMap = map . getAgentTypeMap ( )
232232 expect ( typeMap [ 'claude' ] ) . toBe ( 'hero_knight' )
233- expect ( typeMap [ 'cursor' ] ) . toBe ( 'hero_rogue ' )
234- expect ( typeMap [ 'codex' ] ) . toBe ( 'hero_mage ' )
235- expect ( typeMap [ 'gemini' ] ) . toBe ( 'hero_ranger ' )
236- expect ( typeMap [ 'openclaw' ] ) . toBe ( 'hero_barb ' )
233+ expect ( typeMap [ 'cursor' ] ) . toBe ( 'hero_mage ' )
234+ expect ( typeMap [ 'codex' ] ) . toBe ( 'npc_wizard ' )
235+ expect ( typeMap [ 'gemini' ] ) . toBe ( 'npc_bard ' )
236+ expect ( typeMap [ 'openclaw' ] ) . toBe ( 'npc_guard ' )
237237 expect ( Object . keys ( typeMap ) . length ) . toBe ( 5 )
238238 } )
239239
240240 it ( 'getAllAgentSpriteKeys returns deduplicated set of all variant keys' , ( ) => {
241241 const keys = map . getAllAgentSpriteKeys ( )
242- // 5 agent types × 3 variants = 15, all unique
243- expect ( keys . length ) . toBe ( 15 )
242+ // All keys should be from the approved agent allowlist
243+ for ( const k of keys ) {
244+ expect ( ALLOWED_AGENT_SPRITES . has ( k ) ) . toBe ( true )
245+ }
244246 expect ( keys ) . toContain ( 'hero_knight' )
245247 expect ( keys ) . toContain ( 'npc_guard' )
246248 expect ( keys ) . toContain ( 'hero_cleric' )
247- expect ( keys ) . toContain ( 'npc_assassin' )
248249 } )
249250
250251 it ( 'getMonsterSpriteMap returns all error variant region keys' , ( ) => {
251252 const monsterMap = map . getMonsterSpriteMap ( )
252- // Now collects ALL variants from array pools
253- expect ( monsterMap [ 'bat' ] ) . toBe ( 'monster_bat' )
253+ // Should contain the curated error sprites
254254 expect ( monsterMap [ 'slime' ] ) . toBe ( 'monster_slime' )
255255 expect ( monsterMap [ 'spider' ] ) . toBe ( 'monster_spider' )
256+ expect ( monsterMap [ 'bat' ] ) . toBe ( 'monster_bat' )
256257 expect ( monsterMap [ 'rat' ] ) . toBe ( 'monster_rat' )
257- expect ( monsterMap [ 'skeleton' ] ) . toBe ( 'monster_skeleton' )
258- expect ( monsterMap [ 'zombie' ] ) . toBe ( 'monster_zombie' )
259- expect ( monsterMap [ 'ghost' ] ) . toBe ( 'monster_ghost' )
260- expect ( monsterMap [ 'demon' ] ) . toBe ( 'monster_demon' )
261- expect ( monsterMap [ 'golem' ] ) . toBe ( 'monster_golem' )
262- expect ( monsterMap [ 'dragon' ] ) . toBe ( 'monster_dragon' )
263- expect ( monsterMap [ 'boss_1' ] ) . toBe ( 'monster_boss_1' )
264- expect ( monsterMap [ 'boss_2' ] ) . toBe ( 'monster_boss_2' )
265- // All monster_ prefixed variants should be present
266- expect ( Object . keys ( monsterMap ) . length ) . toBeGreaterThanOrEqual ( 12 )
258+ // All keys should be from allowed error sprites
259+ for ( const key of Object . values ( monsterMap ) ) {
260+ expect ( ALLOWED_ERROR_SPRITES . has ( key ) ) . toBe ( true )
261+ }
267262 } )
268263 } )
269264
@@ -280,7 +275,7 @@ describe('EntitySpriteMap', () => {
280275 // Should return valid keys even before init()
281276 expect ( map . resolveAgent ( 'claude' , 'id-1' ) ) . toBeTruthy ( )
282277 expect ( map . resolveTool ( 'tool_code_edit' ) ) . toBe ( 'shield_town' )
283- expect ( map . resolveEvent ( 'error' ) ) . toBe ( 'skull ' )
278+ expect ( map . resolveEvent ( 'error' ) ) . toBe ( 'monster_slime ' )
284279 } )
285280
286281 it ( 'clearCache resets agent cache' , ( ) => {
@@ -354,4 +349,51 @@ describe('EntitySpriteMap', () => {
354349 expect ( variantKeys ) . toContain ( defaultKey )
355350 } )
356351 } )
352+
353+ // -----------------------------------------------------------------------
354+ // Sprite Allowlist Enforcement
355+ // -----------------------------------------------------------------------
356+
357+ describe ( 'sprite allowlists' , ( ) => {
358+ it ( 'all default agent variants are in ALLOWED_AGENT_SPRITES' , ( ) => {
359+ const types = [ 'claude' , 'cursor' , 'codex' , 'gemini' , 'openclaw' ]
360+ for ( const type of types ) {
361+ const config = map . getAgentConfig ( type )
362+ expect ( config ) . not . toBeNull ( )
363+ expect ( ALLOWED_AGENT_SPRITES . has ( config ! . base ) ) . toBe ( true )
364+ for ( const v of config ! . variants ) {
365+ expect ( ALLOWED_AGENT_SPRITES . has ( v ) ) . toBe ( true )
366+ }
367+ }
368+ } )
369+
370+ it ( 'all default error event sprites are in ALLOWED_ERROR_SPRITES' , ( ) => {
371+ const severities = [ 'default' , 'warning' , 'error' , 'critical' , 'outage' ]
372+ for ( const severity of severities ) {
373+ for ( let i = 0 ; i < 50 ; i ++ ) {
374+ const key = map . resolveEventVariant ( 'error' , severity , `test-${ severity } -${ i } ` )
375+ expect ( ALLOWED_ERROR_SPRITES . has ( key ) ) . toBe ( true )
376+ }
377+ }
378+ } )
379+
380+ it ( 'resolveAgent never returns a key outside the allowlist' , ( ) => {
381+ const types = [ 'claude' , 'cursor' , 'codex' , 'gemini' , 'openclaw' ]
382+ for ( const type of types ) {
383+ for ( let i = 0 ; i < 50 ; i ++ ) {
384+ const key = map . resolveAgent ( type , `agent-${ type } -${ i } ` )
385+ expect ( ALLOWED_AGENT_SPRITES . has ( key ) ) . toBe ( true )
386+ }
387+ }
388+ } )
389+
390+ it ( 'no agent sprite key appears in error allowlist (disjoint pools)' , ( ) => {
391+ // Verify the pools are completely disjoint
392+ let overlap = 0
393+ for ( const key of ALLOWED_AGENT_SPRITES ) {
394+ if ( ALLOWED_ERROR_SPRITES . has ( key ) ) overlap ++
395+ }
396+ expect ( overlap ) . toBe ( 0 )
397+ } )
398+ } )
357399} )
0 commit comments