From d8035680b9d642e619311b23b4a3a14bd7955d85 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 12 Aug 2026 21:24:53 +0800 Subject: [PATCH] test(settings): cover the section directory's disposal, stale-read, and invalidation paths The per-file coverage gate flagged four uncovered locations the new section directory introduced: refresh() after disposal, a read superseded by a newer one, and the two invalidation handlers that make the served namespaces re-read (settings/document-updated and connection/reset). --- .../tests/apply.client.spec.ts | 27 ++++++++++++++ .../tests/stores.client.spec.ts | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/client/ui-plugin-config/tests/apply.client.spec.ts b/packages/client/ui-plugin-config/tests/apply.client.spec.ts index a7eeb952b9..63b3f30406 100644 --- a/packages/client/ui-plugin-config/tests/apply.client.spec.ts +++ b/packages/client/ui-plugin-config/tests/apply.client.spec.ts @@ -116,6 +116,33 @@ describe('ui-plugin-config apply', () => { } }) + it('re-reads the served namespaces when the Host commits a settings document', async () => { + // Which namespaces the Host serves is a registration fact the wire never + // announces on its own, so the section rides the invalidation that can + // accompany a changed composition. + const { ctx, slots, describeSettings } = await bench(['bash']) + declareRoot(slots) + await ctx.plugin({ inject: [...inject], apply }).await() + await vi.waitFor(() => { expect(describeSettings).toHaveBeenCalled() }) + describeSettings.mockClear() + + ctx.remote.$dispatch('settings/document-updated', ['bash', 1]) + + await vi.waitFor(() => { expect(describeSettings).toHaveBeenCalled() }) + }) + + it('re-reads the served namespaces after a reconnect', async () => { + const { ctx, slots, describeSettings } = await bench(['bash']) + declareRoot(slots) + await ctx.plugin({ inject: [...inject], apply }).await() + await vi.waitFor(() => { expect(describeSettings).toHaveBeenCalled() }) + describeSettings.mockClear() + + ctx.emit('connection/reset') + + await vi.waitFor(() => { expect(describeSettings).toHaveBeenCalled() }) + }) + it('re-reads the credential when the Host reports the watched reference changed', async () => { const { ctx, slots, describeCredentials } = await bench() declareRoot(slots) diff --git a/packages/client/ui-plugin-config/tests/stores.client.spec.ts b/packages/client/ui-plugin-config/tests/stores.client.spec.ts index 09be456aa6..30a537a38f 100644 --- a/packages/client/ui-plugin-config/tests/stores.client.spec.ts +++ b/packages/client/ui-plugin-config/tests/stores.client.spec.ts @@ -622,6 +622,41 @@ describe('PluginConfigSectionController', () => { expect(settings.describe).not.toHaveBeenCalled() }) + it('ignores a slot-ledger change that arrives after disposal', async () => { + const settings = settingsApi(['bash']) + let entries = ledger() + const controller = new PluginConfigSectionController(settings.api, () => entries) + await controller.load() + + controller.dispose() + entries = ledger('bash') + controller.refresh() + + expect(controller.inject().hooks.pluginConfigSection.getSnapshot().namespaces).toEqual([]) + }) + + it('drops a read a newer one superseded', async () => { + // The section re-reads on every settings-document invalidation, so a slow + // first answer must not overwrite the newer one that already landed. + const settings = settingsApi(['bash']) + const controller = new PluginConfigSectionController(settings.api, () => ledger('bash', 'agent-loop')) + const slow = Promise.withResolvers() + settings.describe.mockReturnValueOnce(slow.promise as never) + const stale = controller.load() + + await controller.load() + expect(controller.inject().hooks.pluginConfigSection.getSnapshot().namespaces).toEqual(['bash']) + slow.resolve({ + rpcId: 's-0', + result: { ok: true, value: { writable: true, hasDocument: true, namespaces: [ + { ns: 'agent-loop', schema: {}, value: {}, applies: 'live', secrets: [], revision: 0 }, + ] } }, + }) + await stale + + expect(controller.inject().hooks.pluginConfigSection.getSnapshot().namespaces).toEqual(['bash']) + }) + it('reports the Host answered even when it serves nothing this section shows', async () => { const settings = settingsApi(['ui-theme']) const controller = new PluginConfigSectionController(settings.api, () => ledger('bash'))