From 8873de70e5c323ef40a3bec44e6eda49342b9a4e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:10:21 -0700 Subject: [PATCH] fix(runtime): refresh cached capability verdicts --- .../src/runtime/runtime-rpc-client.test.ts | 74 +++++++++++++++++++ .../src/runtime/runtime-rpc-client.ts | 35 +++++++-- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/runtime/runtime-rpc-client.test.ts b/src/renderer/src/runtime/runtime-rpc-client.test.ts index 1394cac147d..d8c267ff46e 100644 --- a/src/renderer/src/runtime/runtime-rpc-client.test.ts +++ b/src/renderer/src/runtime/runtime-rpc-client.test.ts @@ -499,6 +499,80 @@ describe('runtime RPC client routing', () => { expect(statusCalls).toBe(2) }) + it('re-probes a missing capability on retry so a runtime upgrade can recover', async () => { + let statusCalls = 0 + runtimeEnvironmentCall.mockImplementation(() => { + statusCalls += 1 + return Promise.resolve({ + id: 'status', + ok: true, + result: { + runtimeId: 'remote-runtime', + graphStatus: 'ready', + runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION, + capabilities: statusCalls === 1 ? [] : ['linear.issue-attribute-filter.v1'] + }, + _meta: { runtimeId: 'remote-runtime' } + }) + }) + + await expect( + runtimeEnvironmentSupportsCapability('env-cap-upgrade', 'linear.issue-attribute-filter.v1') + ).resolves.toBe(false) + await expect( + runtimeEnvironmentSupportsCapability('env-cap-upgrade', 'linear.issue-attribute-filter.v1') + ).resolves.toBe(true) + expect(statusCalls).toBe(2) + }) + + it('expires a supported capability verdict so a runtime downgrade is detected', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(0)) + try { + let statusCalls = 0 + runtimeEnvironmentCall.mockImplementation(() => { + statusCalls += 1 + return Promise.resolve({ + id: 'status', + ok: true, + result: { + runtimeId: 'remote-runtime', + graphStatus: 'ready', + runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION, + capabilities: statusCalls === 1 ? ['linear.issue-attribute-filter.v1'] : [] + }, + _meta: { runtimeId: 'remote-runtime' } + }) + }) + + await expect( + runtimeEnvironmentSupportsCapability( + 'env-cap-downgrade', + 'linear.issue-attribute-filter.v1' + ) + ).resolves.toBe(true) + vi.setSystemTime(new Date(59_999)) + await expect( + runtimeEnvironmentSupportsCapability( + 'env-cap-downgrade', + 'linear.issue-attribute-filter.v1' + ) + ).resolves.toBe(true) + vi.setSystemTime(new Date(60_000)) + await expect( + runtimeEnvironmentSupportsCapability( + 'env-cap-downgrade', + 'linear.issue-attribute-filter.v1' + ) + ).resolves.toBe(false) + expect(statusCalls).toBe(2) + } finally { + vi.useRealTimers() + } + }) + it('rejects missing advertised runtime capabilities with the caller message', async () => { runtimeEnvironmentCall.mockResolvedValue({ id: 'status', diff --git a/src/renderer/src/runtime/runtime-rpc-client.ts b/src/renderer/src/runtime/runtime-rpc-client.ts index 7141c49c7bc..c7bcfefed68 100644 --- a/src/renderer/src/runtime/runtime-rpc-client.ts +++ b/src/renderer/src/runtime/runtime-rpc-client.ts @@ -9,6 +9,9 @@ export type RuntimeClientTarget = { kind: 'local' } | { kind: 'environment'; env const RUNTIME_COMPATIBILITY_CACHE_MAX = 32 const RECENT_RUNTIME_COMPATIBILITY_FAILURE_TTL_MS = 60_000 +// Why: a saved environment can restart into a different Orca version without +// changing ids; capability verdicts must eventually follow that version change. +const RUNTIME_CAPABILITY_STATUS_TTL_MS = 60_000 type RuntimeCompatibilityCacheEntry = { check: Promise @@ -17,6 +20,7 @@ type RuntimeCompatibilityCacheEntry = { // the probe is in flight, so a recovery clear can drop a doomed pending probe. provenCompatible: boolean status: RuntimeStatus | null + statusCheckedAt: number | null } const runtimeCompatibilityChecks = new Map() @@ -110,7 +114,8 @@ async function ensureRuntimeEnvironmentCompatible( check: Promise.resolve(), failedAt: null, provenCompatible: false, - status: null + status: null, + statusCheckedAt: null } const check = (async () => { const response = await window.api.runtimeEnvironments.call({ @@ -123,6 +128,7 @@ async function ensureRuntimeEnvironmentCompatible( ) assertRuntimeStatusCompatible(status) entry.status = status + entry.statusCheckedAt = Date.now() })() entry.check = check rememberRuntimeEnvironmentCompatibility(environmentId, entry) @@ -215,7 +221,8 @@ export function markRuntimeEnvironmentCompatible(environmentId: string): void { check: Promise.resolve(), failedAt: null, provenCompatible: true, - status: null + status: null, + statusCheckedAt: null }) } @@ -236,7 +243,8 @@ export async function getRuntimeEnvironmentStatus( check: Promise.resolve(), failedAt: null, provenCompatible: true, - status + status, + statusCheckedAt: Date.now() }) return status } @@ -254,15 +262,30 @@ export async function runtimeEnvironmentSupportsCapability( if (cached && cached.failedAt === null) { try { await cached.check - if (cached.status) { - return cached.status.capabilities?.includes(capability) === true + if ( + runtimeCompatibilityChecks.get(trimmed) === cached && + cached.status && + cached.statusCheckedAt !== null && + Date.now() - cached.statusCheckedAt < RUNTIME_CAPABILITY_STATUS_TTL_MS + ) { + const supported = cached.status.capabilities?.includes(capability) === true + if (!supported) { + // Why: an unsupported verdict must not survive a remote upgrade. The + // next explicit retry re-probes instead of pinning the old capability set. + runtimeCompatibilityChecks.delete(trimmed) + } + return supported } } catch { // Fall through to a fresh status.get that refreshes the cache. } } const status = await getRuntimeEnvironmentStatus(trimmed, timeoutMs) - return status.capabilities?.includes(capability) === true + const supported = status.capabilities?.includes(capability) === true + if (!supported && runtimeCompatibilityChecks.get(trimmed)?.status === status) { + runtimeCompatibilityChecks.delete(trimmed) + } + return supported } export async function assertRuntimeEnvironmentCapability(