mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 16:02:42 +00:00
fix(runtime): backfill SSH states for targets learned without a state read
A failed-connect resync labelled a target with no ssh.getState, and the new metadata-refresh gates then skipped it forever: no SSH chip and every worktree mutation to that host failed the connection-generation check. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -190,6 +190,23 @@ describe('hydrateRuntimeEnvironmentSshState', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('reads state for a labelled target the bucket never recorded a state for', async () => {
|
||||
const envId = nextEnvId()
|
||||
// A failed-connect resync can label a target without ever reading its state.
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshTargetsMetadata(envId, [{ id: 'ssh-new', label: 'new box' }])
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-new', label: 'new box' }],
|
||||
states: { 'ssh-new': connState('ssh-new') }
|
||||
})
|
||||
|
||||
await refreshRuntimeEnvironmentSshTargetMetadata(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.connectionStates.get('ssh-new')?.status).toBe('connected')
|
||||
})
|
||||
|
||||
it('prunes removed targets and fetches state only for newly discovered targets', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore.getState().setEnvironmentSshTargetsMetadata(envId, [
|
||||
@@ -558,4 +575,22 @@ describe('resyncRuntimeEnvironmentSshTargets', () => {
|
||||
expect(bucket?.targetLabels.get('ssh-live')).toBe('devbox')
|
||||
expect(bucket?.targetsHydrated).toBe(true)
|
||||
})
|
||||
|
||||
it('reads the connection state of a host re-added under a new target id', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshTargetsMetadata(envId, [{ id: 'ssh-old', label: 'devbox' }])
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-new', label: 'devbox' }],
|
||||
labels: { 'ssh-old': 'devbox' },
|
||||
states: { 'ssh-new': connState('ssh-new') }
|
||||
})
|
||||
|
||||
await resyncRuntimeEnvironmentSshTargets(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetLabels.get('ssh-new')).toBe('devbox')
|
||||
expect(bucket?.connectionStates.get('ssh-new')?.status).toBe('connected')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -130,16 +130,19 @@ async function runEnvironmentSshTargetMetadataRefresh(environmentId: string): Pr
|
||||
const metadataChanged =
|
||||
!bucket?.targetsHydrated || !sshTargetLabelsEqual(bucket.targetLabels, targets)
|
||||
useAppStore.getState().setEnvironmentSshTargetsMetadata(environmentId, targets, generation)
|
||||
const priorTargetIds = new Set(bucket?.targetLabels.keys() ?? [])
|
||||
// Why: read states after the write — a resync racing this fetch can label a target that never had one read.
|
||||
const knownStates = useAppStore.getState().sshStateByEnvironment.get(environmentId)
|
||||
?.connectionStates
|
||||
const needStateRead = targets.filter(
|
||||
(target) => !priorTargetIds.has(target.id) || !knownStates?.has(target.id)
|
||||
)
|
||||
if (!metadataChanged) {
|
||||
await fetchEnvironmentSshConnectionStates(environmentId, needStateRead, generation)
|
||||
return
|
||||
}
|
||||
await syncEnvironmentRemovedSshTargetLabels(environmentId, generation)
|
||||
const priorTargetIds = new Set(bucket?.targetLabels.keys() ?? [])
|
||||
await fetchEnvironmentSshConnectionStates(
|
||||
environmentId,
|
||||
targets.filter((target) => !priorTargetIds.has(target.id)),
|
||||
generation
|
||||
)
|
||||
await fetchEnvironmentSshConnectionStates(environmentId, needStateRead, generation)
|
||||
}
|
||||
|
||||
function requestSshRefreshRerun(entry: SshRefreshEntry, kind: SshRefreshKind): void {
|
||||
@@ -287,10 +290,9 @@ export async function connectRuntimeEnvironmentSshTarget(
|
||||
}
|
||||
|
||||
/** Resyncs the environment's target metadata after a failed connect so a
|
||||
* stale overlay converges to the ghost/re-adopted state (STA-1468). */
|
||||
* stale overlay converges to the ghost/re-adopted state (STA-1468). Full
|
||||
* hydration, not metadata alone: a host re-added under a new target id must
|
||||
* land with a connection state or its chip and mutation gate stay dead. */
|
||||
export async function resyncRuntimeEnvironmentSshTargets(environmentId: string): Promise<void> {
|
||||
await syncEnvironmentSshTargetMetadata(
|
||||
environmentId,
|
||||
getEnvironmentSshStateGeneration(environmentId)
|
||||
)
|
||||
await hydrateRuntimeEnvironmentSshState(environmentId, { force: true })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user