Files
orca/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts
T
Brennan Benson 9ae33a96f9 fix(native-chat): keep damaged chats readable without granting agent actions (#31931)
* fix(native-chat): validate saved next-fence floors before use

* WIP: separate saved conversation observations from runtime proof

* Consolidate duplicate imports for WIP compiler checkpoint

* Preserve saved conversation observations with strict runtime authority

* Consolidate session admission and catalog imports

* fix: align conversation observation contracts and fixtures

* test: update conversation observation and writer fixtures

* test: use checked host fixtures for structured workers

* fix(native-chat): bound conversation projection work per request

* test(native-chat): consolidate scale fixture imports

* test(native-chat): use checked scale fixture types

* test(native-chat): bound unreadable inventory traversal

* Test native CLI admission and durable reference reads

* test: bound cold conversation name subscription work

* test: distinguish native receipt probes from terminal identity

* test: retain exact mutation refusal recovery identity

* fix: explain agent conversation preparation refusals

* test: adapt remaining conversation readers and document CLI timeout

* fix: register preparation refusal English message

* test: align conversation fixtures with checked read APIs

* test(native-chat): use checked conversation in Codex fixture
2026-10-26 13:16:41 -07:00

251 lines
10 KiB
TypeScript

import type * as ClientReducer from '../../../src/shared/structured-agent-session-reducer'
import type * as ClientProjection from '../../../src/shared/structured-agent-session-projection'
import type * as ClientSchemas from '../../../src/shared/agent-session-journal-schemas'
import type { sendPlan } from '../../../src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans'
import {
importReleaseCheckoutModule,
materializeReleaseCheckout,
type ReleaseCheckout
} from './release-checkout'
// Read capability strings and registered methods from each real build rather than assuming what the older peer knows.
export const WORKING_TREE = 'working-tree' as const
export type AgentSessionClientProjection = Pick<
typeof ClientReducer,
'EMPTY_STRUCTURED_AGENT_SESSION' | 'reduceStructuredAgentSession'
> &
Pick<typeof ClientProjection, 'projectStructuredItemsToNativeChat'> &
Pick<typeof ClientSchemas, 'AgentJournalRenderItemSchema'>
// Each module graph owns its host slot; installing into current source cannot populate a release dispatcher.
const STRUCTURED_HOST_REGISTRY =
'/src/main/native-chat/agent-session-wire/structured-agent-session-registry.ts'
const MUTATION_PLANS =
'/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts'
const MUTATION_ADMISSION =
'/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts'
export type RpcReply = {
id: string
ok: boolean
streaming?: true
result?: unknown
error?: { code: string; message: string }
}
export type RpcClientIdentity = {
clientKind?: 'mobile' | 'runtime'
clientCapabilities?: readonly string[]
updateClientCapabilities?: (capabilities: readonly string[]) => void
connectionId?: string
clientId?: string
}
export type AgentSessionDispatcher = {
dispatchStreaming: (
request: { id: string; authToken: string; method: string; params?: unknown },
reply: (message: string) => void,
options?: RpcClientIdentity
) => Promise<void>
}
export type AgentSessionWireBuild = {
/** Human label used in test names and failure messages. */
label: string
/** `working-tree` for current code, otherwise the resolved release commit. */
revision: string
/** Capability strings this build defines. A peer cannot advertise — nor a client
* ask for — a string its own source never names. */
capabilities: readonly string[]
protocolVersion: number
/** RPC method names the build registers, read from source. */
methodNames: readonly string[]
/** A dispatcher carrying a method set this build really ships, so an
* unknown-method answer is about the method and not an empty registry. */
createDispatcher: (runtime: unknown) => AgentSessionDispatcher
/** Put a host in *this* build's slot. Loaded on call so a release that predates
* the surface stays loadable, and throws rather than no-opping so a build with
* no slot cannot read as a surface that answered. */
installStructuredHost: (host: unknown) => Promise<void>
/** This build's own admission of the `agentSession.send` params its host was handed. With no
* journal it stops after the fingerprint check: a fingerprint it derives differently refuses
* as `fingerprintMismatch`, one it agrees with as `sessionNotAttached`. */
admitSend: (sent: SentMessage) => Promise<unknown>
clientProjection: () => Promise<AgentSessionClientProjection>
}
async function loadClientProjection(
load: (path: string) => Promise<Record<string, unknown>>
): Promise<AgentSessionClientProjection> {
const modules = await Promise.all([
load('/src/shared/structured-agent-session-reducer.ts'),
load('/src/shared/structured-agent-session-projection.ts'),
load('/src/shared/agent-session-journal-schemas.ts')
])
const client: Record<string, unknown> = Object.assign({}, ...modules)
const itemSchema = client.AgentJournalRenderItemSchema
if (
typeof client.reduceStructuredAgentSession !== 'function' ||
typeof client.projectStructuredItemsToNativeChat !== 'function' ||
!client.EMPTY_STRUCTURED_AGENT_SESSION ||
typeof itemSchema !== 'object' ||
itemSchema === null ||
!('parse' in itemSchema) ||
typeof itemSchema.parse !== 'function'
) {
throw new Error('Release does not export the structured transcript reader')
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: checked release exports; skew tests exercise admission, reducer and transcript signatures against real host frames.
return client as AgentSessionClientProjection
}
type DispatcherModule = {
RpcDispatcher: new (options: { runtime: unknown; methods: unknown[] }) => AgentSessionDispatcher
}
function registeredMethodNames(methods: readonly unknown[]): string[] {
return methods
.flatMap((method) => {
if (!method || typeof method !== 'object' || !('name' in method)) {
return []
}
const { name } = method
return typeof name === 'string' ? [name] : []
})
.sort()
}
function applyStructuredHost(module: Record<string, unknown>, label: string, host: unknown): void {
const install = module.setStructuredAgentSessionHost
if (typeof install !== 'function') {
throw new Error(`Build ${label} publishes no structured agent-session host registry`)
}
;(install as (next: unknown) => void)(host)
}
/** The `agentSession.send` params a host is handed. */
export type SentMessage = Parameters<typeof sendPlan>[0]
type SendAdmissionModules = {
sendPlan: (sent: SentMessage) => unknown
admitAndRunAgentSessionMutation: (request: {
plan: unknown
envelope: SentMessage['envelope']
journal: () => undefined
store: { getRecord: () => null; selectWritableRecord: () => { kind: 'missing' } }
}) => Promise<unknown>
}
async function admitSend(
plans: Record<string, unknown>,
admission: Record<string, unknown>,
sent: SentMessage
): Promise<unknown> {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the build's own send plan and admission; the absent session and journal stop both admission routes after fingerprint validation. A drifted export or shape fails this check.
const build = { ...plans, ...admission } as unknown as SendAdmissionModules
return build.admitAndRunAgentSessionMutation({
plan: build.sendPlan(sent),
envelope: sent.envelope,
journal: () => undefined,
store: { getRecord: () => null, selectWritableRecord: () => ({ kind: 'missing' }) }
})
}
function capabilityStrings(module: Record<string, unknown>): readonly string[] {
const declared = module.RUNTIME_CAPABILITIES
if (!Array.isArray(declared) || declared.length === 0) {
throw new Error('Cross-version harness found no RUNTIME_CAPABILITIES to compare')
}
return declared as readonly string[]
}
async function loadWorkingTreeBuild(): Promise<AgentSessionWireBuild> {
const [protocol, dispatcher, methodRegistry] = await Promise.all([
import('../../../src/shared/protocol-version'),
import('../../../src/main/runtime/rpc/dispatcher'),
import('../../../src/main/runtime/rpc/methods')
])
const module = dispatcher as unknown as DispatcherModule
const methods = methodRegistry.ALL_RPC_METHODS as unknown[]
return {
label: WORKING_TREE,
revision: WORKING_TREE,
capabilities: capabilityStrings(protocol as unknown as Record<string, unknown>),
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION,
methodNames: registeredMethodNames(methods),
clientProjection: async () => {
const [reducer, projection, schemas] = await Promise.all([
import('../../../src/shared/structured-agent-session-reducer'),
import('../../../src/shared/structured-agent-session-projection'),
import('../../../src/shared/agent-session-journal-schemas')
])
return { ...reducer, ...projection, ...schemas }
},
createDispatcher: (runtime) =>
new module.RpcDispatcher({
runtime,
methods
}),
installStructuredHost: async (host) => {
const registry =
await import('../../../src/main/native-chat/agent-session-wire/structured-agent-session-registry')
applyStructuredHost(registry as unknown as Record<string, unknown>, WORKING_TREE, host)
},
admitSend: async (sent) => {
const [plans, admission] = await Promise.all([
import('../../../src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans'),
import('../../../src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission')
])
return admitSend(plans, admission, sent)
}
}
}
async function loadReleaseBuild(checkout: ReleaseCheckout): Promise<AgentSessionWireBuild> {
const [protocol, dispatcher, methodRegistry] = await Promise.all([
importReleaseCheckoutModule(checkout, '/src/shared/protocol-version.ts'),
importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/dispatcher.ts'),
importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/methods/index.ts')
])
const module = dispatcher as unknown as DispatcherModule
const methods = methodRegistry.ALL_RPC_METHODS as unknown[]
return {
label: checkout.ref,
revision: checkout.commit,
capabilities: capabilityStrings(protocol),
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION as number,
methodNames: registeredMethodNames(methods),
clientProjection: () =>
loadClientProjection((path) => importReleaseCheckoutModule(checkout, path)),
createDispatcher: (runtime) =>
new module.RpcDispatcher({
runtime,
methods
}),
installStructuredHost: async (host) => {
applyStructuredHost(
await importReleaseCheckoutModule(checkout, STRUCTURED_HOST_REGISTRY),
checkout.ref,
host
)
},
admitSend: async (sent) => {
const [plans, admission] = await Promise.all([
importReleaseCheckoutModule(checkout, MUTATION_PLANS),
importReleaseCheckoutModule(checkout, MUTATION_ADMISSION)
])
return admitSend(plans, admission, sent)
}
}
}
// Import current source for WORKING_TREE; extract any other git ref into the cached checkout.
export async function loadAgentSessionWireBuild(ref: string): Promise<AgentSessionWireBuild> {
if (ref === WORKING_TREE) {
return loadWorkingTreeBuild()
}
return loadReleaseBuild(await materializeReleaseCheckout(ref))
}