test: cover the native-chat page contract and the Source Control host watch

Nothing checked that what the desktop sanitizer emits is readable by the page,
even though a page parse failure is permanent. The new contract test pins the
shapes that survive and records three that do not: an over-long text block is
silently dropped, and a turn over the block limit or an over-long message id
fails the whole read. The desktop caps are the released native app's, so the
missing bound belongs to the mobile-web read adapter, not the sanitizer.

The Source Control host subscription had only happy-path coverage from a
roundtrip test. It now has its own cases for watcher failure, end-of-watch,
retirement, overflow batching and an unreadable frame.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-07 04:40:30 -04:00
parent 68cf24a488
commit 3d62049d93
2 changed files with 262 additions and 0 deletions
@@ -0,0 +1,144 @@
import { describe, expect, it } from 'vitest'
import {
MOBILE_WEB_NATIVE_CHAT_BLOCK_TEXT_MAX_CHARACTERS,
MOBILE_WEB_NATIVE_CHAT_MESSAGE_BLOCK_LIMIT,
MOBILE_WEB_NATIVE_CHAT_MESSAGE_ID_MAX_CHARACTERS,
MOBILE_WEB_NATIVE_CHAT_READ_LIMIT,
MobileWebNativeChatReadResultSchema
} from '../../../../shared/mobile-web/native-chat-operation-contract'
import { tolerantMobileWebShellPayload } from '../../../../shared/mobile-web/shell-payload-tolerance'
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import { MOBILE_NATIVE_CHAT_MAX_WINDOW, windowForClient } from './native-chat-rpc-message-sanitizer'
// The page parses the shell-relayed read with the tolerant rewrite, never the raw strict schema.
const pageContract = tolerantMobileWebShellPayload(MobileWebNativeChatReadResultSchema)
const ESC = String.fromCharCode(27)
function asPage(messages: unknown[]) {
return pageContract.safeParse({ messages, hasMore: false })
}
function sanitized(messages: unknown[]) {
return windowForClient(messages as NativeChatMessage[], 'mobile')
}
function message(id: string, blocks: unknown[]) {
return { id, role: 'assistant', blocks, timestamp: 1, source: 'transcript' }
}
describe('native chat sanitizer against the page contract', () => {
it('keeps an adversarial transcript parseable and strips what the page cannot name', () => {
const parsed = asPage(
sanitized([
message('turn-1', [
{
type: 'text',
text: `${ESC}]0;title${String.fromCharCode(7)}plain`,
providerFrame: {
provider: 'claude',
kind: 'raw',
payload: { head: 'h', byteLength: 4, digest: 'd', truncated: false }
}
},
{ type: 'diagram', nodes: [1, 2, 3] },
{ type: 'tool-call', name: 'Bash', input: { command: 'ls' }, state: 'running' },
{ type: 'tool-call', name: 'Read', input: {}, state: 'queued' },
{ type: 'tool-call', name: '', input: {} },
{
type: 'tool-result',
output: 'o'.repeat(5000),
isError: true,
editPatch: { filePath: 'a.ts', hunks: [] }
},
{ type: 'image-ref', path: 'p'.repeat(9000), url: 'data:image/png;base64,AAA', alt: 'ok' }
])
])
)
expect(parsed.success).toBe(true)
expect(parsed.data?.messages[0]?.blocks).toEqual([
{ type: 'text', text: `${ESC}]0;title${String.fromCharCode(7)}plain` },
{ type: 'tool-call', name: 'Bash', input: { command: 'ls' }, state: 'running' },
{ type: 'tool-call', name: 'Read', input: {} },
{
type: 'tool-result',
output: `${'o'.repeat(4000)}\n… (truncated)`,
isError: true
},
{ type: 'image-ref', alt: 'ok' }
])
})
it('keeps every tool-call lifecycle state the page names', () => {
const states = ['running', 'completed', 'failed'] as const
const parsed = asPage(
sanitized([
message(
'turn-2',
states.map((state) => ({ type: 'tool-call', name: 'Bash', input: {}, state }))
)
])
)
expect(parsed.success).toBe(true)
expect(
parsed.data?.messages[0]?.blocks.map((block) => ('state' in block ? block.state : null))
).toEqual([...states])
})
it('bounds a hostile tool-call input inside the page block ceiling', () => {
const deep = { a: { b: { c: { d: { e: { f: 'too deep' } } } } } }
const wide = Object.fromEntries(
Array.from({ length: 200 }, (_, index) => [`k${index}`, 'v'.repeat(200)])
)
const parsed = asPage(
sanitized([message('turn-3', [{ type: 'tool-call', name: 'Bash', input: { deep, wide } }])])
)
expect(parsed.success).toBe(true)
const block = parsed.data?.messages[0]?.blocks[0]
expect(JSON.stringify(block).length).toBeLessThan(
MOBILE_WEB_NATIVE_CHAT_BLOCK_TEXT_MAX_CHARACTERS * 2
)
})
it('never returns more messages than the page read limit accepts', () => {
const messages = Array.from({ length: MOBILE_WEB_NATIVE_CHAT_READ_LIMIT + 500 }, (_, index) =>
message(`turn-${index}`, [{ type: 'text', text: 'hi' }])
)
expect(MOBILE_NATIVE_CHAT_MAX_WINDOW).toBe(MOBILE_WEB_NATIVE_CHAT_READ_LIMIT)
const parsed = asPage(
windowForClient(messages as NativeChatMessage[], 'mobile', messages.length)
)
expect(parsed.success).toBe(true)
expect(parsed.data?.messages).toHaveLength(MOBILE_WEB_NATIVE_CHAT_READ_LIMIT)
})
// The three gaps below are unfixed: the sanitizer's mobile caps are the released native app's,
// and nothing between it and the page re-bounds them to the page contract.
it('gap: a text block over the page ceiling reaches the page as a dropped block', () => {
const text = 'a'.repeat(MOBILE_WEB_NATIVE_CHAT_BLOCK_TEXT_MAX_CHARACTERS + 1)
const parsed = asPage(sanitized([message('turn-4', [{ type: 'text', text }])]))
expect(parsed.success).toBe(true)
expect(parsed.data?.messages[0]?.blocks).toEqual([])
})
it('gap: a turn over the page block limit fails the whole read', () => {
const blocks = Array.from({ length: MOBILE_WEB_NATIVE_CHAT_MESSAGE_BLOCK_LIMIT + 1 }, () => ({
type: 'text',
text: 'hi'
}))
const parsed = asPage(sanitized([message('turn-5', blocks)]))
expect(parsed.success).toBe(false)
})
it('gap: a message id over the page ceiling fails the whole read', () => {
const id = 'x'.repeat(MOBILE_WEB_NATIVE_CHAT_MESSAGE_ID_MAX_CHARACTERS + 1)
const parsed = asPage(sanitized([message(id, [{ type: 'text', text: 'hi' }])]))
expect(parsed.success).toBe(false)
})
})
@@ -0,0 +1,118 @@
import { describe, expect, it, vi } from 'vitest'
import type { MobileWebBridgeClientError } from './mobile-web-bridge-client-error'
import type { MobileWebBridgeSubscriptionClient } from './mobile-web-bridge-subscription-client'
import { subscribeHostSourceControl } from './mobile-web-source-control-host-subscription'
import type { MobileWebSourceControlStatusInvalidation } from '../../shared/mobile-web/source-control-operation-contract'
const PAYLOAD = { workspaceId: 'page-workspace' }
function harness() {
const unsubscribe = vi.fn()
const events: MobileWebSourceControlStatusInvalidation[] = []
const errors: { code: string; retryable: boolean }[] = []
let deliver: ((event: unknown) => void) | undefined
let fail: ((error: MobileWebBridgeClientError) => void) | undefined
const subscribeHost = vi.fn((_payload, onHostEvent, onHostError) => {
deliver = onHostEvent
fail = onHostError
return { ready: Promise.resolve(), unsubscribe }
})
const subscription = subscribeHostSourceControl(
{ subscribeHost } as unknown as MobileWebBridgeSubscriptionClient,
PAYLOAD,
(event) => events.push(event),
(error) => errors.push({ code: error.code, retryable: error.retryable })
)
return {
errors,
events,
subscribeHost,
subscription,
unsubscribe,
deliver: (event: unknown) => deliver?.(event),
fail: (error: MobileWebBridgeClientError) => fail?.(error)
}
}
describe('host-projected Source Control subscription', () => {
it('subscribes over the generic host watch without naming the host workspace', async () => {
const h = harness()
await h.subscription.ready
expect(h.subscribeHost).toHaveBeenCalledWith(
{ method: 'mobileWeb.files.watch', workspaceId: 'page-workspace', params: {} },
expect.any(Function),
expect.any(Function)
)
})
it('reports a watcher failure once as retryable and delivers no invalidation', () => {
const h = harness()
h.deliver({ type: 'error', message: 'watch failed', rootPath: '/private/repo' })
expect(h.errors).toEqual([{ code: 'unavailable', retryable: true }])
expect(h.events).toEqual([])
expect(JSON.stringify(h.errors)).not.toContain('/private/repo')
})
it('retires the host subscription and stops reporting once the page unsubscribes', () => {
const h = harness()
h.deliver({ type: 'error', message: 'watch failed' })
h.subscription.unsubscribe()
h.deliver({ type: 'end' })
h.deliver({ type: 'changed', events: [] })
h.fail({ code: 'unavailable', retryable: true } as MobileWebBridgeClientError)
expect(h.unsubscribe).toHaveBeenCalledOnce()
expect(h.errors).toEqual([{ code: 'unavailable', retryable: true }])
expect(h.events).toEqual([])
})
it('keeps a normal end-of-watch retryable so the page subscription is not closed', () => {
const h = harness()
h.deliver({ type: 'end' })
expect(h.errors).toEqual([{ code: 'unavailable', retryable: true }])
expect(h.unsubscribe).not.toHaveBeenCalled()
})
it('rejects a malformed payload before it reaches the host', async () => {
const errors: { code: string; retryable: boolean }[] = []
const subscribeHost = vi.fn()
const subscription = subscribeHostSourceControl(
{ subscribeHost } as unknown as MobileWebBridgeSubscriptionClient,
{ workspaceId: '' },
vi.fn(),
(error) => errors.push({ code: error.code, retryable: error.retryable })
)
await expect(subscription.ready).rejects.toMatchObject({ code: 'invalid_request' })
await vi.waitFor(() => expect(errors).toEqual([{ code: 'invalid_request', retryable: false }]))
expect(subscribeHost).not.toHaveBeenCalled()
})
it('reports a changed batch the watcher could not bound as an overflow invalidation', () => {
const h = harness()
h.deliver({ type: 'changed', events: [{ kind: 'overflow', absolutePath: '/private/repo' }] })
h.deliver({ type: 'changed', events: [{ kind: 'update', absolutePath: '/private/repo/a.ts' }] })
expect(h.events).toEqual([
{ workspaceId: 'page-workspace', reason: 'overflow' },
{ workspaceId: 'page-workspace', reason: 'changed' }
])
expect(JSON.stringify(h.events)).not.toContain('/private/repo')
})
it('fails an unreadable changed frame instead of publishing an empty invalidation', () => {
const h = harness()
h.deliver({ type: 'changed' })
expect(h.errors).toEqual([{ code: 'invalid_message', retryable: false }])
expect(h.events).toEqual([])
})
})