remove the desktop-pet system
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:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user