remove the desktop-pet system
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

The pet feature was three loosely coupled subsystems that no core flow
depended on: the Petdex catalog page, the pet install/preference store, and
the pet-state socket. All three are removed.

The only cross-boundary touch point was a try/catch-wrapped observer that
mapped chat-run events to pet animations. Removing it leaves the /chat-run
event contract unchanged: no event name, field name, field type, or emission
timing changed.

Removing petdexPublicRoutes also closes the unauthenticated outbound proxy
at /api/hermes/petdex/asset, which shrinks the SSRF surface.
This commit is contained in:
2026-10-20 13:24:17 +08:00
committed by trae-solo-agent
parent 5e1003ceb7
commit b709ec6742
30 changed files with 64 additions and 1793 deletions
-58
View File
@@ -1,58 +0,0 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import type { ActivePet } from '@/api/hermes/pets'
const mockPetsApi = vi.hoisted(() => ({
fetchActivePet: vi.fn(),
adoptPet: vi.fn(),
updateActivePetPreferences: vi.fn(),
}))
vi.mock('@/api/hermes/pets', () => mockPetsApi)
import { usePetsStore } from '@/stores/hermes/pets'
function activePet(overrides: Partial<ActivePet> = {}): ActivePet {
return {
enabled: true,
slug: 'desk-cat',
displayName: 'Desk Cat',
kind: 'cat',
submittedBy: 'petdex',
source: 'petdex',
mime: 'image/webp',
spritesheetDataUrl: 'data:image/webp;base64,AA==',
spritesheetRevision: 1,
frameW: 192,
frameH: 208,
framesPerState: 6,
loopMs: 1100,
scale: 0.33,
position: { x: 10, y: 20 },
stateRows: ['idle'],
installedAt: 1,
updatedAt: 1,
...overrides,
}
}
describe('Pets Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('disables and clears the active pet when hiding it', async () => {
const store = usePetsStore()
store.activePet = activePet()
mockPetsApi.updateActivePetPreferences.mockResolvedValue(null)
await store.hideActivePet()
expect(mockPetsApi.updateActivePetPreferences).toHaveBeenCalledWith({ enabled: false })
expect(store.activePet).toBeNull()
expect(store.hasActivePet).toBe(false)
expect(store.saving).toBe(false)
})
})
-5
View File
@@ -222,11 +222,6 @@ export async function mockHermesApi(page: Page, options: MockHermesApiOptions =
return
}
if (pathname === '/api/hermes/pets/active') {
await route.fulfill(jsonResponse({ pet: null }))
return
}
if (pathname === '/api/hermes/profiles') {
await route.fulfill(jsonResponse({
profiles: [
-58
View File
@@ -1,58 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { fetchPetdexAsset, fetchPetdexManifest } from '../../packages/server/src/services/hermes/petdex'
describe('petdex service', () => {
afterEach(() => {
vi.unstubAllGlobals()
})
it('adds local preview proxy URLs to manifest pets', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
generatedAt: '2026-06-30T00:00:00.000Z',
pets: [{
slug: 'desk-cat',
displayName: 'Desk Cat',
kind: 'cat',
submittedBy: 'petdex',
spritesheetUrl: 'https://assets.petdex.dev/pets/desk-cat/spritesheet.webp',
}],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
})))
const manifest = await fetchPetdexManifest({ force: true })
expect(manifest.pets[0].spritesheetUrl).toBe('https://assets.petdex.dev/pets/desk-cat/spritesheet.webp')
expect(manifest.pets[0].previewUrl).toBe(
'/api/hermes/petdex/asset?url=https%3A%2F%2Fassets.petdex.dev%2Fpets%2Fdesk-cat%2Fspritesheet.webp',
)
})
it('only proxies petdex https assets', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
await expect(fetchPetdexAsset('https://example.com/pet.webp')).rejects.toThrow('Unsupported petdex asset host')
await expect(fetchPetdexAsset('http://assets.petdex.dev/pet.webp')).rejects.toThrow('Unsupported petdex asset host')
expect(fetchMock).not.toHaveBeenCalled()
})
it('fetches allowed petdex image assets with a size limit', async () => {
const body = new Uint8Array([1, 2, 3, 4])
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(body, {
status: 200,
headers: {
'content-type': 'image/webp',
'content-length': String(body.byteLength),
},
})))
const asset = await fetchPetdexAsset('https://assets.petdex.dev/pets/desk-cat/spritesheet.webp')
expect(asset.mime).toBe('image/webp')
expect([...asset.buffer]).toEqual([1, 2, 3, 4])
expect(asset.maxAgeSeconds).toBe(3600)
})
})
-77
View File
@@ -1,77 +0,0 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { getActivePet, updateActivePetPreferences } from '../../packages/server/src/services/hermes/pets'
const originalWebUiHome = process.env.HERMES_WEB_UI_HOME
let hermesHome = ''
function profilePetsDir(profile: string): string {
const segment = Buffer.from(profile || 'default', 'utf-8').toString('base64url')
return join(hermesHome, 'profile-metadata', segment, 'pets')
}
async function writeInstalledPet(profile: string, slug: string): Promise<string> {
const petsDir = profilePetsDir(profile)
const petDir = join(petsDir, slug)
await mkdir(petDir, { recursive: true })
await writeFile(join(petDir, 'spritesheet.webp'), Buffer.from([1, 2, 3]))
await writeFile(join(petDir, 'pet.json'), `${JSON.stringify({
slug,
displayName: 'Desk Cat',
kind: 'cat',
submittedBy: 'petdex',
source: 'petdex',
spritesheetUrl: 'https://assets.petdex.dev/pets/desk-cat/spritesheet.webp',
petJsonUrl: '',
zipUrl: '',
spritesheetFile: 'spritesheet.webp',
mime: 'image/webp',
installedAt: 1,
updatedAt: 1,
}, null, 2)}\n`)
await writeFile(join(petsDir, 'active.json'), `${JSON.stringify({
enabled: true,
slug,
scale: 0.42,
position: { x: 15, y: 30 },
updatedAt: 2,
}, null, 2)}\n`)
return join(petsDir, 'active.json')
}
describe('pets service', () => {
beforeEach(async () => {
hermesHome = await mkdtemp(join(tmpdir(), 'hermes-pets-service-'))
process.env.HERMES_WEB_UI_HOME = hermesHome
})
afterEach(async () => {
await rm(hermesHome, { recursive: true, force: true })
if (originalWebUiHome === undefined) delete process.env.HERMES_WEB_UI_HOME
else process.env.HERMES_WEB_UI_HOME = originalWebUiHome
})
it('persists disabled active pet state and stops returning it as active', async () => {
const profile = 'default'
const activePath = await writeInstalledPet(profile, 'desk-cat')
await expect(getActivePet(profile)).resolves.toMatchObject({
enabled: true,
slug: 'desk-cat',
scale: 0.42,
})
await expect(updateActivePetPreferences(profile, { enabled: false })).resolves.toBeNull()
await expect(getActivePet(profile)).resolves.toBeNull()
const active = JSON.parse(await readFile(activePath, 'utf-8'))
expect(active).toMatchObject({
enabled: false,
slug: 'desk-cat',
scale: 0.42,
})
})
})