From ab22af5841bdc3979ecb6722bf423ff643d5aee2 Mon Sep 17 00:00:00 2001 From: Makito Date: Sat, 28 Feb 2026 23:28:06 +0700 Subject: [PATCH] feat(plugin-sdk): utilize details prop for richer o11y on waiting-deps --- .../plugin-sdk/src/plugin-host/core.test.ts | 45 +++++++++++++++++++ packages/plugin-sdk/src/plugin-host/core.ts | 11 ++++- 2 files changed, 55 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 a4fc86ed3..344fdfc23 100644 --- a/packages/plugin-sdk/src/plugin-host/core.test.ts +++ b/packages/plugin-sdk/src/plugin-host/core.test.ts @@ -1,6 +1,7 @@ import { join } from 'node:path' import { createContext, defineEventa, defineInvokeHandler } from '@moeru/eventa' +import { moduleStatus } from '@proj-airi/plugin-protocol/types' import { describe, expect, it, vi } from 'vitest' import { FileSystemLoader, PluginHost } from '.' @@ -268,6 +269,50 @@ describe('for PluginHost', () => { expect(session.phase).toBe('ready') }) + it('should emit dependency wait details while waiting for required capabilities', async () => { + const host = new PluginHost({ + runtime: 'electron', + transport: { kind: 'in-memory' }, + }) + + const session = await host.load(testManifest, { cwd: '' }) + const statusEvents: Array<{ body?: Record }> = [] + session.channels.host.on(moduleStatus, (payload) => { + statusEvents.push(payload as unknown as { body?: Record }) + }) + + const started = host.init(session.id, { + requiredCapabilities: ['cap:custom'], + capabilityWaitTimeoutMs: 2000, + }) + + await new Promise(resolve => setTimeout(resolve, 20)) + + const waitingStatus = statusEvents.find((event) => { + const body = event.body + return body?.phase === 'preparing' && typeof body.reason === 'string' && body.reason.includes('Waiting for capabilities:') + }) + + expect(waitingStatus).toBeDefined() + expect(waitingStatus?.body).toMatchObject({ + phase: 'preparing', + details: { + lifecyclePhase: 'waiting-deps', + requiredCapabilities: ['cap:custom'], + unresolvedCapabilities: ['cap:custom'], + timeoutMs: 2000, + }, + }) + + reportPluginCapability(host, { + key: 'cap:custom', + state: 'ready', + metadata: { source: 'test' }, + }) + const initialized = await started + expect(initialized.phase).toBe('ready') + }) + it('should fail when required capabilities timeout', 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 cee77c4c2..44ac44501 100644 --- a/packages/plugin-sdk/src/plugin-host/core.ts +++ b/packages/plugin-sdk/src/plugin-host/core.ts @@ -635,14 +635,23 @@ export class PluginHost { // Optional dependency gate before plugin-owned initialization. if (options.requiredCapabilities?.length) { + const capabilityTimeoutMs = options.capabilityWaitTimeoutMs ?? 15000 + const unresolvedCapabilities = options.requiredCapabilities.filter(key => !this.isCapabilityReady(key)) assertTransition(session, 'waiting-deps') session.channels.host.emit(moduleStatus, { identity: session.identity, phase: 'preparing', reason: `Waiting for capabilities: ${options.requiredCapabilities.join(', ')}`, + details: { + // For richer observability + lifecyclePhase: 'waiting-deps', + requiredCapabilities: options.requiredCapabilities, + unresolvedCapabilities, + timeoutMs: capabilityTimeoutMs, + }, }) - await this.waitForCapabilities(options.requiredCapabilities, options.capabilityWaitTimeoutMs) + await this.waitForCapabilities(options.requiredCapabilities, capabilityTimeoutMs) assertTransition(session, 'prepared') session.channels.host.emit(modulePrepared, { identity: session.identity,