mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(agent-status): verify native chat answer delivery
This commit is contained in:
@@ -207,6 +207,7 @@ describe('sendNativeChatAskAnswer', () => {
|
||||
vi.useFakeTimers()
|
||||
sendRuntimePtyInput.mockClear()
|
||||
sendRuntimePtyInput.mockReturnValue(true)
|
||||
sendRuntimePtyInputVerified.mockReset().mockResolvedValue(true)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
@@ -281,18 +282,38 @@ describe('sendNativeChatAskAnswer', () => {
|
||||
expect(sendRuntimePtyInput).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reports delivery only after settling and suppresses it after cancellation', () => {
|
||||
it('reports verified delivery only after settling and suppresses it after cancellation', async () => {
|
||||
const onSettled = vi.fn()
|
||||
sendRuntimePtyInput.mockReturnValueOnce(true).mockReturnValueOnce(false)
|
||||
sendRuntimePtyInputVerified.mockResolvedValueOnce(true).mockResolvedValueOnce(false)
|
||||
const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '1' }, { raw: '\r' }], onSettled)
|
||||
|
||||
vi.advanceTimersByTime(handle.settleAfterMs)
|
||||
await vi.advanceTimersByTimeAsync(handle.settleAfterMs)
|
||||
expect(onSettled).toHaveBeenCalledExactlyOnceWith(false)
|
||||
expect(sendRuntimePtyInput).not.toHaveBeenCalled()
|
||||
|
||||
const canceledSettled = vi.fn()
|
||||
const canceled = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '1' }], canceledSettled)
|
||||
canceled.cancel()
|
||||
vi.runAllTimers()
|
||||
await vi.runAllTimersAsync()
|
||||
expect(canceledSettled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('waits for remote acceptance before reporting delivery', async () => {
|
||||
const onSettled = vi.fn()
|
||||
let resolveAccepted!: (accepted: boolean) => void
|
||||
sendRuntimePtyInputVerified.mockReturnValueOnce(
|
||||
new Promise<boolean>((resolve) => {
|
||||
resolveAccepted = resolve
|
||||
})
|
||||
)
|
||||
|
||||
const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '2' }], onSettled)
|
||||
await vi.advanceTimersByTimeAsync(handle.settleAfterMs)
|
||||
|
||||
expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(SETTINGS, PTY, '2')
|
||||
expect(onSettled).not.toHaveBeenCalled()
|
||||
|
||||
resolveAccepted(true)
|
||||
await vi.waitFor(() => expect(onSettled).toHaveBeenCalledExactlyOnceWith(true))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -162,13 +162,20 @@ export function sendNativeChatAskAnswer(
|
||||
return { cancel: () => {}, settleAfterMs: 0 }
|
||||
}
|
||||
const timers: ReturnType<typeof setTimeout>[] = []
|
||||
let delivered = true
|
||||
const verifiedWrites: Promise<boolean>[] = []
|
||||
let cancelled = false
|
||||
groups.forEach((group, index) => {
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
const bytes = 'raw' in group ? group.raw : buildNativeChatPasteBytes(group.text)
|
||||
if (!sendRuntimePtyInput(settings, ptyId, bytes)) {
|
||||
delivered = false
|
||||
if (onSettled) {
|
||||
// Why: inference must use the remote host's acceptance result, not
|
||||
// the fire-and-forget renderer dispatch result.
|
||||
verifiedWrites.push(
|
||||
sendRuntimePtyInputVerified(settings, ptyId, bytes).catch(() => false)
|
||||
)
|
||||
} else {
|
||||
sendRuntimePtyInput(settings, ptyId, bytes)
|
||||
}
|
||||
}, index * NATIVE_CHAT_QUESTION_STEP_MS)
|
||||
)
|
||||
@@ -178,10 +185,19 @@ export function sendNativeChatAskAnswer(
|
||||
if (onSettled) {
|
||||
// Why: status inference must wait for every paced write and must not run
|
||||
// after cancellation or a rejected runtime write.
|
||||
timers.push(setTimeout(() => onSettled(delivered), settleAfterMs))
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
void Promise.all(verifiedWrites).then((results) => {
|
||||
if (!cancelled) {
|
||||
onSettled(results.every(Boolean))
|
||||
}
|
||||
})
|
||||
}, settleAfterMs)
|
||||
)
|
||||
}
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true
|
||||
for (const timer of timers) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,19 @@ describe('useNativeChatInteractiveSend', () => {
|
||||
expect(mocks.sendNativeChatMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('infers OpenClaude answers through its Claude-compatible selector path', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'openclaude')
|
||||
)
|
||||
|
||||
act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }]))
|
||||
|
||||
const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[3]
|
||||
expect(onSettled).toBeTypeOf('function')
|
||||
onSettled?.(true)
|
||||
expect(mocks.inferQuestionAnswered).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does nothing when no option is answered', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude')
|
||||
|
||||
@@ -86,23 +86,22 @@ export function useNativeChatInteractiveSend(
|
||||
// Other agents' question tools commit a pasted answer, so send label text.
|
||||
// Gate on the transcript agent (not `=== 'claude'`) so OpenClaude — which
|
||||
// runs the same selector — takes the keystroke path too.
|
||||
const onSettled =
|
||||
agent === 'claude'
|
||||
? (delivered: boolean): void => {
|
||||
if (!delivered) {
|
||||
return
|
||||
}
|
||||
inferQuestionAnsweredFromCurrentStatus({
|
||||
paneKey,
|
||||
getStatusEntry: () => useAppStore.getState().agentStatusByPaneKey[paneKey],
|
||||
inferQuestionAnswered: (request) =>
|
||||
window.api.agentStatus.inferQuestionAnswered(request).catch((err) => {
|
||||
console.warn('[agent-question] native-chat inference failed:', err)
|
||||
return false
|
||||
})
|
||||
})
|
||||
const onSettled = shouldStepNativeChatAskAnswer(agent)
|
||||
? (delivered: boolean): void => {
|
||||
if (!delivered) {
|
||||
return
|
||||
}
|
||||
: undefined
|
||||
inferQuestionAnsweredFromCurrentStatus({
|
||||
paneKey,
|
||||
getStatusEntry: () => useAppStore.getState().agentStatusByPaneKey[paneKey],
|
||||
inferQuestionAnswered: (request) =>
|
||||
window.api.agentStatus.inferQuestionAnswered(request).catch((err) => {
|
||||
console.warn('[agent-question] native-chat inference failed:', err)
|
||||
return false
|
||||
})
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
const handle: NativeChatSendHandle = shouldStepNativeChatAskAnswer(agent)
|
||||
? sendNativeChatAskAnswer(
|
||||
settings,
|
||||
|
||||
Reference in New Issue
Block a user