Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ it('renders trustworthy context through the phone consumer while capturing only
for (const input of ['low', 'max', 'opencode', 'absent'] as const) {
const { agent, model, display, live, resting, catalog } = await recordedEffortCase(input)
for (const answer of [live, resting]) {
let response = Promise.resolve(answer)
const { client } = optionsClient(
async () => answer,
() => response,
async () => catalog
)
const harness = await mountOptions({
Expand All @@ -35,6 +36,12 @@ it('renders trustworthy context through the phone consumer while capturing only
}
expect(effort?.kind.currentValue).toBe(display ?? undefined)
expect(harness.current().sendSelection()).toEqual({ selection: { model } })
response = new Promise(() => {})
await harness.rerender({ fence: 2 })
expect(descriptorFor(harness.current().optionSnapshot, 'effort')?.kind.currentValue).toBe(
display ?? undefined
)
expect(harness.current().sendSelection()).toEqual({ selection: { model } })
await harness.unmount()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ it('keeps actual phone operations and correlated acceptance through a stable-cli
const stale = deferred<AgentSessionOptionsResult>()
let oldReads = 0
const physical = optionsClient(() =>
++oldReads === 1 ? Promise.resolve(OPTIONS) : stale.promise
++oldReads === 1 ? Promise.resolve({ ...OPTIONS, acceptsImages: true }) : stale.promise
)
const currentReads: ReturnType<typeof deferred<AgentSessionOptionsResult>>[] = []
const next = optionsClient(() => {
Expand Down Expand Up @@ -77,8 +77,11 @@ it('keeps actual phone operations and correlated acceptance through a stable-cli
await client.migrateTo(next.client, 'relay')
})
await settle()
await act(async () => stale.resolve({ ...OPTIONS, conversationCommands: ['compact'] }))
await act(async () =>
stale.resolve({ ...OPTIONS, acceptsImages: false, conversationCommands: ['compact'] })
)
expect(view.current().conversationCommands).toEqual([])
expect(view.current().acceptsImages).toBe(true)
expect(currentValueOf(view.current().optionSnapshot, 'model')).toBe('gpt-fast')
expect(view.current().sendSelection()).toMatchObject({ selection: { model: 'gpt-fast' } })
await act(async () =>
Expand Down Expand Up @@ -217,6 +220,33 @@ it('reads the new physical path after an idle connected-to-connected migration w
client.close()
})

it('coalesces a burst of turns into one running and one trailing options read', async () => {
const reads: ReturnType<typeof deferred<AgentSessionOptionsResult>>[] = []
const rpc = optionsClient(() => {
const read = deferred<AgentSessionOptionsResult>()
reads.push(read)
return read.promise
})
const { mutate } = recordingMutate(async () => ({ status: 'unknown' }))
const view = await mountOptions({ ...BASE, client: rpc.client, mutate })
expect(reads).toHaveLength(1)
await act(async () => reads[0].resolve(OPTIONS))
await settle()
for (let turn = 1; turn <= 10; turn++) {
await view.rerender({ turnId: `turn-${turn}` })
}
expect(reads).toHaveLength(2)
await act(async () => reads[1].resolve({ ...OPTIONS, conversationCommands: ['clear'] }))
await settle()
expect(view.current().conversationCommands).toEqual([])
expect(reads).toHaveLength(3)
await act(async () => reads[2].resolve({ ...OPTIONS, conversationCommands: ['compact'] }))
await settle()
expect(reads).toHaveLength(3)
expect(view.current().conversationCommands).toEqual(['compact'])
await view.unmount()
})

it.each(['current', 'failed-then-wake', 'true-end'] as const)(
'settles a late refusal after a physical reset without losing progress (%s)',
async (schedule) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import * as selectionOperations from '../../../src/shared/structured-agent-session-selection-operations'
import {
rememberMobileCreatedStructuredSession,
mobileCreatedStructuredSession
} from './mobile-created-structured-sessions'
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -97,6 +102,7 @@ async function mountOptions(props: ProbeProps) {
await settle()
return {
pending: () => rendered.current?.pendingOptionId,
images: () => rendered.current?.acceptsImages,
offered: () => {
const entry = rendered.current?.optionSnapshot.find(
(descriptor) => descriptor.id === 'permissionMode'
Expand Down Expand Up @@ -131,19 +137,52 @@ async function mountOptions(props: ProbeProps) {

// STA-10671: a 1.4.224 host ignores per-chat modes and runs its own Agent Permissions.
describe('the permission pill against a host predating per-chat modes', () => {
it("says the chat runs the host's Agent Permissions, and offers nothing to pick", async () => {
it('publishes legacyHostPermissions only after a current options answer', async () => {
const factory = vi.spyOn(selectionOperations, 'createStructuredSessionSelectionController')
const old = Promise.withResolvers<AgentSessionOptionsResult>()
const current = Promise.withResolvers<AgentSessionOptionsResult>()
const rpc = optionsClient(undefined)
const send = rpc.client.sendRequest
let reads = 0
rpc.client.sendRequest = async (method, params, config) =>
method === 'agentSession.options'
? {
id: 'held',
ok: true,
result: await (++reads === 1 ? old.promise : current.promise),
_meta: { runtimeId: 'runtime-1' }
}
: send(method, params, config)
rememberMobileCreatedStructuredSession('session-1', '/workspace')
const options = await mountOptions({
client: optionsClient(undefined).client,
client: rpc.client,
fence: 1,
optionStamp: 'gpt-live|',
optionStamp: '',
hostAdvertisesPermissionModes: false
})
const constructed = factory.mock.results.at(-1)
if (constructed?.type !== 'return') {
throw new Error('Phone selection owner was not created')
}
const controller = constructed.value
await act(async () => {
const pick = controller.issue('model', 'gpt-live', null)
controller.settle(pick, { kind: 'unknown' })
})
await act(async () => old.resolve({ ...optionsFor(), acceptsImages: false }))
expect(options.permission()).toBeNull()
expect(options.images()).toBe(true)
expect(mobileCreatedStructuredSession('session-1')).toEqual({ worktree: '/workspace' })
await act(async () => current.resolve({ ...optionsFor(), acceptsImages: false }))
expect(options.permission()).toBe('host')
expect(options.offered()).toEqual(['host'])
expect(options.images()).toBe(false)
expect(mobileCreatedStructuredSession('session-1')).toBeUndefined()
vi.mocked(mutate).mockClear()
expect(await options.pick('own', true)).toBe(false)
expect(mutate).not.toHaveBeenCalled()
await options.unmount()
factory.mockRestore()
})

// 1.4.225–1.4.226 apply modes without advertising the capability: their stated mode wins.
Expand Down
Loading
Loading