Skip to content

Commit 7b3b66a

Browse files
mirror: sync from hq/multiverse
Automated by mirror-to-agentis workflow. Source commit: 32df818d428d36e34bed094915684f63fc37f79c
1 parent d9e57a4 commit 7b3b66a

File tree

11 files changed

+619
-278
lines changed

11 files changed

+619
-278
lines changed

apps/web/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"dev": "next dev -p 3000",
8-
"build": "next build",
9-
"build:export": "AGENTIS_STATIC_EXPORT=true next build",
10-
"start": "next start -p 3000",
7+
"dev": "../../scripts/with-root-env.sh next dev -p 3000",
8+
"build": "../../scripts/with-root-env.sh next build",
9+
"build:export": "AGENTIS_STATIC_EXPORT=true ../../scripts/with-root-env.sh next build",
10+
"start": "../../scripts/with-root-env.sh next start -p 3000",
1111
"lint": "eslint .",
1212
"typecheck": "tsc --noEmit"
1313
},
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
{
22
"$schema": "https://json-schemastore.org/entity-sprites.json",
3-
"$comment": "User-editable entity sprite config. Maps semantic entity roles to sprite region keys. Edit this file to customize which sprite appears for each agent type, tool, and event category without touching TypeScript. Region keys must exist in TINY_DUNGEON_REGIONS / TINY_TOWN_REGIONS in SpriteConfig.ts.",
3+
"$comment": "User-editable entity sprite config. All agent keys must be in ALLOWED_AGENT_SPRITES and all error keys must be in ALLOWED_ERROR_SPRITES (see entity-sprite-map.ts). Keys outside those allowlists are rejected at load time.",
44

55
"agents": {
66
"claude": {
77
"base": "hero_knight",
88
"variants": ["hero_knight", "npc_guard", "hero_cleric"]
99
},
1010
"cursor": {
11-
"base": "hero_rogue",
12-
"variants": ["hero_rogue", "npc_thief", "npc_assassin"]
11+
"base": "hero_mage",
12+
"variants": ["hero_mage", "npc_wizard", "npc_priest"]
1313
},
1414
"codex": {
15-
"base": "hero_mage",
16-
"variants": ["hero_mage", "npc_wizard", "npc_monk"]
15+
"base": "npc_wizard",
16+
"variants": ["npc_wizard", "hero_mage", "npc_bard"]
1717
},
1818
"gemini": {
19-
"base": "hero_ranger",
20-
"variants": ["hero_ranger", "npc_bard", "npc_farmer"]
19+
"base": "npc_bard",
20+
"variants": ["npc_bard", "npc_priest", "hero_cleric"]
2121
},
2222
"openclaw": {
23-
"base": "hero_barb",
24-
"variants": ["hero_barb", "npc_smith", "npc_elder"]
23+
"base": "npc_guard",
24+
"variants": ["npc_guard", "monster_skeleton", "hero_knight"]
2525
}
2626
},
2727

@@ -30,24 +30,24 @@
3030
"tool_file_read": "saw",
3131
"tool_web_search": "bucket",
3232
"tool_terminal": "bridge_end_r",
33-
"tool_git": "sword_town",
34-
"tool_testing": "monster_golem",
33+
"tool_git": "gold_bar",
34+
"tool_testing": "mushroom",
3535
"tool_deploy": "shovel",
3636
"tool_documentation": "arrow_item",
3737
"tool_slack": "target_board",
3838
"tool_email": "target_board",
3939
"tool_database": "mushroom",
4040
"tool_api_call": "hammer_town",
41-
"tool_image_gen": "monster_ghost"
41+
"tool_image_gen": "bucket"
4242
},
4343

4444
"events": {
4545
"error": {
46-
"default": ["skull", "monster_skeleton", "monster_zombie"],
47-
"warning": ["monster_bat", "monster_ghost", "monster_slime"],
48-
"error": ["monster_slime", "monster_skeleton", "monster_zombie", "monster_bat"],
49-
"critical": ["monster_spider", "monster_demon", "monster_golem", "monster_dragon"],
50-
"outage": ["monster_rat", "monster_boss_1", "monster_boss_2", "monster_dragon"]
46+
"default": ["monster_slime", "monster_bat", "monster_spider"],
47+
"warning": ["monster_slime", "monster_bat", "monster_rat"],
48+
"error": ["monster_spider", "monster_rat", "npc_smith", "hero_rogue"],
49+
"critical": ["hero_ranger", "item_sword", "item_axe", "item_dagger"],
50+
"outage": ["item_sword", "item_axe", "item_dagger", "hero_ranger"]
5151
},
5252
"deployment": {
5353
"default": "shovel"
@@ -62,7 +62,7 @@
6262
"default": "target_board"
6363
},
6464
"combat": {
65-
"default": "monster_boss_1"
65+
"default": "item_sword"
6666
}
6767
}
6868
}

e2e/transcript-zip-import.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,14 @@ test.describe('Transcript Import Flows', () => {
132132
const fileInput = page.locator('input[type="file"]')
133133
await fileInput.setInputFiles(filePath)
134134

135-
await expect(page.getByText('1 file(s) selected')).toBeVisible()
135+
await expect(page.getByText('1 file ready')).toBeVisible()
136136

137137
fs.unlinkSync(filePath)
138138
})
139139

140140
test('Zip mode: upload screen accepts .zip files', async ({ page }) => {
141141
// Switch to Zip mode
142-
await page.getByRole('button', { name: 'Zip' }).click()
142+
await page.getByRole('radio', { name: 'Zip' }).click()
143143

144144
// Create a valid .zip containing session.jsonl
145145
const jsonlContent = VALID_RECORDS.map(r => JSON.stringify(r)).join('\n')
@@ -151,15 +151,15 @@ test.describe('Transcript Import Flows', () => {
151151
const fileInput = page.locator('input[type="file"]')
152152
await fileInput.setInputFiles(zipPath)
153153

154-
await expect(page.getByText('1 file(s) selected')).toBeVisible()
154+
await expect(page.getByText('1 file ready')).toBeVisible()
155155

156156
fs.unlinkSync(zipPath)
157157
fs.rmdirSync(dir)
158158
})
159159

160160
test('Zip mode: corrupted zip shows error gracefully', async ({ page }) => {
161161
// Switch to Zip mode
162-
await page.getByRole('button', { name: 'Zip' }).click()
162+
await page.getByRole('radio', { name: 'Zip' }).click()
163163

164164
// Create a corrupted .zip file (just random bytes with .zip extension)
165165
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'multiverse-e2e-zip-'))
@@ -171,10 +171,10 @@ test.describe('Transcript Import Flows', () => {
171171

172172
// Fill in project name and try to import
173173
await page.getByPlaceholder('your-repo-name').fill('test-project')
174-
await page.getByRole('button', { name: 'Create Replay' }).click()
174+
await page.getByRole('button', { name: 'Visualize' }).click()
175175

176176
// Should show an error message rather than crashing
177-
await expect(page.locator('.text-red-400')).toBeVisible({ timeout: 10000 })
177+
await expect(page.locator('[role="alert"]')).toBeVisible({ timeout: 10000 })
178178

179179
fs.unlinkSync(zipPath)
180180
fs.rmdirSync(dir)

packages/engine/src/__tests__/entity-sprite-map.test.ts

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// ============================================================================
44

55
import { 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

88
describe('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

Comments
 (0)