mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
test: strengthen RPC cancellation, release rejection, and mobile prompt coverage (#33146)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { resolveCli } from './runtime-serve-terminal-smoke.mjs'
|
||||
import { packagedNodeRuntimePath } from './build-orcad-node.mjs'
|
||||
import { NODE_RUNTIME_ASSETS } from '../../src/shared/node-runtime-pin.ts'
|
||||
import { currentTarget } from './server-build-target.mjs'
|
||||
import { ORCAD_CLI_ENTRY_FILENAME } from '../../src/shared/orcad-artifacts.ts'
|
||||
|
||||
@@ -30,7 +30,13 @@ it('drives Orcad with its packaged CLI and pinned Node even when a desktop CLI e
|
||||
mkdirSync(dirname(bundled), { recursive: true })
|
||||
writeFileSync(bundled, '')
|
||||
expect(resolveCli('orcad', root)).toEqual({
|
||||
command: packagedNodeRuntimePath(directory, currentTarget()),
|
||||
command: join(
|
||||
root,
|
||||
'out',
|
||||
'runtimes',
|
||||
`node-${NODE_RUNTIME_ASSETS[currentTarget()].executableSha256}`,
|
||||
...(process.platform === 'win32' ? ['node.exe'] : ['bin', 'node'])
|
||||
),
|
||||
prefix: [bundled]
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,12 +78,14 @@ describe('antivirus detection report', () => {
|
||||
known: true,
|
||||
scanned: 70,
|
||||
flagged: [
|
||||
{ engine: 'TrendMicro', category: 'malicious', detection: 'Trojan.MSIL.MSILHERACLES' }
|
||||
{ engine: 'TrendMicro', category: 'malicious', detection: 'Trojan.MSIL.MSILHERACLES' },
|
||||
{ engine: 'Microsoft', category: 'suspicious', detection: 'Behavior:Win32/Test' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(report).toContain('1 of 70 engines flag this build')
|
||||
expect(report).toContain('2 of 70 engines flag this build')
|
||||
expect(report).toContain('TrendMicro: Trojan.MSIL.MSILHERACLES')
|
||||
expect(report).toContain('Microsoft: Behavior:Win32/Test')
|
||||
})
|
||||
|
||||
it('flags the release when any single artifact carries a verdict', () => {
|
||||
|
||||
@@ -121,15 +121,19 @@ describe('verify-windows-inner-signature', () => {
|
||||
})
|
||||
|
||||
it('rejects invalid status and unexpected signer subjects', () => {
|
||||
expect(classifySignature({ ...validSignature, status: 'NotSigned' }).message).toMatch(
|
||||
/status is NotSigned/
|
||||
)
|
||||
expect(classifySignature({ ...validSignature, status: 'NotSigned' })).toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringMatching(/status is NotSigned/)
|
||||
})
|
||||
expect(
|
||||
classifySignature({
|
||||
...validSignature,
|
||||
signerSubject: 'CN=SignPath Foundation Test, O=SignPath Foundation, L=Vienna, C=AT'
|
||||
}).message
|
||||
).toMatch(/Unexpected Windows inner executable signer/)
|
||||
})
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringMatching(/Unexpected Windows inner executable signer/)
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts an expected thumbprint as an alternate explicit allowlist', () => {
|
||||
@@ -150,8 +154,11 @@ describe('verify-windows-inner-signature', () => {
|
||||
expectedSigners: [DEFAULT_EXPECTED_SIGNER],
|
||||
expectedThumbprints: ['001122']
|
||||
}
|
||||
).message
|
||||
).toMatch(/Unexpected Windows inner executable signer/)
|
||||
)
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringMatching(/Unexpected Windows inner executable signer/)
|
||||
})
|
||||
expect(
|
||||
classifySignature(validSignature, {
|
||||
expectedSigners: [DEFAULT_EXPECTED_SIGNER],
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('detectAgentPermission', () => {
|
||||
const long = `Do you want to proceed? ${'x'.repeat(300)}`
|
||||
const result = detectAgentPermission({ state: 'waiting', lastAssistantMessage: long })
|
||||
expect(result).not.toBeNull()
|
||||
expect((result?.detail ?? '').length).toBeLessThanOrEqual(160)
|
||||
expect(result?.detail).toBe(`Do you want to proceed? ${'x'.repeat(135)}…`)
|
||||
})
|
||||
|
||||
it('shortens long numbered option labels', () => {
|
||||
@@ -120,7 +120,7 @@ describe('detectAgentPermission', () => {
|
||||
})
|
||||
const second = result?.options[1]
|
||||
expect(second?.send).toBe('2')
|
||||
expect((second?.label ?? '').length).toBeLessThanOrEqual(40)
|
||||
expect(second?.label).toBe('No, and explain in great detail exactly…')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -72,7 +72,20 @@ describe('mobile question rejection', () => {
|
||||
expect(f.cancelPending).toHaveBeenCalledOnce()
|
||||
const sends = f.sendRequest.mock.calls.filter(([method]) => method === 'terminal.send')
|
||||
expect(sends).toHaveLength(1)
|
||||
expect(sends[0]?.[1]).toMatchObject({ terminal: 'terminal', text: '\x1b', enter: false })
|
||||
expect(f.sendRequest).toHaveBeenCalledWith(
|
||||
'terminal.send',
|
||||
{
|
||||
terminal: 'terminal',
|
||||
text: '\x1b',
|
||||
enter: false,
|
||||
requireWriteSettlement: true,
|
||||
client: { id: 'device', type: 'mobile' }
|
||||
},
|
||||
{ timeoutMs: 15_000, budgetSpansConnect: true }
|
||||
)
|
||||
expect(f.cancelPending.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
f.sendRequest.mock.invocationCallOrder[0]!
|
||||
)
|
||||
expect(f.onSendError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { createElement } from 'react'
|
||||
import TestRenderer from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import TestRenderer, { type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
|
||||
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
|
||||
import { buildOmpPlanReviewAnswer } from '../../../src/shared/omp-plan-review-prompt'
|
||||
import { useMobileNativeChatPrompts } from './use-mobile-native-chat-prompts'
|
||||
|
||||
const renderers: ReactTestRenderer[] = []
|
||||
afterEach(() => {
|
||||
TestRenderer.act(() => {
|
||||
for (const renderer of renderers) {
|
||||
renderer.unmount()
|
||||
}
|
||||
})
|
||||
renderers.length = 0
|
||||
})
|
||||
|
||||
const APPROVAL = JSON.stringify({
|
||||
approval: { tool: 'Bash', summary: 'pnpm build > build.log 2>&1' }
|
||||
})
|
||||
@@ -30,7 +40,7 @@ function promptsFor(
|
||||
return null
|
||||
}
|
||||
TestRenderer.act(() => {
|
||||
TestRenderer.create(createElement(Probe))
|
||||
renderers.push(TestRenderer.create(createElement(Probe)))
|
||||
})
|
||||
return captured!
|
||||
}
|
||||
@@ -216,37 +226,47 @@ describe('useMobileNativeChatPrompts ask state gate', () => {
|
||||
})
|
||||
|
||||
describe('useMobileNativeChatPrompts in a structured chat', () => {
|
||||
it('shows a Codex async question and answers it with an ordinary send', async () => {
|
||||
const sendWithOutcome = vi.fn(async () => 'accepted' as const)
|
||||
const questions = [{ title: 'Which color?', options: ['Red', 'Blue'] }]
|
||||
const message: NativeChatMessage = {
|
||||
id: 'ask',
|
||||
role: 'assistant',
|
||||
blocks: [
|
||||
{ type: 'text', text: 'Which color?\n- Red\n- Blue' },
|
||||
{ type: 'tool-call', name: 'request_user_input_async', input: { questions } }
|
||||
],
|
||||
timestamp: null,
|
||||
source: 'transcript'
|
||||
}
|
||||
let captured: ReturnType<typeof useMobileNativeChatPrompts> | undefined
|
||||
function Probe(): null {
|
||||
captured = useMobileNativeChatPrompts({
|
||||
enabled: true,
|
||||
structured: { permission: null, question: null, sendWithOutcome },
|
||||
status: null,
|
||||
messages: [message],
|
||||
transcriptLoading: false
|
||||
it.each([
|
||||
['accepted', true],
|
||||
['queued', true],
|
||||
['unknown', true],
|
||||
['rejected', false]
|
||||
] as const)(
|
||||
'answers a Codex async question through an ordinary %s send',
|
||||
async (outcome, accepted) => {
|
||||
const sendWithOutcome = vi.fn(async () => outcome)
|
||||
const questions = [{ title: 'Which color?', options: ['Red', 'Blue'] }]
|
||||
const message: NativeChatMessage = {
|
||||
id: 'ask',
|
||||
role: 'assistant',
|
||||
blocks: [
|
||||
{ type: 'text', text: 'Which color?\n- Red\n- Blue' },
|
||||
{ type: 'tool-call', name: 'request_user_input_async', input: { questions } }
|
||||
],
|
||||
timestamp: null,
|
||||
source: 'transcript'
|
||||
}
|
||||
let captured: ReturnType<typeof useMobileNativeChatPrompts> | undefined
|
||||
function Probe(): null {
|
||||
captured = useMobileNativeChatPrompts({
|
||||
enabled: true,
|
||||
structured: { permission: null, question: null, sendWithOutcome },
|
||||
status: null,
|
||||
messages: [message],
|
||||
transcriptLoading: false
|
||||
})
|
||||
return null
|
||||
}
|
||||
TestRenderer.act(() => {
|
||||
renderers.push(TestRenderer.create(createElement(Probe)))
|
||||
})
|
||||
return null
|
||||
}
|
||||
TestRenderer.act(() => {
|
||||
TestRenderer.create(createElement(Probe))
|
||||
})
|
||||
|
||||
const ask = captured?.ask
|
||||
expect(ask?.questions[0]?.question).toBe('Which color?')
|
||||
await expect(ask && captured?.structuredAnswer?.(ask, [{ indices: [1] }])).resolves.toBe(true)
|
||||
expect(sendWithOutcome).toHaveBeenCalledWith('> Which color?\n\nBlue')
|
||||
})
|
||||
const ask = captured?.ask
|
||||
expect(ask?.questions[0]?.question).toBe('Which color?')
|
||||
await expect(ask && captured?.structuredAnswer?.(ask, [{ indices: [1] }])).resolves.toBe(
|
||||
accepted
|
||||
)
|
||||
expect(sendWithOutcome).toHaveBeenCalledWith('> Which color?\n\nBlue')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ it('cancels setup using the preselected id and unsubscribes a late handle', asyn
|
||||
|
||||
it('keeps the response deadline while setup is pending', async () => {
|
||||
vi.useFakeTimers()
|
||||
const subscribe = vi.fn(() => new Promise(() => {}))
|
||||
const subscribe = vi.fn((_args: { subscriptionId?: string }) => new Promise(() => {}))
|
||||
const cancelSubscription = vi.fn().mockResolvedValue(undefined)
|
||||
stubRuntimeEnvironmentBridge({ subscribe, cancelSubscription })
|
||||
const pending = callAbortableRuntimeEnvironment(
|
||||
@@ -50,7 +50,12 @@ it('keeps the response deadline while setup is pending', async () => {
|
||||
new AbortController().signal
|
||||
)
|
||||
const rejection = expect(pending).rejects.toThrow('timed out')
|
||||
await vi.advanceTimersByTimeAsync(15_000)
|
||||
await vi.advanceTimersByTimeAsync(14_999)
|
||||
expect(cancelSubscription).not.toHaveBeenCalled()
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await rejection
|
||||
expect(cancelSubscription).toHaveBeenCalledOnce()
|
||||
expect(cancelSubscription).toHaveBeenCalledWith({
|
||||
subscriptionId: subscribe.mock.calls[0]?.[0].subscriptionId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,15 +21,26 @@ beforeEach(() => {
|
||||
describe('callHostRoute', () => {
|
||||
it('calls the route target with the pairing revision captured before the call', async () => {
|
||||
callRuntimeRpc.mockResolvedValue({ ok: true })
|
||||
const signal = new AbortController().signal
|
||||
|
||||
await expect(
|
||||
callHostRoute(route, 'folderWorkspace.update', { id: 'f' }, { timeoutMs: 5 })
|
||||
callHostRoute(
|
||||
route,
|
||||
'folderWorkspace.update',
|
||||
{ id: 'f' },
|
||||
{ timeoutMs: 5, signal, expectedEnvironmentRuntimeId: 'runtime-a' }
|
||||
)
|
||||
).resolves.toEqual({ ok: true })
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
route.target,
|
||||
'folderWorkspace.update',
|
||||
{ id: 'f' },
|
||||
{ timeoutMs: 5, expectedEnvironmentPairingRevision: 7 }
|
||||
{
|
||||
timeoutMs: 5,
|
||||
signal,
|
||||
expectedEnvironmentRuntimeId: 'runtime-a',
|
||||
expectedEnvironmentPairingRevision: 7
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ it('captures the pairing revision before awaiting the compatibility probe', asyn
|
||||
})
|
||||
|
||||
await expect(request).resolves.toEqual({ ok: true })
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledTimes(2)
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(1, {
|
||||
selector: 'env-cas',
|
||||
method: 'status.get',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { brandEphemeralSetupTerminalWorktreeId } from '../../../shared/ephemeral
|
||||
|
||||
describe('toRuntimeWorktreeSelector', () => {
|
||||
it('addresses raw worktree IDs as runtime ID selectors', () => {
|
||||
expect(toRuntimeWorktreeSelector('wt-1')).toBe('id:wt-1')
|
||||
expect(toRuntimeWorktreeSelector(' wt-1 ')).toBe('id:wt-1')
|
||||
expect(toRuntimeWorktreeSelector('repo-1::C:/Users/me/orca/workspaces/orca/new-worktree')).toBe(
|
||||
'id:repo-1::C:/Users/me/orca/workspaces/orca/new-worktree'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user