refactor(runtime): declare the host-status entry once instead of per consumer (#20262)

This commit is contained in:
Neil
2026-09-17 00:11:45 -07:00
committed by GitHub
parent 560c42e1d1
commit 779667c1e7
12 changed files with 100 additions and 48 deletions
@@ -30,7 +30,7 @@ import type { SshConnectionState } from '../../../../shared/ssh-types'
import type { ProjectHostSetup } from '../../../../shared/project-types'
import type { Repo } from '../../../../shared/repo-types'
import type { Worktree } from '../../../../shared/worktree/types'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
import type { AutomationRowAction } from './automation-captured-owner'
import type { AutomationHostTarget } from './automation-host-client'
@@ -70,10 +70,7 @@ export type AutomationListLocalRowProps = {
worktreeForRow?: (row: AutomationListRow, repo: Repo | undefined) => Worktree | undefined
projectHostSetups: readonly ProjectHostSetup[]
sshConnectionStates: ReadonlyMap<string, Pick<SshConnectionState, 'status'>>
runtimeStatusByEnvironmentId: ReadonlyMap<
string,
{ status: RuntimeStatus | null; checkedAt: number }
>
runtimeStatusByEnvironmentId: ReadonlyMap<string, RuntimeEnvironmentStatus>
hostTargetFor: (row: AutomationListRow) => AutomationHostTarget | null
automationSourceHostAvailabilityByRowKey: ReadonlyMap<string, TaskSourceHostAvailability[]>
hostLabelById?: ReadonlyMap<string, string>
@@ -11,7 +11,7 @@ import type { SshConnectionState } from '../../../../shared/ssh-types'
import type { ProjectHostSetup } from '../../../../shared/project-types'
import type { Repo } from '../../../../shared/repo-types'
import type { Worktree } from '../../../../shared/worktree/types'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
import type { AutomationRowAction } from './automation-captured-owner'
import type { AutomationHostTarget } from './automation-host-client'
@@ -53,7 +53,7 @@ import { AutomationListToolbar } from './AutomationListToolbar'
const TEMPLATE_EMPTY_STATES: ReadonlySet<string> = new Set(['host-empty', 'all-hosts-empty'])
const EMPTY_AUTOMATION_RUNS: ReadonlyMap<string, AutomationRun> = new Map()
type AutomationsListPanelProps = {
export type AutomationsListPanelProps = {
hasListItems: boolean
hasFilteredListItems: boolean
listSearchQuery: string
@@ -83,10 +83,7 @@ type AutomationsListPanelProps = {
worktreeForRow?: (row: AutomationListRow, repo: Repo | undefined) => Worktree | undefined
projectHostSetups: readonly ProjectHostSetup[]
sshConnectionStates: ReadonlyMap<string, Pick<SshConnectionState, 'status'>>
runtimeStatusByEnvironmentId: ReadonlyMap<
string,
{ status: RuntimeStatus | null; checkedAt: number }
>
runtimeStatusByEnvironmentId: ReadonlyMap<string, RuntimeEnvironmentStatus>
hostTargetFor: (row: AutomationListRow) => AutomationHostTarget | null
automationSourceHostAvailabilityByRowKey: ReadonlyMap<string, TaskSourceHostAvailability[]>
hostLabelById?: ReadonlyMap<string, string>
@@ -1,7 +1,7 @@
import { TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import type { Automation } from '../../../../shared/automations-types'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
@@ -20,10 +20,7 @@ export function getRepoBackedAutomationSourceContext(
export function getRuntimeSourceHostAvailability(
context: TaskSourceContext,
runtimeStatusByEnvironmentId: ReadonlyMap<
string,
{ status: RuntimeStatus | null; checkedAt: number }
>
runtimeStatusByEnvironmentId: ReadonlyMap<string, RuntimeEnvironmentStatus>
): TaskSourceHostAvailability | null {
const parsed = parseExecutionHostId(context.hostId)
if (parsed?.kind !== 'runtime') {
@@ -11,9 +11,9 @@ import {
import type { AutomationHostTarget } from './automation-host-client'
import type { SshConnectionState } from '../../../../shared/ssh-types'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { ProjectHostSetup } from '../../../../shared/project-types'
import type { Repo } from '../../../../shared/repo-types'
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
import type { Worktree } from '../../../../shared/worktree/types'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
@@ -51,10 +51,7 @@ type AutomationTargetAvailabilityArgs = {
workspace: Worktree | null | undefined
projectHostSetups: readonly ProjectHostSetup[]
sshConnectionStates: ReadonlyMap<string, Pick<SshConnectionState, 'status'>>
runtimeStatusByEnvironmentId?: ReadonlyMap<
string,
{ status: RuntimeStatus | null; checkedAt: number }
>
runtimeStatusByEnvironmentId?: ReadonlyMap<string, RuntimeEnvironmentStatus>
automationHostTarget?: AutomationHostTarget | null
sourceHostAvailability?: readonly TaskSourceHostAvailability[]
}
@@ -267,9 +264,7 @@ function getAutomationSourceProviderLabel(provider: TaskSourceContext['provider'
export function getRuntimeAutomationAvailability(
environmentId: string,
runtimeStatusByEnvironmentId:
| ReadonlyMap<string, { status: RuntimeStatus | null; checkedAt: number }>
| undefined
runtimeStatusByEnvironmentId: ReadonlyMap<string, RuntimeEnvironmentStatus> | undefined
): AutomationTargetAvailability {
const entry = runtimeStatusByEnvironmentId?.get(environmentId)
if (!entry) {
@@ -88,6 +88,7 @@ describe('getPaletteHostBadge', () => {
[
'env-1',
{
checkedAt: 0,
status: {
runtimeId: 'rt',
rendererGraphEpoch: 0,
@@ -115,6 +115,7 @@ describe('sidebar host options', () => {
[
'runtime-1',
{
checkedAt: 0,
status: {
runtimeId: 'rt',
rendererGraphEpoch: 0,
@@ -147,6 +148,7 @@ describe('sidebar host options', () => {
[
'runtime-1',
{
checkedAt: 0,
status: {
runtimeId: 'rt',
rendererGraphEpoch: 0,
@@ -12,8 +12,8 @@ import {
} from '../../../../shared/execution-host-registry'
import type { RuntimeCompatVerdict } from '../../../../shared/protocol-compat'
import type { SshConnectionState, SshConnectionStatus } from '../../../../shared/ssh-types'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
import { translate } from '@/i18n/i18n'
export type SidebarHostOption = {
@@ -43,10 +43,7 @@ export function buildSidebarHostOptions(args: {
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
// Why: live per-environment runtime status lets the registry surface compat
// verdicts and blocked health in the sidebar without re-probing servers.
runtimeStatusByEnvironmentId?: ReadonlyMap<
string,
{ status?: RuntimeStatus | null; appVersion?: string | null }
>
runtimeStatusByEnvironmentId?: ReadonlyMap<string, RuntimeEnvironmentStatus>
runtimeEnvironments?: readonly Pick<PublicKnownRuntimeEnvironment, 'id' | 'name'>[]
// Why: per-host display-label overrides rename hosts everywhere the sidebar
// options feed (host headers, scope picker, focus menu).
@@ -0,0 +1,56 @@
// The store's host-status entry is one shape, and every reader of the map has to accept exactly
// it. A narrower local copy still typechecks against the store — the fields it is missing are all
// optional — so it freezes at the vintage it was written at and never fails. #17710 added
// `remoteControl` and #20003 added `snapshot`; the copies these assertions cover were not updated
// either time. Like the other typed-contract tests, they fail at typecheck, not at runtime.
import { describe, expectTypeOf, it } from 'vitest'
import type { buildExecutionHostRegistry } from '../../../../shared/execution-host-registry'
import type { buildSidebarHostOptions } from '@/components/sidebar/sidebar-host-options'
import type { getRuntimeAutomationAvailability } from '@/components/automations/automation-target-availability'
import type { getRuntimeSourceHostAvailability } from '@/components/automations/automation-source-context'
import type { AutomationListLocalRowProps } from '@/components/automations/AutomationListLocalRow'
import type { AutomationsListPanelProps } from '@/components/automations/AutomationsListPanel'
import type { RuntimeEnvironmentStatus } from './runtime-status-types'
// Compare the entry each reader accepts rather than the surrounding map, so a mismatch names the
// field that drifted instead of every ReadonlyMap method signature.
type AcceptedEntry<T> = NonNullable<T> extends ReadonlyMap<string, infer TEntry> ? TEntry : never
type RegistryEntry = AcceptedEntry<
Parameters<typeof buildExecutionHostRegistry>[0]['runtimeStatusByEnvironmentId']
>
type SidebarEntry = AcceptedEntry<
Parameters<typeof buildSidebarHostOptions>[0]['runtimeStatusByEnvironmentId']
>
type AutomationEntry = AcceptedEntry<Parameters<typeof getRuntimeAutomationAvailability>[1]>
type SourceContextEntry = AcceptedEntry<Parameters<typeof getRuntimeSourceHostAvailability>[1]>
type LocalRowEntry = AcceptedEntry<AutomationListLocalRowProps['runtimeStatusByEnvironmentId']>
type ListPanelEntry = AcceptedEntry<AutomationsListPanelProps['runtimeStatusByEnvironmentId']>
describe('every reader of the runtime host status map accepts the store entry', () => {
it('holds for the execution-host registry', () => {
expectTypeOf<RegistryEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
it('holds for the sidebar host options', () => {
expectTypeOf<SidebarEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
it('holds for runtime automation availability', () => {
expectTypeOf<AutomationEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
// These three only forward the map, which is exactly how a stale copy survives: nothing they
// do with it can fail, so the drift surfaces in whatever they hand it to.
it('holds for the automation source-context reader', () => {
expectTypeOf<SourceContextEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
it('holds for the automation list row', () => {
expectTypeOf<LocalRowEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
it('holds for the automations list panel', () => {
expectTypeOf<ListPanelEntry>().toEqualTypeOf<RuntimeEnvironmentStatus>()
})
})
@@ -1,15 +1,10 @@
import type { RuntimeHostStatusSnapshot } from '../../../../shared/runtime-host-status'
import type {
RuntimeEnvironmentStatus,
RuntimeHostStatusSnapshot
} from '../../../../shared/runtime-host-status'
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
export type RuntimeEnvironmentStatus = {
snapshot?: RuntimeHostStatusSnapshot
status: RuntimeStatus | null
remoteControl?: RuntimeStatus['remoteControl'] | null
appVersion?: string | null
checkedAt: number
connectionGeneration?: number
}
export type { RuntimeEnvironmentStatus }
export type RuntimeStatusSlice = {
readRuntimeHostStatusSnapshots: () => Promise<void>
@@ -121,6 +121,7 @@ describe('execution host registry', () => {
[
'builder',
{
checkedAt: 0,
appVersion: '1.8.0',
status: {
runtimeId: 'runtime-builder',
@@ -139,6 +140,7 @@ describe('execution host registry', () => {
[
'old-server',
{
checkedAt: 0,
appVersion: '1.6.0',
status: {
runtimeId: 'runtime-old',
@@ -187,6 +189,7 @@ describe('execution host registry', () => {
[
'dev-box',
{
checkedAt: 0,
status: {
runtimeId: 'runtime-dev',
rendererGraphEpoch: 1,
@@ -229,6 +232,7 @@ describe('execution host registry', () => {
[
'dev-box',
{
checkedAt: 0,
status: null,
remoteControl: {
state: 'ready',
@@ -264,6 +268,7 @@ describe('execution host registry', () => {
[
'vm-runtime',
{
checkedAt: 0,
status: {
runtimeId: 'runtime-vm',
rendererGraphEpoch: 1,
@@ -340,6 +345,7 @@ describe('execution host registry', () => {
[
'gpu',
{
checkedAt: 0,
appVersion: '1.8.0',
status: {
runtimeId: 'runtime-gpu',
@@ -381,6 +387,7 @@ it('keeps an initial unknown-transport verification connecting', () => {
[
'host',
{
checkedAt: 0,
status: null,
snapshot: {
environmentId: 'host',
+2 -9
View File
@@ -1,4 +1,4 @@
import type { RuntimeHostStatusSnapshot } from './runtime-host-status'
import type { RuntimeEnvironmentStatus } from './runtime-host-status'
import {
LOCAL_EXECUTION_HOST_ID,
getLocalExecutionHostLabel,
@@ -49,14 +49,7 @@ type RuntimeEnvironmentSummary = {
source?: RuntimeEnvironmentSource
}
type RuntimeHostStatus = {
snapshot?: RuntimeHostStatusSnapshot
status?: RuntimeStatus | null
remoteControl?: RuntimeStatus['remoteControl'] | null
appVersion?: string | null
}
type RuntimeStatusByEnvironmentId = ReadonlyMap<string, RuntimeHostStatus>
type RuntimeStatusByEnvironmentId = ReadonlyMap<string, RuntimeEnvironmentStatus>
export type ExecutionHostSource = 'configured-only' | 'include-references'
+15
View File
@@ -17,6 +17,21 @@ export type RuntimeHostStatusSnapshot = {
retired?: true
}
/**
* One client-side record of a host's last status probe: the projected `status`, and the
* `snapshot` evidence for what that projection is worth. Declared once rather than duck-typed
* per consumer — every field added here has been optional, so a local structural copy keeps
* typechecking against the store while silently missing whatever landed after it was written.
*/
export type RuntimeEnvironmentStatus = {
snapshot?: RuntimeHostStatusSnapshot
status: RuntimeStatus | null
remoteControl?: RuntimeStatus['remoteControl'] | null
appVersion?: string | null
checkedAt: number
connectionGeneration?: number
}
export type RuntimeHostStatusResponse = RuntimeRpcResponse<RuntimeStatus>
export function runtimeHostStatusFailure(code: string, message: string): RuntimeRpcFailure {