mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(native-chat): preserve coalesced launch recovery
This commit is contained in:
@@ -258,7 +258,7 @@ export async function submitFolderWorkspaceCreate({
|
||||
await launch.launchResult
|
||||
} catch (error) {
|
||||
if (!(error instanceof StructuredAgentSessionCreateRefusalError)) {
|
||||
return true
|
||||
return !launch.isVisibilityUnknown()
|
||||
}
|
||||
await refusalFallback
|
||||
}
|
||||
|
||||
@@ -236,25 +236,32 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) {
|
||||
...(structuredLaunch ? { providesInitialSurface: true } : {})
|
||||
})
|
||||
|
||||
const { structuredLaunchAccepted, activation } = await settleFullCreationStructuredLaunch({
|
||||
structuredLaunch,
|
||||
agent: tuiAgent,
|
||||
worktreeId: worktree.id,
|
||||
prompt: startupPlan?.draftPrompt ?? submitStartupPrompt,
|
||||
initialActivation,
|
||||
onDefinitiveRefusal: async () => {
|
||||
if (pendingFirstAgentMessageRename) {
|
||||
await applyWorktreeMeta(worktree.id, { pendingFirstAgentMessageRename: true }).catch(
|
||||
() => undefined
|
||||
)
|
||||
const { structuredLaunchAccepted, visibilityUnknown, activation } =
|
||||
await settleFullCreationStructuredLaunch({
|
||||
structuredLaunch,
|
||||
agent: tuiAgent,
|
||||
worktreeId: worktree.id,
|
||||
prompt: startupPlan?.draftPrompt ?? submitStartupPrompt,
|
||||
initialActivation,
|
||||
onDefinitiveRefusal: async () => {
|
||||
if (pendingFirstAgentMessageRename) {
|
||||
await applyWorktreeMeta(worktree.id, { pendingFirstAgentMessageRename: true }).catch(
|
||||
() => undefined
|
||||
)
|
||||
}
|
||||
return activateAndRevealWorktree(worktree.id, {
|
||||
sidebarRevealBehavior: 'auto',
|
||||
createNewTerminalForStartup: true,
|
||||
...(startup ? { startup } : {})
|
||||
})
|
||||
}
|
||||
return activateAndRevealWorktree(worktree.id, {
|
||||
sidebarRevealBehavior: 'auto',
|
||||
createNewTerminalForStartup: true,
|
||||
...(startup ? { startup } : {})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (visibilityUnknown) {
|
||||
setSidebarOpen(true)
|
||||
onCreated?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!structuredLaunchAccepted && startupPlan) {
|
||||
const optionScopeKey =
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
startStructuredCodexLaunch: vi.fn(),
|
||||
activateStructuredAgentSessionById: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/structured-agent-session-launch', () => ({
|
||||
startStructuredCodexLaunch: mocks.startStructuredCodexLaunch
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/structured-agent-session-tab-activation', () => ({
|
||||
activateStructuredAgentSessionById: mocks.activateStructuredAgentSessionById
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/launch-structured-codex-session', () => ({
|
||||
StructuredAgentSessionCreateRefusalError: class extends Error {}
|
||||
}))
|
||||
|
||||
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
|
||||
import { settleFullCreationStructuredLaunch } from './full-creation-structured-launch'
|
||||
|
||||
describe('settleFullCreationStructuredLaunch', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('runs the legacy terminal fallback after a definitive refusal', async () => {
|
||||
const fallbackActivation = { primaryTabId: 'fallback-tab' }
|
||||
const onDefinitiveRefusal = vi.fn().mockResolvedValue(fallbackActivation)
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
launchResult: Promise.reject(new StructuredAgentSessionCreateRefusalError('unsupported')),
|
||||
isVisibilityUnknown: () => false,
|
||||
claimDefinitiveRefusalFallback: (fallback: () => Promise<unknown>) =>
|
||||
Promise.resolve()
|
||||
.then(fallback)
|
||||
.then(() => true)
|
||||
})
|
||||
|
||||
await expect(
|
||||
settleFullCreationStructuredLaunch({
|
||||
structuredLaunch: true,
|
||||
agent: 'codex',
|
||||
worktreeId: 'worktree-1',
|
||||
prompt: 'Fix the route',
|
||||
initialActivation: false,
|
||||
onDefinitiveRefusal
|
||||
})
|
||||
).resolves.toEqual({
|
||||
structuredLaunchAccepted: false,
|
||||
visibilityUnknown: false,
|
||||
activation: fallbackActivation
|
||||
})
|
||||
expect(onDefinitiveRefusal).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('reports an unknown outcome without starting a fallback terminal', async () => {
|
||||
const onDefinitiveRefusal = vi.fn()
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
launchResult: Promise.reject(new Error('connection lost')),
|
||||
isVisibilityUnknown: () => true,
|
||||
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
|
||||
})
|
||||
|
||||
await expect(
|
||||
settleFullCreationStructuredLaunch({
|
||||
structuredLaunch: true,
|
||||
agent: 'codex',
|
||||
worktreeId: 'worktree-1',
|
||||
prompt: 'Fix the route',
|
||||
initialActivation: false,
|
||||
onDefinitiveRefusal
|
||||
})
|
||||
).resolves.toEqual({
|
||||
structuredLaunchAccepted: true,
|
||||
visibilityUnknown: true,
|
||||
activation: false
|
||||
})
|
||||
expect(onDefinitiveRefusal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -13,11 +13,15 @@ export async function settleFullCreationStructuredLaunch(args: {
|
||||
prompt: string
|
||||
initialActivation: Activation
|
||||
onDefinitiveRefusal: () => Activation | Promise<Activation>
|
||||
}): Promise<{ structuredLaunchAccepted: boolean; activation: Activation }> {
|
||||
}): Promise<{
|
||||
structuredLaunchAccepted: boolean
|
||||
visibilityUnknown: boolean
|
||||
activation: Activation
|
||||
}> {
|
||||
let activation = args.initialActivation
|
||||
let structuredLaunchAccepted = args.structuredLaunch
|
||||
if (!args.structuredLaunch || args.agent !== 'codex') {
|
||||
return { structuredLaunchAccepted, activation }
|
||||
return { structuredLaunchAccepted, visibilityUnknown: false, activation }
|
||||
}
|
||||
|
||||
const launch = startStructuredCodexLaunch(args.worktreeId, { prompt: args.prompt })
|
||||
@@ -34,7 +38,9 @@ export async function settleFullCreationStructuredLaunch(args: {
|
||||
} catch (error) {
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
await refusalFallback
|
||||
} else if (launch.isVisibilityUnknown()) {
|
||||
return { structuredLaunchAccepted, visibilityUnknown: true, activation }
|
||||
}
|
||||
}
|
||||
return { structuredLaunchAccepted, activation }
|
||||
return { structuredLaunchAccepted, visibilityUnknown: false, activation }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
startStructuredCodexLaunch: vi.fn(),
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
preflightAgentTrust: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/structured-agent-session-launch', () => ({
|
||||
startStructuredCodexLaunch: mocks.startStructuredCodexLaunch
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: mocks.activateAndRevealWorktree
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-trust-preflight', () => ({
|
||||
preflightAgentTrust: mocks.preflightAgentTrust
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/launch-structured-codex-session', () => ({
|
||||
StructuredAgentSessionCreateRefusalError: class extends Error {}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/native-chat-transcript-readability', () => ({
|
||||
isNativeChatTranscriptLocalReadable: vi.fn(() => true)
|
||||
}))
|
||||
|
||||
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
|
||||
import { settleDirectWorkItemStructuredLaunch } from './launch-work-item-direct-agent-routing'
|
||||
|
||||
const baseArgs = {
|
||||
structuredLaunch: true,
|
||||
agent: 'codex' as const,
|
||||
worktreeId: 'worktree-1',
|
||||
workspacePath: '/repo/worktree',
|
||||
connectionId: null,
|
||||
draftContent: 'Fix the route',
|
||||
promptDelivery: 'draft' as const,
|
||||
primaryTabId: null,
|
||||
startupPlan: null,
|
||||
launchSource: 'task_page' as const
|
||||
}
|
||||
|
||||
describe('settleDirectWorkItemStructuredLaunch', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('runs the legacy terminal fallback after a definitive refusal', async () => {
|
||||
mocks.activateAndRevealWorktree.mockReturnValue({ primaryTabId: 'fallback-tab' })
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
launchResult: Promise.reject(new StructuredAgentSessionCreateRefusalError('unsupported')),
|
||||
isVisibilityUnknown: () => false,
|
||||
claimDefinitiveRefusalFallback: (fallback: () => Promise<unknown>) =>
|
||||
Promise.resolve()
|
||||
.then(fallback)
|
||||
.then(() => true)
|
||||
})
|
||||
|
||||
await expect(settleDirectWorkItemStructuredLaunch(baseArgs)).resolves.toEqual({
|
||||
completed: false,
|
||||
structuredLaunch: false,
|
||||
visibilityUnknown: false,
|
||||
primaryTabId: 'fallback-tab'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports an unknown outcome without starting a fallback terminal', async () => {
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
launchResult: Promise.reject(new Error('connection lost')),
|
||||
isVisibilityUnknown: () => true,
|
||||
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
|
||||
})
|
||||
|
||||
await expect(settleDirectWorkItemStructuredLaunch(baseArgs)).resolves.toEqual({
|
||||
completed: false,
|
||||
structuredLaunch: true,
|
||||
visibilityUnknown: true,
|
||||
primaryTabId: null
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -123,11 +123,12 @@ export async function settleDirectWorkItemStructuredLaunch(args: {
|
||||
}): Promise<{
|
||||
completed: boolean
|
||||
structuredLaunch: boolean
|
||||
visibilityUnknown: boolean
|
||||
primaryTabId: string | null
|
||||
}> {
|
||||
let { structuredLaunch, primaryTabId } = args
|
||||
if (!structuredLaunch || args.agent !== 'codex') {
|
||||
return { completed: false, structuredLaunch, primaryTabId }
|
||||
return { completed: false, structuredLaunch, visibilityUnknown: false, primaryTabId }
|
||||
}
|
||||
|
||||
const launch = startStructuredCodexLaunch(args.worktreeId, {
|
||||
@@ -155,12 +156,18 @@ export async function settleDirectWorkItemStructuredLaunch(args: {
|
||||
})
|
||||
try {
|
||||
await launch.launchResult
|
||||
return { completed: true, structuredLaunch, primaryTabId }
|
||||
return { completed: true, structuredLaunch, visibilityUnknown: false, primaryTabId }
|
||||
} catch (error) {
|
||||
if (!(error instanceof StructuredAgentSessionCreateRefusalError)) {
|
||||
return { completed: true, structuredLaunch, primaryTabId }
|
||||
const visibilityUnknown = launch.isVisibilityUnknown()
|
||||
return {
|
||||
completed: !visibilityUnknown,
|
||||
structuredLaunch,
|
||||
visibilityUnknown,
|
||||
primaryTabId
|
||||
}
|
||||
}
|
||||
await refusalFallback
|
||||
}
|
||||
return { completed: false, structuredLaunch, primaryTabId }
|
||||
return { completed: false, structuredLaunch, visibilityUnknown: false, primaryTabId }
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
|
||||
startupPlan,
|
||||
launchSource
|
||||
})
|
||||
if (structuredResult.visibilityUnknown) {
|
||||
return false
|
||||
}
|
||||
if (structuredResult.completed) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
|
||||
import {
|
||||
settleStructuredCodexLaunchPrompt,
|
||||
type StructuredPromptDeliveryResult
|
||||
} from '@/lib/structured-agent-session-launch-prompt'
|
||||
import type { StructuredAgentSessionOutboxEntry } from '../../../shared/structured-agent-session-outbox'
|
||||
|
||||
export type StructuredRefusalFallback = () =>
|
||||
| void
|
||||
| StructuredPromptDeliveryResult
|
||||
| Promise<void | StructuredPromptDeliveryResult>
|
||||
|
||||
export type StructuredCodexLaunchOptions = {
|
||||
prompt?: string
|
||||
promptDelivery?: 'auto-submit' | 'submit-after-ready'
|
||||
onPromptDelivered?: () => void
|
||||
}
|
||||
|
||||
export type StructuredLaunchCaller = {
|
||||
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
|
||||
refusalFallback: {
|
||||
callback: StructuredRefusalFallback | null
|
||||
promise: Promise<boolean>
|
||||
resolve: (ran: boolean) => void
|
||||
reject: (error: unknown) => void
|
||||
promptDeliveryPromise: Promise<StructuredPromptDeliveryResult | null>
|
||||
resolvePromptDelivery: (result: StructuredPromptDeliveryResult | null) => void
|
||||
started: boolean
|
||||
settled: boolean
|
||||
ran: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type StructuredLaunchCallerGroup = {
|
||||
outcome: 'pending' | 'published' | 'failed' | 'refused' | 'unknown' | 'cancelled'
|
||||
entries: Set<StructuredLaunchCaller>
|
||||
promptDeliveryResults: Set<Promise<StructuredPromptDeliveryResult>>
|
||||
refusalSettlement: {
|
||||
promise: Promise<boolean>
|
||||
resolve: (ran: boolean) => void
|
||||
reject: (error: unknown) => void
|
||||
settled: boolean
|
||||
failure: { error: unknown } | null
|
||||
}
|
||||
onSettled: () => void
|
||||
}
|
||||
|
||||
export function createStructuredLaunchCallerGroup(): StructuredLaunchCallerGroup {
|
||||
const refusalSettlement = Promise.withResolvers<boolean>()
|
||||
return {
|
||||
outcome: 'pending',
|
||||
entries: new Set(),
|
||||
promptDeliveryResults: new Set(),
|
||||
refusalSettlement: {
|
||||
promise: refusalSettlement.promise,
|
||||
resolve: refusalSettlement.resolve,
|
||||
reject: refusalSettlement.reject,
|
||||
settled: false,
|
||||
failure: null
|
||||
},
|
||||
onSettled: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
function settleCallerWithoutFallback(caller: StructuredLaunchCaller): void {
|
||||
if (caller.refusalFallback.settled) {
|
||||
return
|
||||
}
|
||||
caller.refusalFallback.settled = true
|
||||
caller.refusalFallback.resolve(false)
|
||||
caller.refusalFallback.resolvePromptDelivery(null)
|
||||
}
|
||||
|
||||
function finalizeRefusalSettlement(group: StructuredLaunchCallerGroup): void {
|
||||
if (
|
||||
group.outcome !== 'refused' ||
|
||||
group.refusalSettlement.settled ||
|
||||
[...group.entries].some((caller) => !caller.refusalFallback.settled)
|
||||
) {
|
||||
return
|
||||
}
|
||||
group.refusalSettlement.settled = true
|
||||
if (group.refusalSettlement.failure) {
|
||||
group.refusalSettlement.reject(group.refusalSettlement.failure.error)
|
||||
} else {
|
||||
group.refusalSettlement.resolve([...group.entries].some((caller) => caller.refusalFallback.ran))
|
||||
}
|
||||
group.onSettled()
|
||||
}
|
||||
|
||||
function runCallerRefusalFallback(
|
||||
group: StructuredLaunchCallerGroup,
|
||||
caller: StructuredLaunchCaller
|
||||
): void {
|
||||
if (caller.refusalFallback.started || caller.refusalFallback.settled) {
|
||||
return
|
||||
}
|
||||
caller.refusalFallback.started = true
|
||||
const fallback = caller.refusalFallback.callback
|
||||
if (!fallback) {
|
||||
settleCallerWithoutFallback(caller)
|
||||
finalizeRefusalSettlement(group)
|
||||
return
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(fallback)
|
||||
.then(
|
||||
(result) => {
|
||||
caller.refusalFallback.ran = true
|
||||
caller.refusalFallback.resolve(true)
|
||||
caller.refusalFallback.resolvePromptDelivery(result ?? null)
|
||||
},
|
||||
(error) => {
|
||||
group.refusalSettlement.failure ??= { error }
|
||||
caller.refusalFallback.reject(error)
|
||||
caller.refusalFallback.resolvePromptDelivery(null)
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
caller.refusalFallback.settled = true
|
||||
finalizeRefusalSettlement(group)
|
||||
})
|
||||
}
|
||||
|
||||
function trackPromptDelivery(
|
||||
group: StructuredLaunchCallerGroup,
|
||||
promptDeliveryResult: Promise<StructuredPromptDeliveryResult>
|
||||
): void {
|
||||
group.promptDeliveryResults.add(promptDeliveryResult)
|
||||
const settled = (): void => {
|
||||
group.promptDeliveryResults.delete(promptDeliveryResult)
|
||||
group.onSettled()
|
||||
}
|
||||
void promptDeliveryResult.then(settled, settled)
|
||||
}
|
||||
|
||||
export function addStructuredLaunchCaller(args: {
|
||||
group: StructuredLaunchCallerGroup
|
||||
launchResult: Promise<{ sessionId: string; fence: number }>
|
||||
options: StructuredCodexLaunchOptions
|
||||
stagedEntry: StructuredAgentSessionOutboxEntry | null
|
||||
}): StructuredLaunchCaller {
|
||||
const fallback = Promise.withResolvers<boolean>()
|
||||
const fallbackPromptDelivery = Promise.withResolvers<StructuredPromptDeliveryResult | null>()
|
||||
const caller: StructuredLaunchCaller = {
|
||||
refusalFallback: {
|
||||
callback: null,
|
||||
promise: fallback.promise,
|
||||
resolve: fallback.resolve,
|
||||
reject: fallback.reject,
|
||||
promptDeliveryPromise: fallbackPromptDelivery.promise,
|
||||
resolvePromptDelivery: fallbackPromptDelivery.resolve,
|
||||
started: false,
|
||||
settled: false,
|
||||
ran: false
|
||||
}
|
||||
}
|
||||
args.group.entries.add(caller)
|
||||
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
|
||||
launchResult: args.launchResult,
|
||||
options: args.options,
|
||||
stagedEntry: args.stagedEntry
|
||||
})
|
||||
caller.promptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
return (
|
||||
(await caller.refusalFallback.promptDeliveryPromise) ?? {
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
}
|
||||
)
|
||||
}
|
||||
return { delivered: false, failureNotified: true }
|
||||
})
|
||||
if (caller.promptDeliveryResult) {
|
||||
trackPromptDelivery(args.group, caller.promptDeliveryResult)
|
||||
}
|
||||
if (['published', 'failed', 'cancelled'].includes(args.group.outcome)) {
|
||||
settleCallerWithoutFallback(caller)
|
||||
} else if (args.group.outcome === 'refused') {
|
||||
queueMicrotask(() => runCallerRefusalFallback(args.group, caller))
|
||||
}
|
||||
return caller
|
||||
}
|
||||
|
||||
export function settleStructuredLaunchCallersWithoutFallback(
|
||||
group: StructuredLaunchCallerGroup,
|
||||
outcome: 'published' | 'failed' | 'cancelled'
|
||||
): void {
|
||||
group.outcome = outcome
|
||||
for (const caller of group.entries) {
|
||||
settleCallerWithoutFallback(caller)
|
||||
}
|
||||
if (!group.refusalSettlement.settled) {
|
||||
group.refusalSettlement.settled = true
|
||||
group.refusalSettlement.resolve(false)
|
||||
}
|
||||
group.onSettled()
|
||||
}
|
||||
|
||||
export function settleStructuredLaunchCallersWithFallback(
|
||||
group: StructuredLaunchCallerGroup
|
||||
): void {
|
||||
if (group.outcome === 'refused') {
|
||||
return
|
||||
}
|
||||
group.outcome = 'refused'
|
||||
for (const caller of group.entries) {
|
||||
runCallerRefusalFallback(group, caller)
|
||||
}
|
||||
finalizeRefusalSettlement(group)
|
||||
}
|
||||
|
||||
export function claimStructuredLaunchCallerFallback(
|
||||
group: StructuredLaunchCallerGroup,
|
||||
caller: StructuredLaunchCaller,
|
||||
fallback: StructuredRefusalFallback
|
||||
): Promise<boolean> {
|
||||
caller.refusalFallback.callback ??= fallback
|
||||
if (group.outcome === 'refused') {
|
||||
runCallerRefusalFallback(group, caller)
|
||||
}
|
||||
return caller.refusalFallback.promise
|
||||
}
|
||||
|
||||
export function structuredLaunchCallersHavePendingWork(
|
||||
group: StructuredLaunchCallerGroup
|
||||
): boolean {
|
||||
return (
|
||||
group.outcome === 'pending' ||
|
||||
group.outcome === 'unknown' ||
|
||||
group.promptDeliveryResults.size > 0 ||
|
||||
(group.outcome === 'refused' && !group.refusalSettlement.settled)
|
||||
)
|
||||
}
|
||||
@@ -196,6 +196,39 @@ describe('startStructuredCodexLaunch', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the launch reserved until every coalesced prompt delivery settles', async () => {
|
||||
const worktreeId = 'wt-coalesced-prompt-reservation'
|
||||
const intent = launchIntent(worktreeId)
|
||||
let resolveLaunch!: (receipt: { sessionId: string; fence: number }) => void
|
||||
let resolveDelivery!: (result: {
|
||||
ok: true
|
||||
value: { submission: { dispatchState: 'accepted' } }
|
||||
}) => void
|
||||
mocks.createIntent.mockReturnValue(intent)
|
||||
mocks.launch.mockImplementationOnce(() => new Promise((resolve) => (resolveLaunch = resolve)))
|
||||
vi.mocked(refreshLocalStructuredSessionTabs).mockResolvedValue([
|
||||
publishedSnapshot(worktreeId, intent.sessionId)
|
||||
])
|
||||
mocks.callStructuredAgentSession.mockImplementationOnce(
|
||||
() => new Promise((resolve) => (resolveDelivery = resolve))
|
||||
)
|
||||
|
||||
startStructuredCodexLaunch(worktreeId)
|
||||
const coalesced = startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
|
||||
resolveLaunch({ sessionId: intent.sessionId, fence: 1 })
|
||||
await vi.waitFor(() => expect(mocks.callStructuredAgentSession).toHaveBeenCalledOnce())
|
||||
|
||||
startStructuredCodexLaunch(worktreeId)
|
||||
expect(mocks.createIntent).toHaveBeenCalledOnce()
|
||||
expect(mocks.launch).toHaveBeenCalledOnce()
|
||||
|
||||
resolveDelivery({ ok: true, value: { submission: { dispatchState: 'accepted' } } })
|
||||
await expect(coalesced.promptDeliveryResult).resolves.toEqual({
|
||||
delivered: true,
|
||||
failureNotified: false
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps one launch identity per worktree while the outcome is unknown', async () => {
|
||||
const worktreeId = 'wt-unknown-different-prompts'
|
||||
const intent = launchIntent(worktreeId)
|
||||
@@ -315,7 +348,7 @@ describe('startStructuredCodexLaunch', () => {
|
||||
storageFailure.mockRestore()
|
||||
})
|
||||
|
||||
it('discards every coalesced prompt when the launch is definitively refused', async () => {
|
||||
it('runs each caller fallback and preserves its delivery result after refusal', async () => {
|
||||
const worktreeId = 'wt-refused-coalesced-prompts'
|
||||
const intent = launchIntent(worktreeId)
|
||||
let rejectLaunch!: (error: unknown) => void
|
||||
@@ -325,16 +358,36 @@ describe('startStructuredCodexLaunch', () => {
|
||||
)
|
||||
|
||||
const first = startStructuredCodexLaunch(worktreeId, { prompt: 'first prompt' })
|
||||
startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
|
||||
const fallback = first.claimDefinitiveRefusalFallback(vi.fn())
|
||||
const second = startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
|
||||
const firstFallback = vi.fn().mockResolvedValue({
|
||||
delivered: true,
|
||||
failureNotified: false
|
||||
})
|
||||
const secondFallback = vi.fn().mockResolvedValue({
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
})
|
||||
const firstFallbackResult = first.claimDefinitiveRefusalFallback(firstFallback)
|
||||
const secondFallbackResult = second.claimDefinitiveRefusalFallback(secondFallback)
|
||||
expect(readOutbox(intent.sessionId)).toHaveLength(2)
|
||||
|
||||
rejectLaunch(new StructuredAgentSessionCreateRefusalError('unsupported'))
|
||||
await expect(first.launchResult).rejects.toBeInstanceOf(
|
||||
StructuredAgentSessionCreateRefusalError
|
||||
)
|
||||
await expect(fallback).resolves.toBe(true)
|
||||
await expect(firstFallbackResult).resolves.toBe(true)
|
||||
await expect(secondFallbackResult).resolves.toBe(true)
|
||||
await expect(first.promptDeliveryResult).resolves.toEqual({
|
||||
delivered: true,
|
||||
failureNotified: false
|
||||
})
|
||||
await expect(second.promptDeliveryResult).resolves.toEqual({
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
})
|
||||
|
||||
expect(firstFallback).toHaveBeenCalledOnce()
|
||||
expect(secondFallback).toHaveBeenCalledOnce()
|
||||
expect(readOutbox(intent.sessionId)).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -16,47 +16,37 @@ import {
|
||||
type StructuredCodexLaunchReceipt,
|
||||
type StructuredLaunchRecoveryState
|
||||
} from '@/lib/structured-agent-session-launch-recovery'
|
||||
import type { StructuredPromptDeliveryResult } from '@/lib/structured-agent-session-launch-prompt'
|
||||
import {
|
||||
settleStructuredCodexLaunchPrompt,
|
||||
type StructuredPromptDeliveryResult
|
||||
} from '@/lib/structured-agent-session-launch-prompt'
|
||||
addStructuredLaunchCaller,
|
||||
claimStructuredLaunchCallerFallback,
|
||||
createStructuredLaunchCallerGroup,
|
||||
settleStructuredLaunchCallersWithFallback,
|
||||
settleStructuredLaunchCallersWithoutFallback,
|
||||
structuredLaunchCallersHavePendingWork,
|
||||
type StructuredCodexLaunchOptions,
|
||||
type StructuredLaunchCaller,
|
||||
type StructuredLaunchCallerGroup,
|
||||
type StructuredRefusalFallback
|
||||
} from '@/lib/structured-agent-session-launch-callers'
|
||||
|
||||
export type { StructuredCodexLaunchReceipt }
|
||||
|
||||
type StructuredRefusalFallback = () =>
|
||||
| void
|
||||
| StructuredPromptDeliveryResult
|
||||
| Promise<void | StructuredPromptDeliveryResult>
|
||||
export type { StructuredCodexLaunchOptions, StructuredCodexLaunchReceipt }
|
||||
|
||||
type StructuredLaunchState = StructuredLaunchRecoveryState & {
|
||||
identity: string
|
||||
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
|
||||
refusalFallback: {
|
||||
callback: StructuredRefusalFallback | null
|
||||
promise: Promise<boolean>
|
||||
resolve: (ran: boolean) => void
|
||||
reject: (error: unknown) => void
|
||||
promptDeliveryPromise: Promise<StructuredPromptDeliveryResult | null>
|
||||
resolvePromptDelivery: (result: StructuredPromptDeliveryResult | null) => void
|
||||
started: boolean
|
||||
}
|
||||
callers: StructuredLaunchCallerGroup
|
||||
}
|
||||
|
||||
type StructuredLaunchStateResult = {
|
||||
state: StructuredLaunchState
|
||||
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
|
||||
}
|
||||
|
||||
export type StructuredCodexLaunchOptions = {
|
||||
prompt?: string
|
||||
promptDelivery?: 'auto-submit' | 'submit-after-ready'
|
||||
onPromptDelivered?: () => void
|
||||
caller: StructuredLaunchCaller
|
||||
}
|
||||
|
||||
export type StructuredCodexLaunchResult = {
|
||||
sessionId: string
|
||||
launchResult: Promise<StructuredCodexLaunchReceipt>
|
||||
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
|
||||
isVisibilityUnknown: () => boolean
|
||||
claimDefinitiveRefusalFallback: (fallback: StructuredRefusalFallback) => Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -72,33 +62,20 @@ function cleanupLaunchState(state: StructuredLaunchState): void {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeCleanupLaunchState(state: StructuredLaunchState): void {
|
||||
if (structuredLaunchCallersHavePendingWork(state.callers)) {
|
||||
return
|
||||
}
|
||||
cleanupLaunchState(state)
|
||||
}
|
||||
|
||||
function settleDefinitiveRefusalFallback(state: StructuredLaunchState): void {
|
||||
if (state.refusalFallback.started) {
|
||||
if (state.callers.outcome === 'refused') {
|
||||
return
|
||||
}
|
||||
state.refusalFallback.started = true
|
||||
abandonStructuredAgentSessionLaunchIntent(state.intent)
|
||||
discardStructuredAgentSessionLaunchOutbox(state.intent.sessionId)
|
||||
const fallback = state.refusalFallback.callback
|
||||
if (!fallback) {
|
||||
state.refusalFallback.resolve(false)
|
||||
state.refusalFallback.resolvePromptDelivery(null)
|
||||
cleanupLaunchState(state)
|
||||
return
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(fallback)
|
||||
.then(
|
||||
(result) => {
|
||||
state.refusalFallback.resolve(true)
|
||||
state.refusalFallback.resolvePromptDelivery(result ?? null)
|
||||
},
|
||||
(error) => {
|
||||
state.refusalFallback.reject(error)
|
||||
state.refusalFallback.resolvePromptDelivery(null)
|
||||
}
|
||||
)
|
||||
.finally(() => cleanupLaunchState(state))
|
||||
settleStructuredLaunchCallersWithFallback(state.callers)
|
||||
}
|
||||
|
||||
function trackLaunchSettlement(
|
||||
@@ -110,12 +87,8 @@ function trackLaunchSettlement(
|
||||
if (state.promise !== promise) {
|
||||
return
|
||||
}
|
||||
state.refusalFallback.resolve(false)
|
||||
if (state.promptDeliveryResult) {
|
||||
void state.promptDeliveryResult.finally(() => cleanupLaunchState(state))
|
||||
} else {
|
||||
cleanupLaunchState(state)
|
||||
}
|
||||
settleStructuredLaunchCallersWithoutFallback(state.callers, 'published')
|
||||
maybeCleanupLaunchState(state)
|
||||
},
|
||||
(error) => {
|
||||
if (state.promise !== promise || state.cancelled) {
|
||||
@@ -124,8 +97,10 @@ function trackLaunchSettlement(
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
settleDefinitiveRefusalFallback(state)
|
||||
} else if (!state.visibilityUnknown) {
|
||||
state.refusalFallback.resolve(false)
|
||||
cleanupLaunchState(state)
|
||||
settleStructuredLaunchCallersWithoutFallback(state.callers, 'failed')
|
||||
maybeCleanupLaunchState(state)
|
||||
} else {
|
||||
state.callers.outcome = 'unknown'
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -138,7 +113,7 @@ function trackLaunchFailureToast(state: StructuredLaunchState): void {
|
||||
}
|
||||
if (
|
||||
error instanceof StructuredAgentSessionCreateRefusalError &&
|
||||
(await state.refusalFallback.promise.catch(() => false))
|
||||
(await state.callers.refusalSettlement.promise.catch(() => false))
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -160,61 +135,42 @@ function structuredCodexLaunchState(
|
||||
const existing = pendingStructuredLaunchesByIdentity.get(identity)
|
||||
if (existing) {
|
||||
if (existing.visibilityUnknown) {
|
||||
existing.callers.outcome = 'pending'
|
||||
existing.promise = reconcileUnknownLaunch(existing)
|
||||
trackLaunchSettlement(existing, existing.promise)
|
||||
trackLaunchFailureToast(existing)
|
||||
}
|
||||
const text = options.prompt?.trim() ?? ''
|
||||
const stagedPrompt = text
|
||||
? enqueueStructuredAgentSessionLaunchPrompt(existing.intent.sessionId, text)
|
||||
: null
|
||||
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
|
||||
launchResult: existing.promise,
|
||||
options,
|
||||
stagedEntry: stagedPrompt
|
||||
})
|
||||
const recoveredPromptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
return (
|
||||
(await existing.refusalFallback.promptDeliveryPromise) ?? {
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
}
|
||||
)
|
||||
}
|
||||
return { delivered: false, failureNotified: true }
|
||||
})
|
||||
const stagedPrompt =
|
||||
text && existing.callers.outcome !== 'refused'
|
||||
? enqueueStructuredAgentSessionLaunchPrompt(existing.intent.sessionId, text)
|
||||
: null
|
||||
return {
|
||||
state: existing,
|
||||
...(recoveredPromptDeliveryResult
|
||||
? { promptDeliveryResult: recoveredPromptDeliveryResult }
|
||||
: {})
|
||||
caller: addStructuredLaunchCaller({
|
||||
group: existing.callers,
|
||||
launchResult: existing.promise,
|
||||
options,
|
||||
stagedEntry: stagedPrompt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = Promise.withResolvers<boolean>()
|
||||
const fallbackPromptDelivery = Promise.withResolvers<StructuredPromptDeliveryResult | null>()
|
||||
const intent = createStructuredCodexSessionLaunchIntent(worktreeId)
|
||||
const text = options.prompt?.trim() ?? ''
|
||||
const stagedPrompt = text
|
||||
? enqueueStructuredAgentSessionLaunchPrompt(intent.sessionId, text)
|
||||
: null
|
||||
const callers = createStructuredLaunchCallerGroup()
|
||||
const state: StructuredLaunchState = {
|
||||
identity,
|
||||
intent,
|
||||
promise: Promise.resolve({ sessionId: '', fence: 0 }),
|
||||
visibilityUnknown: false,
|
||||
cancelled: false,
|
||||
refusalFallback: {
|
||||
callback: null,
|
||||
promise: fallback.promise,
|
||||
resolve: fallback.resolve,
|
||||
reject: fallback.reject,
|
||||
promptDeliveryPromise: fallbackPromptDelivery.promise,
|
||||
resolvePromptDelivery: fallbackPromptDelivery.resolve,
|
||||
started: false
|
||||
}
|
||||
callers
|
||||
}
|
||||
callers.onSettled = () => maybeCleanupLaunchState(state)
|
||||
state.promise =
|
||||
text && !stagedPrompt
|
||||
? Promise.reject(
|
||||
@@ -223,28 +179,18 @@ function structuredCodexLaunchState(
|
||||
)
|
||||
)
|
||||
: launchAndReconcile(state)
|
||||
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
|
||||
const caller = addStructuredLaunchCaller({
|
||||
group: state.callers,
|
||||
launchResult: state.promise,
|
||||
options,
|
||||
stagedEntry: stagedPrompt
|
||||
})
|
||||
state.promptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
return (
|
||||
(await state.refusalFallback.promptDeliveryPromise) ?? {
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
}
|
||||
)
|
||||
}
|
||||
return { delivered: false, failureNotified: true }
|
||||
})
|
||||
pendingStructuredLaunchesByIdentity.set(identity, state)
|
||||
trackLaunchSettlement(state, state.promise)
|
||||
trackLaunchFailureToast(state)
|
||||
return {
|
||||
state,
|
||||
...(state.promptDeliveryResult ? { promptDeliveryResult: state.promptDeliveryResult } : {})
|
||||
caller
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +203,7 @@ export function cancelStructuredCodexLaunch(worktreeId: string, sessionId: strin
|
||||
return false
|
||||
}
|
||||
state.cancelled = true
|
||||
settleStructuredLaunchCallersWithoutFallback(state.callers, 'cancelled')
|
||||
cleanupLaunchState(state)
|
||||
discardStructuredAgentSessionLaunchOutbox(state.intent.sessionId)
|
||||
abandonStructuredAgentSessionLaunchIntent(state.intent)
|
||||
@@ -267,14 +214,13 @@ export function startStructuredCodexLaunch(
|
||||
worktreeId: string,
|
||||
options: StructuredCodexLaunchOptions = {}
|
||||
): StructuredCodexLaunchResult {
|
||||
const { state, promptDeliveryResult } = structuredCodexLaunchState(worktreeId, options)
|
||||
const { state, caller } = structuredCodexLaunchState(worktreeId, options)
|
||||
return {
|
||||
sessionId: state.intent.sessionId,
|
||||
launchResult: state.promise,
|
||||
...(promptDeliveryResult ? { promptDeliveryResult } : {}),
|
||||
claimDefinitiveRefusalFallback: (fallback) => {
|
||||
state.refusalFallback.callback ??= fallback
|
||||
return state.refusalFallback.promise
|
||||
}
|
||||
...(caller.promptDeliveryResult ? { promptDeliveryResult: caller.promptDeliveryResult } : {}),
|
||||
isVisibilityUnknown: () => state.visibilityUnknown,
|
||||
claimDefinitiveRefusalFallback: (fallback) =>
|
||||
claimStructuredLaunchCallerFallback(state.callers, caller, fallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { preflightAgentTrust as preflightWorkspaceAgentTrust } from '@/lib/agent-trust-preflight'
|
||||
import { activateAndRevealWorktree, type ActivateAndRevealResult } from '@/lib/worktree-activation'
|
||||
@@ -225,6 +226,16 @@ export async function executeWorktreeCreation(
|
||||
if (structuredSession.cancelled) {
|
||||
return
|
||||
}
|
||||
if (structuredSession.visibilityUnknown) {
|
||||
useAppStore.getState().updatePendingWorktreeCreation(creationId, {
|
||||
status: 'error',
|
||||
error: translate(
|
||||
'auto.lib.worktree.creation.flow.structured.launch.unknown',
|
||||
'Could not confirm whether Codex chat opened. Retry to check again.'
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Why: clearing synchronously right after activation lets React commit the
|
||||
|
||||
@@ -81,6 +81,7 @@ describe('launchStructuredWorktreeSession', () => {
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
sessionId: 'session-1',
|
||||
launchResult,
|
||||
isVisibilityUnknown: () => false,
|
||||
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
|
||||
})
|
||||
|
||||
@@ -111,6 +112,7 @@ describe('launchStructuredWorktreeSession', () => {
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
accepted: true,
|
||||
cancelled: true,
|
||||
visibilityUnknown: false,
|
||||
activation: false,
|
||||
primaryTabId: null
|
||||
})
|
||||
@@ -124,4 +126,44 @@ describe('launchStructuredWorktreeSession', () => {
|
||||
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
|
||||
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('reports an unknown launch without claiming a visible surface', async () => {
|
||||
mocks.startStructuredCodexLaunch.mockReturnValue({
|
||||
sessionId: 'session-unknown',
|
||||
launchResult: Promise.reject(new Error('connection lost')),
|
||||
isVisibilityUnknown: () => true,
|
||||
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
|
||||
})
|
||||
|
||||
await expect(
|
||||
launchStructuredWorktreeSession({
|
||||
creationId: 'creation-1',
|
||||
request: {
|
||||
repoId: 'repo-1',
|
||||
name: 'routing-recovery',
|
||||
setupDecision: 'run',
|
||||
agent: 'codex',
|
||||
pendingFirstAgentMessageRename: false,
|
||||
note: '',
|
||||
startupPlan: null,
|
||||
quickPrompt: 'Fix the route',
|
||||
quickTelemetry: null
|
||||
},
|
||||
worktreeId: 'worktree-1',
|
||||
shouldActivateOnCompletion: true,
|
||||
fallbackStartupOpt: undefined,
|
||||
activation: false,
|
||||
primaryTabId: null
|
||||
})
|
||||
).resolves.toEqual({
|
||||
accepted: true,
|
||||
cancelled: false,
|
||||
visibilityUnknown: true,
|
||||
activation: false,
|
||||
primaryTabId: null
|
||||
})
|
||||
|
||||
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
|
||||
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
|
||||
export type WorktreeCreationStructuredSessionResult = {
|
||||
accepted: boolean
|
||||
cancelled: boolean
|
||||
visibilityUnknown: boolean
|
||||
activation: ActivateAndRevealResult | false
|
||||
primaryTabId: string | null
|
||||
}
|
||||
@@ -45,11 +46,12 @@ export async function launchStructuredWorktreeSession(args: {
|
||||
}): Promise<WorktreeCreationStructuredSessionResult> {
|
||||
let { activation, primaryTabId } = args
|
||||
let accepted = true
|
||||
let visibilityUnknown = false
|
||||
if (args.request.agent !== 'codex') {
|
||||
return { accepted, cancelled: false, activation, primaryTabId }
|
||||
return { accepted, cancelled: false, visibilityUnknown, activation, primaryTabId }
|
||||
}
|
||||
if (!useAppStore.getState().pendingWorktreeCreations[args.creationId]) {
|
||||
return { accepted, cancelled: true, activation, primaryTabId }
|
||||
return { accepted, cancelled: true, visibilityUnknown, activation, primaryTabId }
|
||||
}
|
||||
|
||||
const launch = startStructuredCodexLaunch(args.worktreeId, {
|
||||
@@ -127,7 +129,7 @@ export async function launchStructuredWorktreeSession(args: {
|
||||
const receipt = await launch.launchResult
|
||||
if (cancelled) {
|
||||
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
|
||||
return { accepted, cancelled, activation, primaryTabId }
|
||||
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
|
||||
}
|
||||
if (args.shouldActivateOnCompletion) {
|
||||
activateStructuredAgentSessionById({
|
||||
@@ -138,13 +140,15 @@ export async function launchStructuredWorktreeSession(args: {
|
||||
} catch (error) {
|
||||
if (cancelled) {
|
||||
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
|
||||
return { accepted, cancelled, activation, primaryTabId }
|
||||
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
|
||||
}
|
||||
if (error instanceof StructuredAgentSessionCreateRefusalError) {
|
||||
await refusalFallback
|
||||
} else {
|
||||
visibilityUnknown = launch.isVisibilityUnknown()
|
||||
}
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
return { accepted, cancelled, activation, primaryTabId }
|
||||
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PendingWorktreeCreation, WorktreeCreationRequest } from './pending-worktree-creation'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
ensureWorktreeHasInitialTerminal: vi.fn(),
|
||||
launchStructuredWorktreeSession: vi.fn()
|
||||
}))
|
||||
|
||||
const request: WorktreeCreationRequest = {
|
||||
repoId: 'repo-1',
|
||||
name: 'routing-recovery',
|
||||
setupDecision: 'run',
|
||||
agent: 'codex',
|
||||
agentLaunchRoute: 'structured-native-chat',
|
||||
pendingFirstAgentMessageRename: false,
|
||||
note: '',
|
||||
startupPlan: null,
|
||||
quickPrompt: 'Recover the route',
|
||||
quickTelemetry: null
|
||||
}
|
||||
|
||||
const store = {
|
||||
activeView: 'terminal',
|
||||
activePendingCreationId: 'creation-1' as string | null,
|
||||
pendingWorktreeCreations: {} as Record<string, PendingWorktreeCreation>,
|
||||
repos: [],
|
||||
createWorktree: vi.fn(),
|
||||
updatePendingWorktreeCreation: vi.fn(),
|
||||
removePendingWorktreeCreation: vi.fn(),
|
||||
updateWorktreeMeta: vi.fn()
|
||||
}
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: { getState: () => store }
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: mocks.activateAndRevealWorktree
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
|
||||
ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/new-workspace', () => ({
|
||||
ensureAgentStartupInTerminal: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workspace-activation-terminal-focus', () => ({
|
||||
queueWorkspaceActivationTerminalFocus: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/ephemeral-vm-worktree-creation', () => ({
|
||||
prepareRequestForCreate: vi.fn(async () => request),
|
||||
attachEphemeralVmRuntimeToWorkspace: vi.fn(),
|
||||
cleanupEphemeralVmRuntimeForFailedCreate: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/provisioned-root-create-options', () => ({
|
||||
getProvisionedRootCreateOptions: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-creation-agent-seeds', () => ({
|
||||
seedAgentTabStateAfterWorktreeCreate: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-draft-startup-view-mode', () => ({
|
||||
resolveBackendDraftStartup: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-creation-flow-startup', () => ({
|
||||
buildWorktreeCreationStartupOpt: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-creation-structured-session', () => ({
|
||||
launchStructuredWorktreeSession: mocks.launchStructuredWorktreeSession
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { error: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string) => fallback
|
||||
}))
|
||||
|
||||
import { executeWorktreeCreation } from './worktree-creation-flow-execute'
|
||||
|
||||
describe('structured worktree creation unknown outcome', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
store.pendingWorktreeCreations = {
|
||||
'creation-1': {
|
||||
creationId: 'creation-1',
|
||||
phase: 'creating',
|
||||
status: 'creating',
|
||||
startedAt: 1,
|
||||
indeterminate: false,
|
||||
loaderVisible: true,
|
||||
request
|
||||
}
|
||||
}
|
||||
store.createWorktree.mockResolvedValue({
|
||||
worktree: { id: 'worktree-1', repoId: 'repo-1' }
|
||||
})
|
||||
mocks.activateAndRevealWorktree.mockReturnValue(false)
|
||||
mocks.launchStructuredWorktreeSession.mockResolvedValue({
|
||||
accepted: true,
|
||||
cancelled: false,
|
||||
visibilityUnknown: true,
|
||||
activation: false,
|
||||
primaryTabId: null
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the operation on its retry surface instead of reporting completion', async () => {
|
||||
await executeWorktreeCreation('creation-1', request)
|
||||
|
||||
expect(store.updatePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', {
|
||||
status: 'error',
|
||||
error: 'Could not confirm whether Codex chat opened. Retry to check again.'
|
||||
})
|
||||
expect(store.removePendingWorktreeCreation).not.toHaveBeenCalled()
|
||||
expect(mocks.ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user