From 256f34f7d5de2378d16db9ae0db3c99fb614b4c8 Mon Sep 17 00:00:00 2001 From: Makito Date: Sun, 1 Mar 2026 00:26:56 +0700 Subject: [PATCH] feat(plugin-sdk): add degraded and withdrawn cap states --- .../plugin-sdk/src/plugin-host/core.test.ts | 59 +++++++++++++++++++ packages/plugin-sdk/src/plugin-host/core.ts | 26 ++++++++ .../apis/protocol/capabilities/index.ts | 2 +- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/plugin-sdk/src/plugin-host/core.test.ts b/packages/plugin-sdk/src/plugin-host/core.test.ts index 344fdfc23..671492a13 100644 --- a/packages/plugin-sdk/src/plugin-host/core.test.ts +++ b/packages/plugin-sdk/src/plugin-host/core.test.ts @@ -326,6 +326,65 @@ describe('for PluginHost', () => { })).rejects.toThrow('Capability `cap:missing` is not ready after 10ms.') }) + it('should support degraded and withdrawn capability states', () => { + const host = new PluginHost({ + runtime: 'electron', + transport: { kind: 'in-memory' }, + }) + + const announced = host.announceCapability('cap:dynamic', { source: 'announce' }) + expect(announced).toMatchObject({ + key: 'cap:dynamic', + state: 'announced', + metadata: { source: 'announce' }, + }) + + const degraded = host.markCapabilityDegraded('cap:dynamic', { reason: 'upstream-degraded' }) + expect(degraded).toMatchObject({ + key: 'cap:dynamic', + state: 'degraded', + metadata: { reason: 'upstream-degraded' }, + }) + expect(host.isCapabilityReady('cap:dynamic')).toBe(false) + + const withdrawn = host.withdrawCapability('cap:dynamic', { reason: 'disabled' }) + expect(withdrawn).toMatchObject({ + key: 'cap:dynamic', + state: 'withdrawn', + metadata: { reason: 'disabled' }, + }) + expect(host.isCapabilityReady('cap:dynamic')).toBe(false) + expect(host.listCapabilities()).toEqual(expect.arrayContaining([ + expect.objectContaining({ + key: 'cap:dynamic', + state: 'withdrawn', + }), + ])) + }) + + it('should resolve waits only when capability reaches ready state', async () => { + const host = new PluginHost({ + runtime: 'electron', + transport: { kind: 'in-memory' }, + }) + + host.markCapabilityDegraded('cap:unstable', { reason: 'booting' }) + const waiting = host.waitForCapability('cap:unstable', 2000) + + await new Promise(resolve => setTimeout(resolve, 20)) + host.withdrawCapability('cap:unstable', { reason: 'restarting' }) + + await new Promise(resolve => setTimeout(resolve, 20)) + host.markCapabilityReady('cap:unstable', { source: 'recovered' }) + + const resolved = await waiting + expect(resolved).toMatchObject({ + key: 'cap:unstable', + state: 'ready', + metadata: { source: 'recovered' }, + }) + }) + it('should preserve previous cwd when reloading plugin', async () => { const host = new PluginHost({ runtime: 'electron', diff --git a/packages/plugin-sdk/src/plugin-host/core.ts b/packages/plugin-sdk/src/plugin-host/core.ts index 44ac44501..9b22e1ca3 100644 --- a/packages/plugin-sdk/src/plugin-host/core.ts +++ b/packages/plugin-sdk/src/plugin-host/core.ts @@ -817,6 +817,32 @@ export class PluginHost { return descriptor } + markCapabilityDegraded(key: string, metadata?: Record) { + const current = this.capabilities.get(key) + const descriptor: CapabilityDescriptor = { + key, + state: 'degraded', + metadata: metadata ?? current?.metadata, + updatedAt: Date.now(), + } + + this.capabilities.set(key, descriptor) + return descriptor + } + + withdrawCapability(key: string, metadata?: Record) { + const current = this.capabilities.get(key) + const descriptor: CapabilityDescriptor = { + key, + state: 'withdrawn', + metadata: metadata ?? current?.metadata, + updatedAt: Date.now(), + } + + this.capabilities.set(key, descriptor) + return descriptor + } + listCapabilities() { return [...this.capabilities.values()] } diff --git a/packages/plugin-sdk/src/plugin/apis/protocol/capabilities/index.ts b/packages/plugin-sdk/src/plugin/apis/protocol/capabilities/index.ts index 52cd05f76..510366e2d 100644 --- a/packages/plugin-sdk/src/plugin/apis/protocol/capabilities/index.ts +++ b/packages/plugin-sdk/src/plugin/apis/protocol/capabilities/index.ts @@ -2,7 +2,7 @@ import { defineInvokeEventa } from '@moeru/eventa' export interface CapabilityDescriptor { key: string - state: 'announced' | 'ready' + state: 'announced' | 'ready' | 'degraded' | 'withdrawn' metadata?: Record updatedAt: number }