From ab1ef0c88eeb6e670525e41c9c19520390b5fcc9 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:35:02 -0700 Subject: [PATCH] test(desktop): pin that a scoped Capabilities TTS panel saves into its scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ) fails this test with `expected undefined to deeply equal {profile: 'scout', …}`. --- .../settings/toolset-config-panel.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/desktop/src/app/settings/toolset-config-panel.test.tsx b/apps/desktop/src/app/settings/toolset-config-panel.test.tsx index 0c1fc10030..8b5d5df91c 100644 --- a/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +++ b/apps/desktop/src/app/settings/toolset-config-panel.test.tsx @@ -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() + + 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>>, + 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()