mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(ci): repair main's line limits and a stale test import (#33675)
* test(native-chat): split startup report coverage below line limit * test(native-chat): keep startup status reads synchronous * refactor(native-chat): move status child fields below line limit * test(native-chat): annotate exported startup mock types * test(chat-rename): import launch persistence from its current path
This commit is contained in:
+225
@@ -0,0 +1,225 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, vi, type Mock } from 'vitest'
|
||||
import type { AgentJournalSubmission } from '../../../shared/agent-session-journal-types'
|
||||
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope'
|
||||
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
|
||||
import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store'
|
||||
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness'
|
||||
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
|
||||
import type {
|
||||
AgentSessionAcquisition,
|
||||
StructuredAgentSessionAcquireInput,
|
||||
StructuredAgentSessionAdapter
|
||||
} from './structured-agent-session-adapter'
|
||||
import {
|
||||
claudeAndCodexDeclared,
|
||||
NO_STRUCTURED_AGENTS
|
||||
} from './structured-agent-session-adapter-router-test-support'
|
||||
import { StructuredAgentSessionHost } from './structured-agent-session-host'
|
||||
import {
|
||||
HOST_TEST_NOW as NOW,
|
||||
HOST_TEST_SESSION as SESSION,
|
||||
HOST_TEST_THREAD as THREAD,
|
||||
hostTestAttachParams,
|
||||
hostTestMessage,
|
||||
hostTestOperationId,
|
||||
resetHostTestOperationIds
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
|
||||
import type { StructuredAgentSessionStartupLimits } from './structured-agent-session-startup-attempt-contract'
|
||||
|
||||
export const CALLER = { callerKey: 'client-1' }
|
||||
|
||||
export const eventually = (assertion: () => void | Promise<void>, interval = 0): Promise<void> =>
|
||||
vi.waitFor(assertion, { timeout: 10_000, interval })
|
||||
|
||||
let root: string
|
||||
export let store: AgentSessionRecordStore
|
||||
export let host: StructuredAgentSessionHost
|
||||
export let acquire: Mock<StructuredAgentSessionAdapter['acquire']>
|
||||
export let dispatch: Mock<StructuredAgentSessionAdapter['dispatch']>
|
||||
export let closeSession: Mock<NonNullable<StructuredAgentSessionAdapter['closeSession']>>
|
||||
let setOption: Mock<StructuredAgentSessionAdapter['setOption']>
|
||||
export const providerStarted: Mock = vi.fn()
|
||||
export const recordLiveListing: Mock = vi.fn()
|
||||
const openGates = new Map<string, (model: string) => void>()
|
||||
|
||||
export async function proveOpen(model = 'default'): Promise<void> {
|
||||
const generation = `generation-${acquire.mock.calls.length}`
|
||||
await eventually(() => expect(openGates.has(generation)).toBe(true))
|
||||
openGates.get(generation)!(model)
|
||||
openGates.delete(generation)
|
||||
}
|
||||
|
||||
export function acquisition(input: StructuredAgentSessionAcquireInput): AgentSessionAcquisition {
|
||||
return {
|
||||
process: {
|
||||
hostId: 'local',
|
||||
pid: 4242,
|
||||
processStartTimeMs: 1_700_000_000_000,
|
||||
spawnToken: input.spawnToken
|
||||
},
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
link: {
|
||||
linkId: `link-${input.fence}`,
|
||||
handle: codexProviderHandle(THREAD),
|
||||
origin: store.getRecord(SESSION)?.providerHandleChain.length ? 'resumed' : 'created',
|
||||
mintedAtFence: input.fence,
|
||||
observedAt: NOW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const publishFirst: StructuredAgentSessionAdapter['acquire'] = async (input) =>
|
||||
acquisition(input)
|
||||
|
||||
type StartupHostOptions = {
|
||||
startupLimits?: Partial<StructuredAgentSessionStartupLimits>
|
||||
supportsGoals?: boolean
|
||||
}
|
||||
|
||||
async function startHost({ startupLimits, supportsGoals }: StartupHostOptions = {}): Promise<void> {
|
||||
host = new StructuredAgentSessionHost({
|
||||
agents: supportsGoals ? claudeAndCodexDeclared() : NO_STRUCTURED_AGENTS,
|
||||
logger: createStructuredAgentSessionLogger(),
|
||||
store,
|
||||
adapter: {
|
||||
open: ({ acquisitionGeneration }) =>
|
||||
new Promise((resolve) => {
|
||||
openGates.set(acquisitionGeneration!, (model) =>
|
||||
resolve({ reportedOptions: { model, reportedPermissionMode: 'ask' } })
|
||||
)
|
||||
}),
|
||||
acquire,
|
||||
dispatch,
|
||||
closeSession,
|
||||
releaseAcquisition: vi.fn(async () => true),
|
||||
cancelTurn: vi.fn(async () => ({ cancelled: false })),
|
||||
answerPrompt: vi.fn(async () => undefined),
|
||||
setOption
|
||||
},
|
||||
modelCatalog: {
|
||||
peek: () => null,
|
||||
read: vi.fn(async () => ({ origin: 'unknown' as const })),
|
||||
recordLiveListing,
|
||||
retainedLiveModelIds: vi.fn(async () => []),
|
||||
prewarm: vi.fn(async () => {}),
|
||||
stop: vi.fn(),
|
||||
providerStarted
|
||||
},
|
||||
journalDatabase: openTestJournalHostDatabase(root),
|
||||
claimKeyId: 'key-1',
|
||||
mintSpawnToken: () => `spawn-${acquire.mock.calls.length}`,
|
||||
idleSweep: { intervalMs: 60 * 60_000, idleMs: 60 * 60_000 },
|
||||
...(startupLimits === undefined ? {} : { startupLimits }),
|
||||
now: () => NOW
|
||||
})
|
||||
}
|
||||
|
||||
export async function restartWith(
|
||||
next: StructuredAgentSessionAdapter['acquire'],
|
||||
options: StartupHostOptions = {}
|
||||
): Promise<void> {
|
||||
await host.close(SESSION, 'evict')
|
||||
await host.flushAllStreamedEvents()
|
||||
acquire.mockImplementation(next)
|
||||
closeSession.mockClear()
|
||||
await startHost(options)
|
||||
acquire.mockClear()
|
||||
}
|
||||
|
||||
export function registerStartupAttemptHostTests(): void {
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers({
|
||||
toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval']
|
||||
})
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-startup-attempt-'))
|
||||
resetHostTestOperationIds()
|
||||
providerStarted.mockReset()
|
||||
recordLiveListing.mockReset()
|
||||
openGates.clear()
|
||||
acquire = vi.fn(async (input) => acquisition(input))
|
||||
dispatch = vi.fn(async () => ({
|
||||
state: 'accepted' as const,
|
||||
providerIdentity: {
|
||||
provider: 'codex' as const,
|
||||
threadId: THREAD,
|
||||
turnId: `turn-${dispatch.mock.calls.length}`,
|
||||
ordinal: dispatch.mock.calls.length
|
||||
}
|
||||
}))
|
||||
closeSession = vi.fn(async () => true)
|
||||
setOption = vi.fn(async () => undefined)
|
||||
store = await openTestAgentSessionRecordStore(root)
|
||||
await startHost()
|
||||
expect(await host.attach(CALLER, hostTestAttachParams(null))).toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await host.flushAllStreamedEvents()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sendParams(text: string, clientOperationId = hostTestOperationId()) {
|
||||
const body = hostTestMessage(text)
|
||||
return {
|
||||
envelope: {
|
||||
sessionId: SESSION,
|
||||
clientOperationId,
|
||||
expectedRuntimeFence: 1,
|
||||
payloadFingerprint: computeAgentSessionPayloadFingerprint({
|
||||
method: 'agentSession.send',
|
||||
sessionId: SESSION,
|
||||
fields: { body }
|
||||
})
|
||||
},
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
export async function accept(text: string, clientOperationId?: string): Promise<string> {
|
||||
const params = sendParams(text, clientOperationId)
|
||||
expect(await host.send(CALLER, params)).toMatchObject({
|
||||
ok: true,
|
||||
value: { submission: { dispatchState: 'pending', handoverRecorded: true } }
|
||||
})
|
||||
return params.envelope.clientOperationId
|
||||
}
|
||||
|
||||
export async function submission(id: string): Promise<AgentJournalSubmission | undefined> {
|
||||
return (await host.journalSnapshot(SESSION)).submissions.find(
|
||||
(entry) => entry.clientMessageId === id
|
||||
)
|
||||
}
|
||||
|
||||
export async function heldBehindStart(text: string, interval = 0): Promise<string> {
|
||||
const id = await accept(text)
|
||||
await eventually(
|
||||
async () => expect(host.readStatusSummary(SESSION)?.hostExecutionPhase).toBe('starting'),
|
||||
interval
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
return host.cancel(CALLER, {
|
||||
envelope: {
|
||||
sessionId: SESSION,
|
||||
clientOperationId: hostTestOperationId(),
|
||||
expectedRuntimeFence: null,
|
||||
payloadFingerprint: computeAgentSessionPayloadFingerprint({
|
||||
method: 'agentSession.cancel',
|
||||
sessionId: SESSION,
|
||||
fields: { turnId: 'turn-none' }
|
||||
})
|
||||
},
|
||||
turnId: 'turn-none'
|
||||
})
|
||||
}
|
||||
+26
-315
@@ -1,230 +1,52 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CodexAppServerRequestError } from '../../codex/app-server/codex-app-server-request-error'
|
||||
import { withProviderRequestTimeout } from '../../provider-process/provider-request-timeout'
|
||||
import { AgentSessionAcquisitionRefusal } from './structured-agent-session-adapter'
|
||||
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
|
||||
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
|
||||
import type { AgentJournalSubmission } from '../../../shared/agent-session-journal-types'
|
||||
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../shared/agent-session-journal-types'
|
||||
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope'
|
||||
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
|
||||
import { DISPATCH_REJECTED_CANCELLED } from '../../../shared/structured-agent-session-dispatch-rejection'
|
||||
import { NATIVE_CHAT_STOPPED_BEFORE_START_TEXT } from '../../../shared/native-chat-stopped-before-start'
|
||||
import { projectStructuredAgentSessionMessages } from '../../../shared/structured-agent-session-message-projection'
|
||||
import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store'
|
||||
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness'
|
||||
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
|
||||
import type {
|
||||
AgentSessionAcquisition,
|
||||
StructuredAgentSessionAcquireInput,
|
||||
StructuredAgentSessionAdapter
|
||||
} from './structured-agent-session-adapter'
|
||||
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
|
||||
import {
|
||||
claudeAndCodexDeclared,
|
||||
NO_STRUCTURED_AGENTS
|
||||
} from './structured-agent-session-adapter-router-test-support'
|
||||
import { StructuredAgentSessionHost } from './structured-agent-session-host'
|
||||
import {
|
||||
HOST_TEST_NOW as NOW,
|
||||
HOST_TEST_SESSION as SESSION,
|
||||
HOST_TEST_THREAD as THREAD,
|
||||
hostTestAttachParams,
|
||||
hostTestMessage,
|
||||
hostTestOperationId,
|
||||
resetHostTestOperationIds
|
||||
hostTestOperationId
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
|
||||
import type { StructuredAgentSessionStartupLimits } from './structured-agent-session-startup-attempt-contract'
|
||||
import {
|
||||
CALLER,
|
||||
accept,
|
||||
acquisition,
|
||||
acquire,
|
||||
closeSession,
|
||||
dispatch,
|
||||
eventually,
|
||||
heldBehindStart,
|
||||
host,
|
||||
proveOpen,
|
||||
providerStarted,
|
||||
publishFirst,
|
||||
restartWith as restartHostWith,
|
||||
stop,
|
||||
store,
|
||||
submission,
|
||||
registerStartupAttemptHostTests
|
||||
} from './structured-agent-session-startup-attempt-host-test-support'
|
||||
|
||||
const CALLER = { callerKey: 'client-1' }
|
||||
|
||||
const eventually = (assertion: () => void | Promise<void>, interval = 0): Promise<void> =>
|
||||
vi.waitFor(assertion, { timeout: 10_000, interval })
|
||||
|
||||
let root: string
|
||||
let store: AgentSessionRecordStore
|
||||
let host: StructuredAgentSessionHost
|
||||
let acquire: Mock<StructuredAgentSessionAdapter['acquire']>
|
||||
let dispatch: Mock<StructuredAgentSessionAdapter['dispatch']>
|
||||
let closeSession: Mock<NonNullable<StructuredAgentSessionAdapter['closeSession']>>
|
||||
let setOption: Mock<StructuredAgentSessionAdapter['setOption']>
|
||||
let startupLimits: Partial<StructuredAgentSessionStartupLimits> | undefined
|
||||
let supportsGoals = false
|
||||
const providerStarted = vi.fn()
|
||||
const recordLiveListing = vi.fn()
|
||||
const openGates = new Map<string, (model: string) => void>()
|
||||
|
||||
async function proveOpen(model = 'default'): Promise<void> {
|
||||
const generation = `generation-${acquire.mock.calls.length}`
|
||||
await eventually(() => expect(openGates.has(generation)).toBe(true))
|
||||
openGates.get(generation)!(model)
|
||||
openGates.delete(generation)
|
||||
}
|
||||
|
||||
function acquisition(input: StructuredAgentSessionAcquireInput): AgentSessionAcquisition {
|
||||
return {
|
||||
process: {
|
||||
hostId: 'local',
|
||||
pid: 4242,
|
||||
processStartTimeMs: 1_700_000_000_000,
|
||||
spawnToken: input.spawnToken
|
||||
},
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
link: {
|
||||
linkId: `link-${input.fence}`,
|
||||
handle: codexProviderHandle(THREAD),
|
||||
origin: store.getRecord(SESSION)?.providerHandleChain.length ? 'resumed' : 'created',
|
||||
mintedAtFence: input.fence,
|
||||
observedAt: NOW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const publishFirst: StructuredAgentSessionAdapter['acquire'] = async (input) => acquisition(input)
|
||||
|
||||
async function startHost(): Promise<void> {
|
||||
host = new StructuredAgentSessionHost({
|
||||
agents: supportsGoals ? claudeAndCodexDeclared() : NO_STRUCTURED_AGENTS,
|
||||
logger: createStructuredAgentSessionLogger(),
|
||||
store,
|
||||
adapter: {
|
||||
open: ({ acquisitionGeneration }) =>
|
||||
new Promise((resolve) => {
|
||||
openGates.set(acquisitionGeneration!, (model) =>
|
||||
resolve({ reportedOptions: { model, reportedPermissionMode: 'ask' } })
|
||||
)
|
||||
}),
|
||||
acquire,
|
||||
dispatch,
|
||||
closeSession,
|
||||
releaseAcquisition: vi.fn(async () => true),
|
||||
cancelTurn: vi.fn(async () => ({ cancelled: false })),
|
||||
answerPrompt: vi.fn(async () => undefined),
|
||||
setOption
|
||||
},
|
||||
modelCatalog: {
|
||||
peek: () => null,
|
||||
read: vi.fn(async () => ({ origin: 'unknown' as const })),
|
||||
recordLiveListing,
|
||||
retainedLiveModelIds: vi.fn(async () => []),
|
||||
prewarm: vi.fn(async () => {}),
|
||||
stop: vi.fn(),
|
||||
providerStarted
|
||||
},
|
||||
journalDatabase: openTestJournalHostDatabase(root),
|
||||
claimKeyId: 'key-1',
|
||||
mintSpawnToken: () => `spawn-${acquire.mock.calls.length}`,
|
||||
idleSweep: { intervalMs: 60 * 60_000, idleMs: 60 * 60_000 },
|
||||
...(startupLimits === undefined ? {} : { startupLimits }),
|
||||
now: () => NOW
|
||||
})
|
||||
}
|
||||
|
||||
async function restartWith(next: StructuredAgentSessionAdapter['acquire']): Promise<void> {
|
||||
await host.close(SESSION, 'evict')
|
||||
await host.flushAllStreamedEvents()
|
||||
acquire.mockImplementation(next)
|
||||
closeSession.mockClear()
|
||||
await startHost()
|
||||
acquire.mockClear()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers({
|
||||
toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval']
|
||||
})
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-startup-attempt-'))
|
||||
resetHostTestOperationIds()
|
||||
beforeEach(() => {
|
||||
startupLimits = undefined
|
||||
supportsGoals = false
|
||||
providerStarted.mockReset()
|
||||
recordLiveListing.mockReset()
|
||||
openGates.clear()
|
||||
acquire = vi.fn(async (input) => acquisition(input))
|
||||
dispatch = vi.fn(async () => ({
|
||||
state: 'accepted' as const,
|
||||
providerIdentity: {
|
||||
provider: 'codex' as const,
|
||||
threadId: THREAD,
|
||||
turnId: `turn-${dispatch.mock.calls.length}`,
|
||||
ordinal: dispatch.mock.calls.length
|
||||
}
|
||||
}))
|
||||
closeSession = vi.fn(async () => true)
|
||||
setOption = vi.fn(async () => undefined)
|
||||
store = await openTestAgentSessionRecordStore(root)
|
||||
await startHost()
|
||||
expect(await host.attach(CALLER, hostTestAttachParams(null))).toMatchObject({ ok: true })
|
||||
})
|
||||
registerStartupAttemptHostTests()
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await host.flushAllStreamedEvents()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
function sendParams(text: string, clientOperationId = hostTestOperationId()) {
|
||||
const body = hostTestMessage(text)
|
||||
return {
|
||||
envelope: {
|
||||
sessionId: SESSION,
|
||||
clientOperationId,
|
||||
expectedRuntimeFence: 1,
|
||||
payloadFingerprint: computeAgentSessionPayloadFingerprint({
|
||||
method: 'agentSession.send',
|
||||
sessionId: SESSION,
|
||||
fields: { body }
|
||||
})
|
||||
},
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
async function accept(text: string, clientOperationId?: string): Promise<string> {
|
||||
const params = sendParams(text, clientOperationId)
|
||||
expect(await host.send(CALLER, params)).toMatchObject({
|
||||
ok: true,
|
||||
value: { submission: { dispatchState: 'pending', handoverRecorded: true } }
|
||||
})
|
||||
return params.envelope.clientOperationId
|
||||
}
|
||||
|
||||
async function submission(id: string): Promise<AgentJournalSubmission | undefined> {
|
||||
return (await host.journalSnapshot(SESSION)).submissions.find(
|
||||
(entry) => entry.clientMessageId === id
|
||||
)
|
||||
}
|
||||
|
||||
async function heldBehindStart(text: string, interval = 0): Promise<string> {
|
||||
const id = await accept(text)
|
||||
await eventually(
|
||||
async () =>
|
||||
expect((await host.readStatusSummary(SESSION))?.hostExecutionPhase).toBe('starting'),
|
||||
interval
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
function stop() {
|
||||
return host.cancel(CALLER, {
|
||||
envelope: {
|
||||
sessionId: SESSION,
|
||||
clientOperationId: hostTestOperationId(),
|
||||
expectedRuntimeFence: null,
|
||||
payloadFingerprint: computeAgentSessionPayloadFingerprint({
|
||||
method: 'agentSession.cancel',
|
||||
sessionId: SESSION,
|
||||
fields: { turnId: 'turn-none' }
|
||||
})
|
||||
},
|
||||
turnId: 'turn-none'
|
||||
})
|
||||
async function restartWith(next: StructuredAgentSessionAdapter['acquire']): Promise<void> {
|
||||
await restartHostWith(next, { startupLimits, supportsGoals })
|
||||
}
|
||||
|
||||
const HOST_STOPPED = agentSessionFailureWords(agentSessionFailureFact('hostStopped'), {
|
||||
@@ -272,59 +94,6 @@ describe('a send while the agent starts', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'keeps current-generation reports across readiness (replaced=%s)',
|
||||
async (replaced) => {
|
||||
await store.replaceSessionOptions({
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
permissionMode: 'own',
|
||||
now: NOW
|
||||
})
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
const fence = store.getRecord(SESSION)!.lease.runtimeFence
|
||||
const acquisitionGeneration = `generation-${acquire.mock.calls.length}`
|
||||
const report = (
|
||||
reportedOptions: { reportedPermissionMode?: string | null },
|
||||
generation = acquisitionGeneration,
|
||||
reportFence = fence
|
||||
) =>
|
||||
host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: reportFence,
|
||||
acquisitionGeneration: generation,
|
||||
reportedOptions
|
||||
})
|
||||
const early = report({ reportedPermissionMode: 'accept-edits' })
|
||||
expect(host.readStatusSummary(SESSION)?.runtimePermission).toBeNull()
|
||||
if (replaced) {
|
||||
await host.close(SESSION, 'evict')
|
||||
await early
|
||||
await proveOpen()
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(host.readStatusSummary(SESSION)?.runtimePermission).toBeNull()
|
||||
return
|
||||
}
|
||||
await proveOpen()
|
||||
await early
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
const current = () => host.readStatusSummary(SESSION)?.runtimePermission
|
||||
expect(current()).toEqual({ fence, mode: 'accept-edits' })
|
||||
await report({ reportedPermissionMode: 'full-access' }, 'foreign')
|
||||
await report({ reportedPermissionMode: 'full-access' }, acquisitionGeneration, fence + 1)
|
||||
await report({})
|
||||
expect(current()).toEqual({ fence, mode: 'accept-edits' })
|
||||
await report({ reportedPermissionMode: null })
|
||||
expect(current()).toEqual({ fence, mode: null })
|
||||
expect(store.getRecord(SESSION)?.permissionMode).toBe('own')
|
||||
expect(dispatch).toHaveBeenCalledTimes(1)
|
||||
await host.close(SESSION, 'evict')
|
||||
expect(current()).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
describe('a start that ends before it proves itself', () => {
|
||||
it.each([false, true])(
|
||||
'settles only its own message after startup failure (acquisition refusal: %s)',
|
||||
@@ -486,64 +255,6 @@ describe('a start that ends before it proves itself', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('what a started child reports', () => {
|
||||
it('keeps provider reports out of the saved next-message choice', async () => {
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
await proveOpen('reported')
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
await host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'from-settings' }
|
||||
})
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(store.getRecord(SESSION)?.options).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves a later accepted pick when an older child reports options', async () => {
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
await store.replaceSessionOptions({
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
options: { model: 'picked' },
|
||||
now: NOW
|
||||
})
|
||||
await proveOpen('reported')
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
await host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'from-settings' }
|
||||
})
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(store.getRecord(SESSION)?.options).toEqual({ model: 'picked' })
|
||||
})
|
||||
|
||||
it('saves a settings readback that arrives before open proves the start', async () => {
|
||||
await restartWith(publishFirst)
|
||||
await heldBehindStart('hello')
|
||||
const listing = { models: [], configuredDefault: { modelId: 'opus' } }
|
||||
const reported = host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'opus' },
|
||||
catalogListing: listing
|
||||
})
|
||||
await proveOpen()
|
||||
await reported
|
||||
|
||||
await eventually(() => expect(recordLiveListing).toHaveBeenCalledWith(SESSION, listing))
|
||||
})
|
||||
})
|
||||
|
||||
function startupAttemptOpen(): boolean {
|
||||
return host.collaboratorsForTests().runtimeState.startupAttempts.isOpen(SESSION)
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
HOST_TEST_NOW as NOW,
|
||||
HOST_TEST_SESSION as SESSION
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import {
|
||||
acquire,
|
||||
dispatch,
|
||||
eventually,
|
||||
heldBehindStart,
|
||||
host,
|
||||
proveOpen,
|
||||
publishFirst,
|
||||
recordLiveListing,
|
||||
restartWith,
|
||||
store,
|
||||
submission,
|
||||
registerStartupAttemptHostTests
|
||||
} from './structured-agent-session-startup-attempt-host-test-support'
|
||||
|
||||
registerStartupAttemptHostTests()
|
||||
|
||||
it.each([false, true])(
|
||||
'keeps current-generation reports across readiness (replaced=%s)',
|
||||
async (replaced) => {
|
||||
await store.replaceSessionOptions({
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
permissionMode: 'own',
|
||||
now: NOW
|
||||
})
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
const fence = store.getRecord(SESSION)!.lease.runtimeFence
|
||||
const acquisitionGeneration = `generation-${acquire.mock.calls.length}`
|
||||
const report = (
|
||||
reportedOptions: { reportedPermissionMode?: string | null },
|
||||
generation = acquisitionGeneration,
|
||||
reportFence = fence
|
||||
) =>
|
||||
host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: reportFence,
|
||||
acquisitionGeneration: generation,
|
||||
reportedOptions
|
||||
})
|
||||
const early = report({ reportedPermissionMode: 'accept-edits' })
|
||||
expect(host.readStatusSummary(SESSION)?.runtimePermission).toBeNull()
|
||||
if (replaced) {
|
||||
await host.close(SESSION, 'evict')
|
||||
await early
|
||||
await proveOpen()
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(host.readStatusSummary(SESSION)?.runtimePermission).toBeNull()
|
||||
return
|
||||
}
|
||||
await proveOpen()
|
||||
await early
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
const current = () => host.readStatusSummary(SESSION)?.runtimePermission
|
||||
expect(current()).toEqual({ fence, mode: 'accept-edits' })
|
||||
await report({ reportedPermissionMode: 'full-access' }, 'foreign')
|
||||
await report({ reportedPermissionMode: 'full-access' }, acquisitionGeneration, fence + 1)
|
||||
await report({})
|
||||
expect(current()).toEqual({ fence, mode: 'accept-edits' })
|
||||
await report({ reportedPermissionMode: null })
|
||||
expect(current()).toEqual({ fence, mode: null })
|
||||
expect(store.getRecord(SESSION)?.permissionMode).toBe('own')
|
||||
expect(dispatch).toHaveBeenCalledTimes(1)
|
||||
await host.close(SESSION, 'evict')
|
||||
expect(current()).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
describe('what a started child reports', () => {
|
||||
it('keeps provider reports out of the saved next-message choice', async () => {
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
await proveOpen('reported')
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
await host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'from-settings' }
|
||||
})
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(store.getRecord(SESSION)?.options).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves a later accepted pick when an older child reports options', async () => {
|
||||
await restartWith(publishFirst)
|
||||
const id = await heldBehindStart('hello')
|
||||
await store.replaceSessionOptions({
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
options: { model: 'picked' },
|
||||
now: NOW
|
||||
})
|
||||
await proveOpen('reported')
|
||||
await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted'))
|
||||
await host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'from-settings' }
|
||||
})
|
||||
await host.flushStreamedEvents(SESSION)
|
||||
expect(store.getRecord(SESSION)?.options).toEqual({ model: 'picked' })
|
||||
})
|
||||
|
||||
it('saves a settings readback that arrives before open proves the start', async () => {
|
||||
await restartWith(publishFirst)
|
||||
await heldBehindStart('hello')
|
||||
const listing = { models: [], configuredDefault: { modelId: 'opus' } }
|
||||
const reported = host.handleAdapterEvent({
|
||||
type: 'options-reported',
|
||||
sessionId: SESSION,
|
||||
fence: store.getRecord(SESSION)!.lease.runtimeFence,
|
||||
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
|
||||
reportedOptions: { model: 'opus' },
|
||||
catalogListing: listing
|
||||
})
|
||||
await proveOpen()
|
||||
await reported
|
||||
|
||||
await eventually(() => expect(recordLiveListing).toHaveBeenCalledWith(SESSION, listing))
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,20 @@ export function structuredStatusChildWork(
|
||||
return { children, ...(tasks ? { backgroundTasks: tasks } : {}) }
|
||||
}
|
||||
|
||||
/** Exclude per-tick usage from global broadcasts; chat background channels retain it. */
|
||||
export function structuredStatusChildWorkFields(
|
||||
views: readonly AgentChildWorkView[] | undefined,
|
||||
provider: StructuredAgentId
|
||||
): StructuredStatusChildWork {
|
||||
const { children, backgroundTasks } = structuredStatusChildWork(views, provider)
|
||||
return {
|
||||
...(backgroundTasks && backgroundTasks.length > 0
|
||||
? { backgroundTasks: backgroundTasks.map(({ totalTokens: _tokens, ...task }) => task) }
|
||||
: {}),
|
||||
...(children ? { children } : {})
|
||||
}
|
||||
}
|
||||
|
||||
export function structuredStatusChildrenEqual(
|
||||
a: readonly AgentChildWorkView[] | undefined,
|
||||
b: readonly AgentChildWorkView[] | undefined
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
retainedStructuredStatusSummaries,
|
||||
structuredAgentSessionStatusSummary
|
||||
} from './structured-agent-session-status-summary'
|
||||
import { structuredStatusChildWork } from './structured-agent-session-status-child-work'
|
||||
import { structuredStatusChildWorkFields } from './structured-agent-session-status-child-work'
|
||||
import {
|
||||
StructuredAgentSessionJournalProjections,
|
||||
type StructuredAgentSessionJournalProjection
|
||||
@@ -244,16 +244,7 @@ export class StructuredAgentSessionStatusFeed {
|
||||
sessionId: string,
|
||||
provider: StatusFeedSession['params']['provider']
|
||||
): Pick<AgentSessionStatusSummary, 'children' | 'backgroundTasks'> {
|
||||
const { children, backgroundTasks } = structuredStatusChildWork(
|
||||
this.readChildWork(sessionId),
|
||||
provider
|
||||
)
|
||||
return {
|
||||
...(backgroundTasks && backgroundTasks.length > 0
|
||||
? { backgroundTasks: backgroundTasks.map(({ totalTokens: _tokens, ...task }) => task) }
|
||||
: {}),
|
||||
...(children ? { children } : {})
|
||||
}
|
||||
return structuredStatusChildWorkFields(this.readChildWork(sessionId), provider)
|
||||
}
|
||||
|
||||
/** Publish evidence then refold parent/summary; sink failure must not block other publications. */
|
||||
|
||||
@@ -41,7 +41,7 @@ import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-typ
|
||||
import { applyWebSessionTabsSnapshot } from './web-session-tabs-sync/snapshot-api'
|
||||
import { applyWebSessionTabsStorePatch } from './web-session-tabs-sync/store-patch'
|
||||
import { applyLocalStructuredSessionTabSnapshots } from './local-structured-session-tabs-sync/snapshot-apply'
|
||||
import { readStructuredAgentLaunchRecord } from '@/lib/structured-agent-session-launch-persistence'
|
||||
import { readStructuredAgentLaunchRecord } from '@/lib/agent-session/launch/structured-agent-session-launch-persistence'
|
||||
|
||||
const mock = vi.hoisted(() => ({
|
||||
rpc: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user