From bba74159f804844bd50fc9094eb19c6d32b302ac Mon Sep 17 00:00:00 2001 From: wxr <51250936+wxrbyte@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:17:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(diagnostics):=20=E8=AF=8A=E6=96=AD=E5=8C=85?= =?UTF-8?q?=E7=9A=84=E5=88=B8=E5=95=86=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=9C=9F=E5=AE=9E=E6=8E=A2=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collectDiagnostics 用 providerSummaries.some(id === 'longbridge-broker') 判断 券商是否连接,但 providerSummaries 只是「已注册 provider」列表,而 broker 在构造 函数里被无条件注册 —— 该表达式恒为 true,账户数还是写死的字面量 1,从未查询过 真实账户。改为按 kind === 'broker-account' 找到 broker 并调用 accounts(),与 entryFor(Connections 界面)同源:未登录时 accounts() 返回 ok:false,据实上报 未连接、0 个账户。 修复前 12 pass / 1 fail → 修复后 13 pass / 0 fail。 Closes #245 --- apps/electron/src/main/kernelHost.test.ts | 37 ++++++++++++++++++++++- apps/electron/src/main/kernelHost.ts | 32 ++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/electron/src/main/kernelHost.test.ts b/apps/electron/src/main/kernelHost.test.ts index 0a5684e..7464970 100644 --- a/apps/electron/src/main/kernelHost.test.ts +++ b/apps/electron/src/main/kernelHost.test.ts @@ -6,6 +6,8 @@ let lastKernelOptions: Record | null = null; let lastMarketData: FakeMarketDataService | null = null; let lastAutomationContext: unknown = null; let forwardedEvents: unknown[] = []; +// Providers the mocked ProviderRouter reports as registered (default: none). +let routerProviderList: Array> = []; const routerFetchers = { getQuote: async () => ({ symbol: 'AAPL.US' }) }; class FakeMarketDataService { @@ -86,6 +88,8 @@ class FakeAgentKernel { mock.module('electron', () => ({ app: { getPath: () => '/tmp/finagent-test', + getVersion: () => '0.0.0-test', + isPackaged: false, }, safeStorage: { isEncryptionAvailable: () => true, @@ -177,7 +181,7 @@ mock.module('@finagent/shared', () => ({ register = () => undefined; setRouting = () => undefined; get = () => undefined; - list = () => []; + list = () => routerProviderList; coverage = () => []; }, ConnectionStore: class { @@ -352,6 +356,7 @@ beforeEach(() => { lastMarketData = null; lastAutomationContext = null; forwardedEvents = []; + routerProviderList = []; }); afterEach(() => { @@ -436,6 +441,36 @@ describe('AgentKernelHost', () => { host.dispose(); }); + it('reports the broker as disconnected when the broker has no signed-in account', async () => { + routerProviderList = [ + { + id: 'longbridge-broker', + kind: 'broker-account', + accounts: async () => ({ ok: false, error: { code: 'AUTH_EXPIRED' } }), + }, + ]; + const host = new AgentKernelHost(); + + const bundle = await host.collectDiagnostics(); + expect(bundle.providers.broker).toEqual({ connected: false, accountCount: 0 }); + host.dispose(); + }); + + it('reports the connected broker account count from the broker itself', async () => { + routerProviderList = [ + { + id: 'longbridge-broker', + kind: 'broker-account', + accounts: async () => ({ ok: true, data: [{ id: 'acct-1', name: 'Default (acct-1)' }] }), + }, + ]; + const host = new AgentKernelHost(); + + const bundle = await host.collectDiagnostics(); + expect(bundle.providers.broker).toEqual({ connected: true, accountCount: 1 }); + host.dispose(); + }); + it('forwards kernel agent events to the attached window', async () => { let subscriber: ((event: AgentEvent) => void) | null = null; fakeRuns.subscribe = (listener) => { diff --git a/apps/electron/src/main/kernelHost.ts b/apps/electron/src/main/kernelHost.ts index 63bea45..93c3f43 100644 --- a/apps/electron/src/main/kernelHost.ts +++ b/apps/electron/src/main/kernelHost.ts @@ -1579,6 +1579,12 @@ export class AgentKernelHost { } catch { // Provider wiring lands with the connector slice; empty until then. } + // `providerSummaries` lists every REGISTERED provider, and the broker is + // registered unconditionally in the constructor — so "is the broker in the + // list" is a constant, not a connection state. Ask the broker itself, the + // way `entryFor` does for the Connections surface. + const { connected: brokerConnected, accountCount: brokerAccountCount } = + await this.brokerAccountFacts(); return collectDiagnostics({ version: app.getVersion(), os: process.platform, @@ -1589,8 +1595,8 @@ export class AgentKernelHost { llmProviderId: llmState?.model?.provider ?? null, llmModel: llmState?.model?.id ?? null, financialProviders: providerSummaries, - brokerConnected: providerSummaries.some((p) => p.id === 'longbridge-broker'), - brokerAccountCount: providerSummaries.some((p) => p.id === 'longbridge-broker') ? 1 : 0, + brokerConnected, + brokerAccountCount, skillsLoadedCount: this.skillHub.listSkills().length, capabilities: this.registry, resources: { dev: !app.isPackaged, root: getRuntimeRoot() }, @@ -2461,6 +2467,28 @@ export class AgentKernelHost { } } + /** + * Broker facts for the diagnostics bundle. Registration is not connection: + * the broker provider is registered unconditionally at construction, so its + * presence in the provider list says nothing about whether an account is + * actually signed in. Ask the broker for its accounts — the same call + * `entryFor` makes for the Connections surface. + */ + private async brokerAccountFacts(): Promise<{ connected: boolean; accountCount: number }> { + const broker = this.providerRouter.list().find((provider) => provider.kind === 'broker-account'); + if (!broker || broker.kind !== 'broker-account') { + return { connected: false, accountCount: 0 }; + } + try { + const accounts = await broker.accounts(); + if (!accounts.ok) return { connected: false, accountCount: 0 }; + return { connected: accounts.data.length > 0, accountCount: accounts.data.length }; + } catch { + // A broker that cannot answer is reported as not connected. + return { connected: false, accountCount: 0 }; + } + } + private listFinancialProviders(): FinancialProviderSummary[] { return this.providerRouter.list().map((provider) => { const coverage = this.providerRouter.coverage().find((c) => c.providerId === provider.id);