chore(mobile): App Store prep — privacy manifest, debug-log cleanup, and protocol-version compat block (#1440)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-05-05 19:23:56 -07:00
committed by GitHub
co-authored by Orca
parent e91377f0b2
commit 63e36d05fd
15 changed files with 482 additions and 63 deletions
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it } from 'vitest'
import { evaluateCompat } from './protocol-compat'
const MOBILE_V = 1
describe('evaluateCompat', () => {
it('returns ok when both desktop fields are undefined and constants are wide-open', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: undefined,
desktopMinCompatibleMobileVersion: undefined
})
expect(verdict).toEqual({ kind: 'ok' })
})
it('returns ok when desktop reports version equal to mobile', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: MOBILE_V,
desktopMinCompatibleMobileVersion: 0
})
expect(verdict).toEqual({ kind: 'ok' })
})
it('returns ok when desktop reports a newer version (additive changes assumed safe)', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: MOBILE_V + 5,
desktopMinCompatibleMobileVersion: 0
})
expect(verdict).toEqual({ kind: 'ok' })
})
it('blocks with mobile-too-old when desktop requires a newer mobile', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: 5,
desktopMinCompatibleMobileVersion: MOBILE_V + 1
})
expect(verdict).toEqual({
kind: 'blocked',
reason: 'mobile-too-old',
desktopVersion: 5,
requiredMobileVersion: MOBILE_V + 1
})
})
it('coerces undefined desktopVersion to 0 in the verdict payload', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: undefined,
desktopMinCompatibleMobileVersion: MOBILE_V + 1
})
expect(verdict).toMatchObject({
kind: 'blocked',
reason: 'mobile-too-old',
desktopVersion: 0
})
})
it('blocks with desktop-too-old when desktop reports below the local minimum', () => {
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 5,
desktopProtocolVersion: 3,
desktopMinCompatibleMobileVersion: 0
})
expect(verdict).toEqual({
kind: 'blocked',
reason: 'desktop-too-old',
desktopVersion: 3,
requiredDesktopVersion: 5
})
})
it('mobile-too-old wins precedence when both constraints would fire', () => {
// Why: documents the intended kill-switch precedence — desktop's
// refusal of a too-old mobile takes priority over mobile's local
// refusal of a too-old desktop.
const verdict = evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 99,
desktopProtocolVersion: -1,
desktopMinCompatibleMobileVersion: MOBILE_V + 1
})
expect(verdict.kind).toBe('blocked')
expect((verdict as { reason: string }).reason).toBe('mobile-too-old')
})
it('with minCompatibleDesktopVersion = 0 every reported desktop passes', () => {
for (const v of [0, 1, 2, 99]) {
expect(
evaluateCompat({
mobileProtocolVersion: MOBILE_V,
minCompatibleDesktopVersion: 0,
desktopProtocolVersion: v,
desktopMinCompatibleMobileVersion: 0
})
).toEqual({ kind: 'ok' })
}
})
})
+50
View File
@@ -0,0 +1,50 @@
// Why: pure compat evaluator shared between desktop tests and mobile
// runtime. Mobile imports a thin wrapper (`mobile/src/transport/protocol-compat.ts`)
// that injects the mobile-side constants; desktop tests import this
// directly so the function is covered by the root vitest suite.
// All four numbers are passed in to keep the function dependency-free.
export type CompatVerdict =
| { kind: 'ok' }
| {
kind: 'blocked'
reason: 'mobile-too-old' | 'desktop-too-old'
desktopVersion: number
requiredMobileVersion?: number
requiredDesktopVersion?: number
}
export function evaluateCompat(input: {
mobileProtocolVersion: number
minCompatibleDesktopVersion: number
desktopProtocolVersion: number | undefined
desktopMinCompatibleMobileVersion: number | undefined
}): CompatVerdict {
// Why: absent fields → 0 lets mobile keep talking to pre-PR desktops.
// Bumping minCompatibleDesktopVersion above 0 will fence those older
// desktops alongside any explicitly-version-0 desktop, which is the
// intended kill-switch behavior.
const desktopVersion = input.desktopProtocolVersion ?? 0
const requiredMobile = input.desktopMinCompatibleMobileVersion ?? 0
// Why: mobile-too-old precedence — if desktop says "I refuse this
// mobile build" (kill switch), that wins over any local mobile
// judgment about desktop's age.
if (input.mobileProtocolVersion < requiredMobile) {
return {
kind: 'blocked',
reason: 'mobile-too-old',
desktopVersion,
requiredMobileVersion: requiredMobile
}
}
if (desktopVersion < input.minCompatibleDesktopVersion) {
return {
kind: 'blocked',
reason: 'desktop-too-old',
desktopVersion,
requiredDesktopVersion: input.minCompatibleDesktopVersion
}
}
return { kind: 'ok' }
}
+23
View File
@@ -0,0 +1,23 @@
// Why: declares the desktop's mobile-pairing protocol version so mobile
// builds can detect declared-incompatible combos and hard-block at pair
// time. Today's values are wide-open (mobile=any, desktop=any), so
// nothing actually blocks; the wire format is ready for the day we
// ship a genuinely-breaking change.
//
// Bump DESKTOP_PROTOCOL_VERSION when:
// - You remove an RPC method or required parameter that mobile uses.
// - You change the meaning (units, nullability) of an existing field
// mobile reads.
// - You change encryption, framing, or the auth handshake.
// Do NOT bump for:
// - Adding new RPC methods.
// - Adding new optional fields on existing methods.
// - Adding new event types in `terminal.subscribe`.
//
// Bump MIN_COMPATIBLE_MOBILE_VERSION when desktop ships a change that
// requires a minimum mobile version to function safely. This is the
// "kill switch": desktop can refuse old mobile builds without needing
// a desktop release of mobile.
export const DESKTOP_PROTOCOL_VERSION = 1
export const MIN_COMPATIBLE_MOBILE_VERSION = 0
+5
View File
@@ -11,6 +11,11 @@ export type RuntimeStatus = {
authoritativeWindowId: number | null
liveTabCount: number
liveLeafCount: number
// Why: optional so mobile builds can read both new and pre-PR desktops.
// Absence is treated as 0 by mobile's compat evaluator. See
// src/shared/protocol-version.ts for bump discipline.
protocolVersion?: number
minCompatibleMobileVersion?: number
}
export type CliRuntimeState =