fix(runtime): refresh cached capability verdicts

This commit is contained in:
Jinjing
2026-07-10 17:37:02 -07:00
parent e1c16af879
commit 8873de70e5
2 changed files with 103 additions and 6 deletions
@@ -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',
+29 -6
View File
@@ -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<void>
@@ -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<string, RuntimeCompatibilityCacheEntry>()
@@ -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(