hermes-agent/apps/desktop/src/store/pet.test.ts
Brooklyn Nicholson c6d6a1c30d feat(desktop): add pet roam + motion/direction store signals
Opt-in $petRoam (localStorage), $petMotion (run/jump pose) and $petRoamDir (-1/0/1) feed the shared $petState only while the agent is at rest ($petAtRest), so a wander never overrides real activity.
2026-06-29 14:26:02 -05:00

91 lines
3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
$petActivity,
$petAtRest,
$petMotion,
$petState,
derivePetState,
flashPetActivity,
setPetActivity
} from './pet'
describe('derivePetState', () => {
it('rests at idle by default and uses waiting when awaiting input', () => {
expect(derivePetState({})).toBe('idle')
expect(derivePetState({ awaitingInput: true })).toBe('waiting')
})
it('runs when busy or a tool is executing', () => {
expect(derivePetState({ busy: true })).toBe('run')
expect(derivePetState({ toolRunning: true })).toBe('run')
})
it('reviews while reasoning (below tool, above bare busy)', () => {
expect(derivePetState({ reasoning: true })).toBe('review')
expect(derivePetState({ reasoning: true, busy: true })).toBe('review')
expect(derivePetState({ reasoning: true, toolRunning: true })).toBe('run')
})
it('waits (blocked on the user) above the in-flight signals', () => {
expect(derivePetState({ awaitingInput: true, toolRunning: true, busy: true })).toBe('waiting')
// but a finish beat still wins over waiting
expect(derivePetState({ justCompleted: true, awaitingInput: true })).toBe('wave')
})
it('honors the full priority chain: error > celebrate > complete > tool', () => {
expect(derivePetState({ error: true, celebrate: true, busy: true })).toBe('failed')
expect(derivePetState({ celebrate: true, justCompleted: true, toolRunning: true })).toBe('jump')
expect(derivePetState({ justCompleted: true, toolRunning: true })).toBe('wave')
})
})
describe('roam motion', () => {
it('only reports at-rest when the agent-driven state is plain idle', () => {
$petActivity.set({})
expect($petAtRest.get()).toBe(true)
$petActivity.set({ busy: true })
expect($petAtRest.get()).toBe(false)
$petActivity.set({})
expect($petAtRest.get()).toBe(true)
})
it('shows the roam pose while wandering, but never overrides real activity', () => {
$petActivity.set({})
$petMotion.set('run')
expect($petState.get()).toBe('run')
// Hops surface the jump pose.
$petMotion.set('jump')
expect($petState.get()).toBe('jump')
// Activity wins over a wander in progress.
$petActivity.set({ reasoning: true, busy: true })
expect($petState.get()).toBe('review')
// Back at rest, the wander resumes its pose; clearing it returns to idle.
$petActivity.set({})
expect($petState.get()).toBe('jump')
$petMotion.set(null)
expect($petState.get()).toBe('idle')
$petActivity.set({})
})
})
describe('flashPetActivity', () => {
it('clears stale sibling beats so a completion never inherits a prior error', () => {
// A turn errors (sad), then the next turn finishes cleanly. The celebrate
// beat must win — error is highest priority, so a merge-only flash would
// keep the pet on the failed pose.
setPetActivity({ error: true })
flashPetActivity({ celebrate: true })
expect($petActivity.get().error).toBe(false)
expect($petState.get()).toBe('jump')
setPetActivity({})
})
})