test(desktop): pin that a scoped Capabilities TTS panel saves into its scope

Review on the PR: the load-bearing direction (profile B's scope
forwarded into saveHermesConfigRecord) was only covered by the live
E2E; the unit test asserted just the unscoped default. Renders the panel
with profile={profile:'scout', connectionId:'gw-2'} and asserts the
autosave carries exactly that scope. The @/hermes full-replacement mock
gains profileScopeKey, which use-config-record reaches when a scope is
present. Sabotage (drop the profile prop from <VoiceProviderFields>)
fails this test with `expected undefined to deeply equal {profile:
'scout', …}`.
This commit is contained in:
teknium1
2026-09-14 18:35:02 -07:00
committed by Teknium
parent bdac5c8e2e
commit ab1ef0c88e

View File

@@ -68,6 +68,12 @@ vi.mock('@/hermes', () => ({
saveHermesConfig: (config: unknown) => saveHermesConfig(config),
saveHermesConfigRecord: (config: unknown, profile?: unknown) => saveHermesConfigRecord(config, profile),
getElevenLabsVoices: () => getElevenLabsVoices(),
// use-config-record keys its query cache by scope via profileScopeKey; a
// scoped panel reaches it, so the full-replacement mock must provide it.
profileScopeKey: (scope?: { profile?: string; connectionId?: string } | string) =>
typeof scope === 'object' && scope
? `${scope.connectionId ?? ''}::${scope.profile ?? 'default'}`
: (scope ?? 'default'),
// @/store/profile (pulled in transitively via use-config-record's
// normalizeProfileKey import) calls this at module-init; the full-replacement
// mock must provide it or the module graph throws on load.
@@ -206,6 +212,40 @@ describe('ToolsetConfigPanel', () => {
expect(saveHermesConfigRecord.mock.calls.at(-1)?.[1]).toBeUndefined()
})
it('autosaves the inline voice fields into the profile the panel is scoped to', async () => {
// The bug: a scoped Capabilities panel (scope selector → profile B) rendered
// these fields unscoped, so editing B's voice PUT the ACTIVE profile's config.
getToolsetConfig.mockResolvedValue(
config({
active_provider: 'OpenAI TTS',
providers: [
{
name: 'OpenAI TTS',
badge: 'paid',
tag: 'High quality voices',
env_vars: [],
post_setup: null,
requires_nous_auth: false,
is_active: true,
tts_provider: 'openai'
}
]
})
)
const scope = { profile: 'scout', connectionId: 'gw-2' }
render(<ToolsetConfigPanel onConfiguredChange={vi.fn()} profile={scope} toolset="tts" />)
fireEvent.change(await screen.findByDisplayValue('alloy'), { target: { value: 'marin' } })
await waitFor(() => expect(saveHermesConfigRecord).toHaveBeenCalled(), { timeout: 3000 })
const [saved, forwarded] = saveHermesConfigRecord.mock.calls.at(-1) as [
Record<string, Record<string, Record<string, string>>>,
unknown
]
expect(saved.tts.openai.voice).toBe('marin')
expect(forwarded).toEqual(scope)
})
it('renders no inline voice fields for rows without tts_provider (older backend)', async () => {
render(<ToolsetConfigPanel onConfiguredChange={vi.fn()} toolset="tts" />)