Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/__fuzz__/harness/__tests__/harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,96 @@ describe('fuzz harness — known-divergence allowlist', () => {
)
})

// Two entries reach the generative decode target now, and both had to be
// pinned there rather than widened: the sweeps report one classified shape,
// `decode-parity` reports every kind of disagreement there is.
it('excuses an unimplemented codec on decode parity only when the bridge refused the bytes', async () => {
const { KNOWN_DIVERGENCES } = await import('../divergence.ts')
const registry = KNOWN_DIVERGENCES.filter(entry => entry.id === 'proto-unknown-type-dropped')
assert.equal(registry.length, 1, 'the entry under test is still in the registry')

const excused = (target: string, input: unknown, local: unknown, upstream: unknown): boolean =>
applyAllowlist([{ target, input, local, upstream }], new Date('2026-01-01'), registry).unexcused.length === 0

const parity = 'proto:decode-parity'
const refusal = '<throw Error: unknown proto type: BotAvatarMetadata>'
const bytes = { path: 'BotAvatarMetadata', origin: 'js-encoded', bytes: '' }

assert.ok(excused(parity, bytes, refusal, {}), 'the bridge refusing a type it does not implement')
// The type is named either way, so only the outcome tells these apart.
assert.ok(!excused(parity, bytes, { wordCount: 3 }, {}), 'returning data for an unimplemented type is not this')
assert.ok(!excused(parity, bytes, refusal, refusal), 'both refusing is not a divergence to explain')
assert.ok(
!excused(
parity,
{ path: 'Message', origin: 'js-encoded', bytes: '' },
'<throw Error: unknown proto type: Message>',
{}
),
'a refusal naming a type the bridge does implement is unexplained'
)
// The sweeps are unchanged: their classifier has already established the
// shape, so naming the type is the whole test there.
assert.ok(excused('proto:type-coverage', 'BotAvatarMetadata', 'a', 'b'), 'the sweeps still match by name alone')
})

it('composes the field rename with a field the bridge never writes, and nothing else', async () => {
const { KNOWN_DIVERGENCES } = await import('../divergence.ts')
const registry = KNOWN_DIVERGENCES.filter(entry => entry.id === 'proto-field-renamed-and-dropped')
assert.equal(registry.length, 1, 'the entry under test is still in the registry')

const excused = (local: unknown, upstream: unknown, path = 'SyncActionValue'): boolean =>
applyAllowlist(
[{ target: 'proto:decode-parity', input: { path }, local, upstream }],
new Date('2026-01-01'),
registry
).unexcused.length === 0

const renamed = { agentAction: { deviceId: '0n' } }
const declared = { agentAction: { deviceID: '0n' } }

assert.ok(excused(renamed, declared), 'the rename alone')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert.ok(
excused(renamed, { ...declared, businessBroadcastAssociationAction: {} }),
'the rename beside a field the bridge never writes — the shape deep mode draws'
)
// A field that is not on the list is a new gap, not this one.
assert.ok(!excused(renamed, { ...declared, chatLockSettings: {} }), 'an undocumented missing field still fails')
// A value that differs where both sides have the key is a misread.
assert.ok(!excused({ agentAction: { deviceId: '1n' } }, declared), 'a changed value is never this entry')
// And the absence is directional: a key only this side has is invention.
assert.ok(
!excused({ ...renamed, businessBroadcastAssociationAction: {} }, declared),
'a key upstream never produced is a different defect'
)

// A renamed field is decoded under another name, not dropped, so its leaf
// must never license an absence. Here `deviceID` is the real rename and
// `deviceAgentID` has genuinely gone missing beside it — taking the leaves
// of the encoder-gap list let the first stand in for the second.
assert.ok(
!excused({ ...renamed, chatAssignment: {} }, { ...declared, chatAssignment: { deviceAgentID: '' } }),
'a valid rename does not license a deviceAgentID that genuinely went missing'
)
// And the absence is pinned to where it was measured. `messageParamsJson`
// is documented unwritten on PaymentExtendedMetadata; the schema declares
// another on NativeFlowMessage, and a new drop of that one is a finding.
assert.ok(
!excused(
{
messageHistoryMetadata: { oldestMessageTimestampInWindow: '1n' },
interactiveMessage: { nativeFlowMessage: {} }
},
{
messageHistoryMetadata: { oldestMessageTimestamp: '1n' },
interactiveMessage: { nativeFlowMessage: { messageParamsJson: '{}' } }
},
'Message'
),
'a documented leaf dropped at an undocumented path still fails'
)
})

it('keeps every shipped registry entry well-formed', async () => {
const { KNOWN_DIVERGENCES } = await import('../divergence.ts')
const ids = new Set<string>()
Expand Down
98 changes: 94 additions & 4 deletions src/__fuzz__/harness/divergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,66 @@ export const undoRenames = (value: unknown, depth = 0): unknown => {
return out
}

/**
* The keys a decoded object may be missing, qualified by where they sit.
*
* Deliberately NOT the leaves of `NOT_ENCODED_FIELDS`, for two reasons that a
* leaf-name set gets wrong in opposite directions.
*
* That list is the *encoder* gap, and three of its entries — `deviceID`,
* `deviceAgentID`, `oldestMessageTimestamp` — are decoded under a renamed
* property rather than dropped. Taking its leaves would let one valid rename
* stand in for a key genuinely missing somewhere else: a `SyncActionValue`
* showing the expected `AgentAction.deviceId` alongside a newly absent
* `ChatAssignmentAction.deviceAgentID` would pass, because `undoRenames`
* restores the first and the set forgives the second.
*
* And a leaf name is not unique. `messageParamsJson` is unwritten on
* `Message.PaymentExtendedMetadata`, while the schema declares another on
* `Message.InteractiveMessage.NativeFlowMessage` — so accepting the bare leaf
* at any nesting depth would excuse a new drop of the second as though it were
* the documented first.
*
* Keyed by `<decoded type>.<key path>` and measured rather than derived: this
* is the absence the decode targets actually produced. A drop anywhere else,
* including the same leaf under a different holder, still fails.
*/
const DECODE_OMITTED_PATHS: ReadonlySet<string> = new Set(['SyncActionValue.businessBroadcastAssociationAction'])

/**
* True when the two decodes agree once the documented absences are allowed on
* the bridge's side, and nothing else differs.
*
* Directional on purpose. Upstream may carry a key this side lacks, and only at
* a path `DECODE_OMITTED_PATHS` names; this side carrying a key upstream lacks
* is a decoder inventing a property, which is a different defect. Any value that
* differs where both sides have the key fails outright, so this can never excuse
* a misread — only an absence that is already on the record.
*/
const sameExceptUnwrittenFields = (local: unknown, upstream: unknown, path: string, depth = 0): boolean => {
if (depth > 12) return sameShape(local, upstream)
if (Array.isArray(local) || Array.isArray(upstream)) {
if (!Array.isArray(local) || !Array.isArray(upstream) || local.length !== upstream.length) return false
// The index is not part of the path: a repeated field's elements all share
// the declaring field, and numbering them would make the set unwritable.
return local.every((item, index) => sameExceptUnwrittenFields(item, upstream[index], path, depth + 1))
}
const ourKeys = plainObject(local)
const theirKeys = plainObject(upstream)
if (ourKeys === undefined || theirKeys === undefined) return sameShape(local, upstream)
const ours = local as Record<string, unknown>
const theirs = upstream as Record<string, unknown>
for (const key of theirKeys) {
const here = `${path}.${key}`
if (!Object.hasOwn(ours, key)) {
if (!DECODE_OMITTED_PATHS.has(here)) return false
continue
}
if (!sameExceptUnwrittenFields(ours[key], theirs[key], here, depth + 1)) return false
}
return ourKeys.every(key => Object.hasOwn(theirs, key))
}

/**
* The keys of a plain object, or `undefined` for anything else.
*
Expand Down Expand Up @@ -1190,10 +1250,23 @@ export const KNOWN_DIVERGENCES: readonly KnownDivergence[] = [
)
)
}
return sameShape(undoRenames(divergence.local), undoRenames(divergence.upstream))
// `sameExceptUnwrittenFields`, not bare `sameShape`: the generative decode
// targets draw whole messages, so a SyncActionValue carrying a renamed
// field usually carries `businessBroadcastAssociationAction` as well —
// which the bridge never writes, and which `proto-field-not-encoded`
// already documents. Two known gaps in one message explained neither, and
// the finite sweeps never saw the pair because they vary one field at a
// time. Composing here is what the buffer entries do, for the same reason.
// The composition stays exact: only the eleven enumerated fields may be
// absent, only from this side, and any value both sides carry must match.
// Rooted at the decoded type, so a documented absence is pinned to the
// message it was measured in rather than to a bare property name.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const decoded = inputPath(divergence.input)
if (decoded === undefined) return false
return sameExceptUnwrittenFields(undoRenames(divergence.local), undoRenames(divergence.upstream), decoded)
},
reason:
'Three fields round-trip under a different property name than upstream declares, and the bridge encoder silently drops the upstream spelling: SyncActionValue.ChatAssignmentAction.deviceAgentID becomes deviceAgentId, SyncActionValue.AgentAction.deviceID becomes deviceId, and Message.MessageHistoryMetadata.oldestMessageTimestamp becomes oldestMessageTimestampInWindow. The property name is the public API — code written against the upstream types reads undefined, and writes are lost with no error at all. ALREADY TRACKED: all three are in KNOWN_WIRE_GAPS in scripts/compatibility/proto-runtime-audit.ts; the sweep rediscovered them from generated input rather than finding them.',
'Three fields round-trip under a different property name than upstream declares, and the bridge encoder silently drops the upstream spelling: SyncActionValue.ChatAssignmentAction.deviceAgentID becomes deviceAgentId, SyncActionValue.AgentAction.deviceID becomes deviceId, and Message.MessageHistoryMetadata.oldestMessageTimestamp becomes oldestMessageTimestampInWindow. The property name is the public API — code written against the upstream types reads undefined, and writes are lost with no error at all. On the generative decode targets the rename arrives beside a field the bridge never writes, because one drawn message carries both; that half is NOT_ENCODED_FIELDS and has its own entry, and this predicate composes with it rather than either one covering the pair alone. ALREADY TRACKED: all three renames are in KNOWN_WIRE_GAPS in scripts/compatibility/proto-runtime-audit.ts; the sweep rediscovered them from generated input rather than finding them.',
review: '2026-10-01'
},
{
Expand Down Expand Up @@ -1408,9 +1481,26 @@ export const KNOWN_DIVERGENCES: readonly KnownDivergence[] = [
},
{
id: 'proto-unknown-type-dropped',
target: /^proto:(unknown-type-dropped|type-coverage)$/u,
// `decode-parity` as well as the two sweeps. The sweeps reach the type
// through a field that holds one, where the bridge silently omits it; the
// generative target hands the type's own bytes to both decoders and the
// bridge *throws* instead. One missing codec, two ways of meeting it.
target: /^proto:(unknown-type-dropped|type-coverage|decode-parity)$/u,
status: 'open',
when: divergence => namesUnknownCodecType(divergence.input),
when: divergence => {
if (!namesUnknownCodecType(divergence.input)) return false
// Naming the type is the whole test on the sweeps, whose classifier has
// already established the shape. Not on decode-parity, which reports
// every kind of disagreement: without the outcome pinned, this entry
// would excuse the bridge returning *wrong data* for a type it does not
// implement just as readily as refusing to decode it.
if (divergence.target !== 'proto:decode-parity') return true
return (
isThrow(divergence.local) &&
UNKNOWN_CODEC_TYPES.some(type => divergence.local === `<throw Error: unknown proto type: ${type}>`) &&
plainObject(divergence.upstream) !== undefined
)
},
reason:
'The bridge codec does not implement every message type the upstream protos declare (BotAvatarMetadata at the time of writing), and a field holding one is silently omitted rather than reported: MessageContextInfo{botMetadata:{avatarMetadata:{}}} encodes to 3a00 instead of 3a020a00. ALREADY TRACKED: BotAvatarMetadata is in KNOWN_UNSUPPORTED_CODECS and its fields in KNOWN_WIRE_GAPS in scripts/compatibility/proto-runtime-audit.ts. The unknown-type set here is probed at runtime rather than listed, so this entry stops matching by itself once the bridge implements them.',
review: '2026-10-01'
Expand Down
Loading