fix(client-ui-plugin-config): refresh the key badge when the Host reports the credential changed

The card read the credential only when its settings scope published, and a
credential is not part of any settings section: a key written from the Models
page — which addresses the same reference — left this badge reporting a state
the Host had already replaced. It now re-reads on credentials/changed for the
reference it watches, and ignores the event for any other reference.
This commit is contained in:
Yichen Jiang
2026-08-11 18:00:24 +08:00
parent aa6abf218c
commit 3c8cf3a564
7 changed files with 81 additions and 4 deletions

View File

@@ -17,14 +17,15 @@ async function bench() {
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
ctx.provide('locale', locale)
const describeCredentials = vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } }))
ctx.provide('connection', {
isLoopback: true,
api: {
settings: { describe: vi.fn(() => Promise.resolve({ rpcId: 's', result: { ok: false, error: {} } })) },
credentials: { describe: vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } })) },
credentials: { describe: describeCredentials },
},
} as never)
return { ctx, slots: ctx.get('slots') as SlotsService }
return { ctx, slots: ctx.get('slots') as SlotsService, describeCredentials }
}
function declareRoot(slots: SlotsService): () => void {
@@ -76,6 +77,33 @@ describe('ui-plugin-config apply', () => {
}
})
it('re-reads the credential when the Host reports the watched reference changed', async () => {
const { ctx, slots, describeCredentials } = await bench()
declareRoot(slots)
await ctx.plugin({ inject: [...inject], apply }).await()
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalled() })
describeCredentials.mockClear()
// A key written on another surface changes no settings section, so this
// event is the only thing that reaches the card.
ctx.emit('credentials/changed', 'DEEPSEEK_API_KEY')
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalledTimes(1) })
})
it('ignores a credential change for a reference no card watches', async () => {
const { ctx, slots, describeCredentials } = await bench()
declareRoot(slots)
await ctx.plugin({ inject: [...inject], apply }).await()
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalled() })
describeCredentials.mockClear()
ctx.emit('credentials/changed', 'SOME_OTHER_KEY')
await Promise.resolve()
expect(describeCredentials).not.toHaveBeenCalled()
})
it('registers into a declaration that arrives after apply', async () => {
const { ctx, slots } = await bench()
await ctx.plugin({ inject: [...inject], apply }).await()

View File

@@ -436,6 +436,30 @@ describe('WebSearchCardController', () => {
expect(credentials.set).not.toHaveBeenCalled()
})
it('re-reads when the Host reports the watched reference changed', async () => {
const host = stubSettingsScope<WebSearchSettings>()
const credentials = credentialsApi(false)
const controller = new WebSearchCardController(host.scope, credentials.api)
host.publish({ status: 'ready', writable: true, value: {}, user: {} })
await vi.waitFor(() => { expect(credentials.describe).toHaveBeenCalled() })
credentials.describe.mockClear()
// Another reference is not this card's business.
controller.refreshCredential('OTHER_KEY')
expect(credentials.describe).not.toHaveBeenCalled()
// A key written on another surface reaches this card only through this signal.
credentials.describe.mockImplementation(() => Promise.resolve({
rpcId: 'c-1' as never,
result: { ok: true as const, value: { credentials: { DEEPSEEK_API_KEY: { configured: true, writable: true } } } },
}))
controller.refreshCredential('DEEPSEEK_API_KEY')
await vi.waitFor(() => {
expect(controller.inject().hooks.webSearchCard.getSnapshot().apiKeyConfigured).toBe(true)
})
})
it('addresses the reference the section declares rather than the default', async () => {
const host = stubSettingsScope<WebSearchSettings>()
const credentials = credentialsApi(false)