fix(relay): scope identity evidence backstop to visible clients

This commit is contained in:
Merge Sim
2026-08-31 13:54:15 -07:00
parent aaa20293bf
commit c819af29a3
2 changed files with 49 additions and 7 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import * as ptyShellUtils from './pty-shell-utils'
import * as processTableSnapshot from '../shared/process-table-snapshot'
const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({
mockPtySpawn: vi.fn(),
@@ -181,6 +182,33 @@ describe('PtyHandler', () => {
expect(dispatcher.notify).not.toHaveBeenCalledWith('pty.data', expect.anything())
})
it('keeps stale replay bytes out of the live identity scanner', async () => {
let dataCallback: ((data: string) => void) | undefined
mockPtySpawn.mockReturnValue({
...mockPtyInstance,
onData: vi.fn((cb: (data: string) => void) => {
dataCallback = cb
}),
onExit: vi.fn()
})
vi.spyOn(processTableSnapshot, 'getStrictProcessTableSnapshot').mockResolvedValue([])
const spawn = await spawnPty()
const boundary = '\x1b]133;C\x07'
dataCallback!(boundary)
await vi.advanceTimersByTimeAsync(0)
expect(handler.getIdentityEvidenceDebugSnapshot().processTableReads).toBe(1)
await vi.advanceTimersByTimeAsync(5_001)
await expect(attachPty({ id: PTY_1, suppressReplayNotification: true })).resolves.toEqual({
incarnationId: spawn.incarnationId,
replay: boundary
})
await vi.advanceTimersByTimeAsync(0)
expect(handler.getIdentityEvidenceDebugSnapshot().processTableReads).toBe(1)
})
it('suppresses legacy replay after the V1 owner is already active', async () => {
let dataCallback: ((data: string) => void) | undefined
mockPtySpawn.mockReturnValue({
+21 -7
View File
@@ -874,13 +874,7 @@ export class PtyHandler {
private wireAndStore(managed: ManagedPty): void {
managed.physicalExit = new PhysicalExitTracker()
this.ptys.set(managed.id, managed)
if (this.identityEvidenceBackstopTimer === null) {
this.identityEvidenceBackstopTimer = setInterval(
() => this.reconcileVisibleIdentityEvidence(),
IDENTITY_EVIDENCE_BACKSTOP_MS
)
this.identityEvidenceBackstopTimer.unref?.()
}
this.ensureIdentityEvidenceBackstopTimer()
if (this.dispatcher.hasConnectedClients?.()) {
this.scheduleIdentityEvidenceRead()
}
@@ -1047,11 +1041,16 @@ export class PtyHandler {
}
const visible = new Set(ids as string[])
this.identityEvidenceVisibleByClient.set(context.clientId, visible)
this.ensureIdentityEvidenceBackstopTimer()
this.publishHeldIdentityEvidenceToClient(context.clientId, visible)
return { ok: true }
})
this.dispatcher.onClientDetached?.((clientId) => {
this.identityEvidenceVisibleByClient.delete(clientId)
if (this.identityEvidenceVisibleByClient.size === 0 && this.identityEvidenceBackstopTimer) {
clearInterval(this.identityEvidenceBackstopTimer)
this.identityEvidenceBackstopTimer = null
}
})
this.dispatcher.onRequest('pty.getDefaultShell', async () => resolveDefaultShell())
this.dispatcher.onRequest('pty.serialize', (p) => this.serialize(p))
@@ -2553,6 +2552,21 @@ export class PtyHandler {
}
}
private ensureIdentityEvidenceBackstopTimer(): void {
if (
this.identityEvidenceBackstopTimer !== null ||
this.identityEvidenceVisibleByClient.size === 0 ||
this.ptys.size === 0
) {
return
}
this.identityEvidenceBackstopTimer = setInterval(
() => this.reconcileVisibleIdentityEvidence(),
IDENTITY_EVIDENCE_BACKSTOP_MS
)
this.identityEvidenceBackstopTimer.unref?.()
}
private scheduleIdentityEvidenceRead(id?: string): void {
if (id) {
this.identityEvidencePendingIds.add(id)