mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test(remote-runtime): pin the 12 hand-kept error-code lists to their callers' policy (#30246)
Each list that decides delivered / undelivered / recoverable / definitive by code now has a test naming its codes, the message fragments it relies on, and nearby codes it must not treat the same. Includes the #30146 shape: a client-minted runtime_manually_disconnected keeps a user's close for replay. Tests only; no production change.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY } from '../../shared/jira-agent-capability'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
import { JIRA_WRITE_HANDLERS } from './jira-write'
|
||||
|
||||
function clientFailingWrite(failure: Error) {
|
||||
return vi.fn(async (method: string) => {
|
||||
if (method === 'status.get') {
|
||||
return { result: { capabilities: [JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY] } }
|
||||
}
|
||||
if (method === 'jira.status') {
|
||||
return { result: { sites: [{ id: 'site-1' }], selectedSiteId: 'site-1' } }
|
||||
}
|
||||
throw failure
|
||||
})
|
||||
}
|
||||
|
||||
async function clearAssigneeAfter(failure: Error): Promise<unknown> {
|
||||
const handler = JIRA_WRITE_HANDLERS['jira assignee clear']
|
||||
try {
|
||||
await handler({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler only calls client.call.
|
||||
client: { call: clientFailingWrite(failure) } as never,
|
||||
cwd: '/tmp',
|
||||
flags: new Map([['key', 'ORCA-1']]),
|
||||
json: false
|
||||
})
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
throw new Error('expected the write to fail')
|
||||
}
|
||||
|
||||
// Pins which write failures say "may have been applied" (round-2 S5), so a dropped code is deliberate.
|
||||
describe('Jira write outcome-unknown codes', () => {
|
||||
it.each([
|
||||
'runtime_timeout',
|
||||
'runtime_connection_failed',
|
||||
'runtime_disconnected',
|
||||
'runtime_unavailable'
|
||||
])('reports %s as an unverified write', async (code) => {
|
||||
await expect(
|
||||
clearAssigneeAfter(new RuntimeClientError(code, 'failed.'))
|
||||
).resolves.toMatchObject({ code: 'jira_write_unverified' })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_source_mismatch',
|
||||
'invalid_argument'
|
||||
])('passes %s through unchanged', async (code) => {
|
||||
const failure = new RuntimeClientError(code, 'failed.')
|
||||
await expect(clearAssigneeAfter(failure)).resolves.toBe(failure)
|
||||
})
|
||||
|
||||
it('passes a non-client error through unchanged', async () => {
|
||||
const failure = new Error('runtime_timeout')
|
||||
await expect(clearAssigneeAfter(failure)).resolves.toBe(failure)
|
||||
})
|
||||
})
|
||||
@@ -314,4 +314,19 @@ describe('orchestration mutation recovery', () => {
|
||||
expect(result.data).not.toHaveProperty('originalCommand')
|
||||
expect((result.data as { recovery: object }).recovery).not.toHaveProperty('retryCommand')
|
||||
})
|
||||
|
||||
// Pins the outcome-unknown codes (round-2 S5): only these get recovery guidance.
|
||||
it.each([
|
||||
['runtime_unavailable', true],
|
||||
['remote_runtime_unavailable', true],
|
||||
['runtime_timeout', true],
|
||||
['invalid_runtime_response', true],
|
||||
['runtime_manually_disconnected', false],
|
||||
['remote_runtime_timeout', false],
|
||||
['runtime_rpc_queue_overloaded', false],
|
||||
['invalid_argument', false]
|
||||
])('treats %s as an unknown mutation outcome: %s', (code, unknown) => {
|
||||
const error = new RuntimeClientError(code, 'failed', { orchestrationRequestId: 'request-1' })
|
||||
expect(orchestrationMutationRecoveryError(error) !== error).toBe(unknown)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from '../../../../shared/remote-runtime-client-error'
|
||||
import { classifyAutomationHostQueryError } from './automation-host-cache-error'
|
||||
|
||||
function classify(error: unknown): { code: string; retryable: boolean } {
|
||||
const { code, retryable } = classifyAutomationHostQueryError(error, { attempt: 1, now: 0 })
|
||||
return { code, retryable }
|
||||
}
|
||||
|
||||
// Pins the RPC-code mapping of host query failures (round-2 S5), so a dropped code is deliberate.
|
||||
describe('classifyAutomationHostQueryError code mapping', () => {
|
||||
it.each([
|
||||
['runtime_unavailable', 'authority_unavailable', true],
|
||||
['remote_runtime_unavailable', 'authority_unavailable', true],
|
||||
['relay_quota_exceeded', 'authority_unavailable', true],
|
||||
['timeout', 'timeout', true],
|
||||
['runtime_timeout', 'timeout', true],
|
||||
['permission_denied', 'permission_denied', false],
|
||||
['unauthorized', 'permission_denied', false],
|
||||
['forbidden', 'permission_denied', false],
|
||||
['capability_unsupported', 'incompatible', false],
|
||||
['legacy_read_only', 'incompatible', false],
|
||||
['invalid_runtime_response', 'invalid_response', false],
|
||||
['invalid_argument', 'invalid_response', false],
|
||||
['runtime_manually_disconnected', 'unknown', true],
|
||||
['runtime_rpc_queue_overloaded', 'unknown', true],
|
||||
['remote_runtime_busy', 'unknown', true]
|
||||
])('maps %s to %s (retryable: %s)', (rpcCode, code, retryable) => {
|
||||
expect(classify(new RemoteRuntimeClientError(rpcCode, 'failed'))).toEqual({ code, retryable })
|
||||
})
|
||||
|
||||
it('reads a code-stripped message token', () => {
|
||||
expect(
|
||||
classify(new Error("Error invoking remote method 'x': Error: failed: runtime_timeout"))
|
||||
).toEqual({ code: 'timeout', retryable: true })
|
||||
})
|
||||
})
|
||||
@@ -524,4 +524,37 @@ describe('structured agent session launch', () => {
|
||||
|
||||
expect(transportError).not.toBeInstanceOf(StructuredAgentSessionCreateRefusalError)
|
||||
})
|
||||
|
||||
// Pins the thrown-error half (round-2 S5): a host that flattened the refusal to runtime_error
|
||||
// still ends it with the token, and client-minted codes never prove nothing was created.
|
||||
it('reads a definitive token from a flattened runtime_error message', async () => {
|
||||
mockSupportedCreate(() => {
|
||||
throw Object.assign(new Error('structured_agent_session_unsupported'), {
|
||||
code: 'runtime_error'
|
||||
})
|
||||
})
|
||||
const error = await launchStructuredAgentSession(
|
||||
createStructuredAgentSessionLaunchIntent('workspace-flattened-refusal', 'codex')
|
||||
).catch((caught: unknown) => caught)
|
||||
|
||||
expect(error).toBeInstanceOf(StructuredAgentSessionCreateRefusalError)
|
||||
expect(error).toMatchObject({ code: 'structured_agent_session_unsupported' })
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'runtime_unavailable',
|
||||
'remote_runtime_unavailable',
|
||||
'method_not_supported'
|
||||
])('keeps a thrown %s outcome unknown', async (code) => {
|
||||
mockSupportedCreate(() => {
|
||||
throw Object.assign(new Error('failed'), { code })
|
||||
})
|
||||
const error = await launchStructuredAgentSession(
|
||||
createStructuredAgentSessionLaunchIntent(`workspace-client-${code}`, 'codex')
|
||||
).catch((caught: unknown) => caught)
|
||||
|
||||
expect(error).not.toBeInstanceOf(StructuredAgentSessionCreateRefusalError)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from '../../../shared/remote-runtime-client-error'
|
||||
import type { WebRuntimeBrowserCreationContext } from './web-runtime-browser-creation-context'
|
||||
import { prepareWebRuntimeBrowserCreationFailure } from './web-runtime-browser-creation-failure'
|
||||
|
||||
vi.mock('../store', () => ({ useAppStore: { getState: vi.fn(), setState: vi.fn() } }))
|
||||
|
||||
function failedCreateContext(): WebRuntimeBrowserCreationContext {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the failure path reads only worktreeId and the group fields, which are absent here.
|
||||
args: { worktreeId: 'repo::/w' } as never,
|
||||
environmentId: 'env-1',
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the prepare step never reads the intent owner.
|
||||
intentOwner: {} as never,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the prepare step never calls the environment.
|
||||
callEnvironment: vi.fn() as never,
|
||||
shouldFocusOnCreate: false,
|
||||
shouldSelectWorktree: false,
|
||||
provisionalPageId: 'provisional-page',
|
||||
hostSupportsKnownPageId: true,
|
||||
expectsClientHosting: false,
|
||||
unsubscribeFocusGuard: vi.fn(),
|
||||
guardedPageId: 'provisional-page',
|
||||
createdPageId: null,
|
||||
createAttempted: true,
|
||||
staged: null
|
||||
}
|
||||
}
|
||||
|
||||
function prepare(error: unknown): { cleanupPageId: string | null; createOutcomeUnknown: boolean } {
|
||||
const { cleanupPageId, createOutcomeUnknown } = prepareWebRuntimeBrowserCreationFailure(
|
||||
failedCreateContext(),
|
||||
error
|
||||
)
|
||||
return { cleanupPageId, createOutcomeUnknown }
|
||||
}
|
||||
|
||||
// Pins which create failures prove no page exists (round-2 S5), so a dropped code is deliberate.
|
||||
describe('prepareWebRuntimeBrowserCreationFailure definitive codes', () => {
|
||||
it.each([
|
||||
'browser_error',
|
||||
'capability_unsupported',
|
||||
'invalid_argument',
|
||||
'invalid_params',
|
||||
'method_not_found',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'selector_ambiguous',
|
||||
'selector_not_found',
|
||||
'unauthorized'
|
||||
])('skips the provisional-page cleanup after %s', (code) => {
|
||||
expect(prepare(new RemoteRuntimeClientError(code, 'failed'))).toEqual({
|
||||
cleanupPageId: null,
|
||||
createOutcomeUnknown: false
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_unavailable',
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_timeout',
|
||||
'remote_runtime_busy',
|
||||
'runtime_error'
|
||||
])('cleans up the provisional page after %s', (code) => {
|
||||
expect(prepare(new RemoteRuntimeClientError(code, 'failed'))).toEqual({
|
||||
cleanupPageId: 'provisional-page',
|
||||
createOutcomeUnknown: false
|
||||
})
|
||||
})
|
||||
|
||||
it('reads a definitive code from a code-stripped message token', () => {
|
||||
expect(
|
||||
prepare(new Error("Error invoking remote method 'x': Error: failed: method_not_found"))
|
||||
).toEqual({
|
||||
cleanupPageId: null,
|
||||
createOutcomeUnknown: false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,185 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as TrackingDecisions from './web-session-tabs-sync/tracking-decisions'
|
||||
import { closeWebRuntimeSessionTab } from './web-runtime-session-tab-lifecycle'
|
||||
import {
|
||||
resetWebSessionCloseReplaysForTests,
|
||||
takeDueWebSessionCloseReplays
|
||||
} from './web-session-close-replay'
|
||||
import { resetWebSessionFocusIntentForTests } from './web-session-focus-intent'
|
||||
import {
|
||||
isWebSessionCloseIntentPending,
|
||||
resetWebSessionCloseIntentForTests
|
||||
} from './web-session-close-intent'
|
||||
import { ENVIRONMENT_ID, WORKTREE_ID } from './web-runtime-session-test-harness'
|
||||
import { stubRuntimeEnvironmentBridge } from './runtime-environment-bridge-fixture'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
setActiveWorktree: vi.fn(),
|
||||
createBrowserTab: vi.fn(),
|
||||
closeEmptyGroup: vi.fn(),
|
||||
moveUnifiedTabToGroup: vi.fn(),
|
||||
setRemoteBrowserPageHandle: vi.fn(),
|
||||
focusBrowserTabInWorktree: vi.fn(),
|
||||
applyWebSessionTabsSnapshot: vi.fn(),
|
||||
decideWebSessionTabsSnapshot: vi.fn(() => ({ apply: true, settlesHostMirror: true })),
|
||||
getWebSessionTabsTrackingGeneration: vi.fn(() => 0),
|
||||
acceptReplayedWebSessionTabsSnapshot: vi.fn(),
|
||||
resolveHostSessionTabIdForWebSessionTab: vi.fn(),
|
||||
trackTerminalPaneSplit: vi.fn(),
|
||||
deliverLaunchPromptToAgentTab: vi.fn(),
|
||||
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
|
||||
getRuntimeEnvironmentIdForWorktree: vi.fn(),
|
||||
hasMaterializedWebRuntimeBrowserPage: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../store', () => ({
|
||||
useAppStore: {
|
||||
getState: mocks.getState,
|
||||
setState: mocks.setState,
|
||||
subscribe: mocks.subscribe
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./web-session-tabs-sync/tracking-lifecycle', () => ({
|
||||
acceptReplayedWebSessionTabsSnapshot: mocks.acceptReplayedWebSessionTabsSnapshot,
|
||||
getWebSessionTabsTrackingGeneration: mocks.getWebSessionTabsTrackingGeneration
|
||||
}))
|
||||
vi.mock('./web-session-tabs-sync/snapshot-api', () => ({
|
||||
applyWebSessionTabsSnapshot: mocks.applyWebSessionTabsSnapshot
|
||||
}))
|
||||
vi.mock('./web-session-tabs-sync/tracking-decisions', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof TrackingDecisions>()),
|
||||
decideWebSessionTabsSnapshot: mocks.decideWebSessionTabsSnapshot
|
||||
}))
|
||||
vi.mock('./web-session-tabs-sync/store-patch', () => ({
|
||||
applyWebSessionTabsStorePatch: (buildPatch: (state: unknown) => unknown) => {
|
||||
mocks.setState(buildPatch)
|
||||
// The production caller invokes the returned settle receipt.
|
||||
return () => {}
|
||||
}
|
||||
}))
|
||||
vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({
|
||||
resolveHostSessionTabIdForWebSessionTab: mocks.resolveHostSessionTabIdForWebSessionTab
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/feature-education-telemetry', () => ({
|
||||
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-runtime-owner', () => ({
|
||||
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
|
||||
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
|
||||
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
|
||||
}))
|
||||
|
||||
vi.mock('./web-runtime-browser-materialization', () => ({
|
||||
hasMaterializedWebRuntimeBrowserPage: mocks.hasMaterializedWebRuntimeBrowserPage
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
resetWebSessionCloseIntentForTests()
|
||||
resetWebSessionCloseReplaysForTests()
|
||||
})
|
||||
|
||||
const OWNER = { environmentId: ENVIRONMENT_ID }
|
||||
|
||||
async function closeAfter(failure: unknown, rejects = false): Promise<boolean> {
|
||||
const runtimeCall = vi.fn()
|
||||
if (rejects) {
|
||||
runtimeCall.mockRejectedValueOnce(failure)
|
||||
} else {
|
||||
runtimeCall.mockResolvedValueOnce(failure)
|
||||
}
|
||||
runtimeCall.mockResolvedValue({ id: 'close', ok: true, result: { closed: true } })
|
||||
stubRuntimeEnvironmentBridge({ call: runtimeCall })
|
||||
await expect(
|
||||
closeWebRuntimeSessionTab({
|
||||
worktreeId: WORKTREE_ID,
|
||||
tabId: 'local-browser-unified',
|
||||
reason: 'user'
|
||||
})
|
||||
).resolves.toBe('failed')
|
||||
const held = isWebSessionCloseIntentPending(
|
||||
OWNER,
|
||||
WORKTREE_ID,
|
||||
'host-browser-unified',
|
||||
Date.now() + 60_000
|
||||
)
|
||||
const replayQueued =
|
||||
takeDueWebSessionCloseReplays(OWNER, WORKTREE_ID, new Set(['host-browser-unified'])).length > 0
|
||||
expect(replayQueued).toBe(held)
|
||||
return held
|
||||
}
|
||||
|
||||
function envelope(code: string): unknown {
|
||||
return { id: 'close', ok: false, error: { code, message: 'failed' } }
|
||||
}
|
||||
|
||||
// Pins which close failures keep the user's close for replay (round-2 S5), so a dropped code is deliberate.
|
||||
describe('user close: which failures keep the intent', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
|
||||
mocks.getState.mockReturnValue({
|
||||
settings: {
|
||||
activeRuntimeEnvironmentId: ENVIRONMENT_ID
|
||||
},
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
activeTabIdByWorktree: {},
|
||||
activeGroupIdByWorktree: {},
|
||||
setActiveWorktree: mocks.setActiveWorktree
|
||||
})
|
||||
mocks.resolveHostSessionTabIdForWebSessionTab.mockImplementation(
|
||||
(_state, args: { tabId: string }) =>
|
||||
args.tabId === 'local-browser-unified' ? 'host-browser-unified' : null
|
||||
)
|
||||
// Store patches must run their updater so snapshot reconciliation is observable.
|
||||
mocks.setState.mockImplementation((updater: (state: unknown) => unknown) => {
|
||||
updater({
|
||||
state: 'before',
|
||||
activeWorktreeId: WORKTREE_ID
|
||||
})
|
||||
})
|
||||
mocks.applyWebSessionTabsSnapshot.mockReturnValue({ state: 'after' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetWebSessionFocusIntentForTests()
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_unavailable',
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_timeout',
|
||||
'remote_runtime_timeout'
|
||||
])('keeps the close when the answer is %s', async (code) => {
|
||||
expect(await closeAfter(envelope(code))).toBe(true)
|
||||
})
|
||||
|
||||
// #30146: the web client mints this as a bare Error; it must never drop the close.
|
||||
it('keeps the close when the client itself refuses with runtime_manually_disconnected', async () => {
|
||||
expect(await closeAfter(new Error('runtime_manually_disconnected'), true)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the close on any thrown transport failure', async () => {
|
||||
expect(await closeAfter(new Error('socket hang up'), true)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'invalid_argument',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'ssh_user_disconnected',
|
||||
'conflict'
|
||||
])('drops the close when the answer is %s', async (code) => {
|
||||
expect(await closeAfter(envelope(code))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isStableAdoptionFailure } from './web-session-terminal-orphan-recovery-adoption'
|
||||
|
||||
function failure(code: string, message = 'refused'): unknown {
|
||||
return { id: 'adopt', ok: false, error: { code, message } }
|
||||
}
|
||||
|
||||
// Pins which adoption failures stop retrying (round-2 S5), so a dropped code is deliberate.
|
||||
describe('isStableAdoptionFailure', () => {
|
||||
it.each(['method_not_found', 'capability_unsupported', 'invalid_runtime_response'])(
|
||||
'treats %s as stable',
|
||||
(code) => {
|
||||
expect(isStableAdoptionFailure(failure(code))).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
'remote_runtime_unavailable',
|
||||
'remote_runtime_busy',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'runtime_timeout',
|
||||
'runtime_unavailable',
|
||||
'reconnecting',
|
||||
'timeout',
|
||||
'runtime_manually_disconnected',
|
||||
'terminal_not_found'
|
||||
])('keeps retrying after %s', (code) => {
|
||||
expect(isStableAdoptionFailure(failure(code))).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps retrying a code-stripped connection failure even if it names a stable code', () => {
|
||||
expect(
|
||||
isStableAdoptionFailure(
|
||||
new Error('Could not connect to the remote Orca runtime: method_not_found')
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('reads a stable code from a thrown error message token', () => {
|
||||
expect(isStableAdoptionFailure(new Error('Adoption failed: capability_unsupported'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from '../../../shared/remote-runtime-client-error'
|
||||
import { isWebHostUnreachableAtStartup } from './startup-web-host-unreachable'
|
||||
|
||||
vi.mock('sonner', () => ({ toast: vi.fn() }))
|
||||
|
||||
function stubWindow(webClient: boolean, snapshots: unknown[]): void {
|
||||
vi.stubGlobal('window', {
|
||||
__ORCA_WEB_CLIENT__: webClient,
|
||||
api: { runtimeEnvironments: { getStatusSnapshots: vi.fn(async () => snapshots) } }
|
||||
})
|
||||
}
|
||||
|
||||
// Pins which boot failures read as "host unreachable" (round-2 S5), so a dropped code is deliberate.
|
||||
describe('isWebHostUnreachableAtStartup', () => {
|
||||
beforeEach(() => {
|
||||
stubWindow(true, [])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'remote_runtime_unavailable',
|
||||
'remote_runtime_busy',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'runtime_timeout',
|
||||
'runtime_unavailable',
|
||||
'reconnecting',
|
||||
'timeout'
|
||||
])('treats %s as unreachable while the transport is down', async (code) => {
|
||||
await expect(
|
||||
isWebHostUnreachableAtStartup(new RemoteRuntimeClientError(code, 'failed'))
|
||||
).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'Timed out while connecting to the remote Orca runtime.',
|
||||
'Remote Orca runtime connection interrupted.',
|
||||
'Could not connect to the remote Orca runtime.'
|
||||
])('treats the code-less message "%s" as unreachable', async (message) => {
|
||||
await expect(isWebHostUnreachableAtStartup(new Error(message))).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it.each(['runtime_manually_disconnected', 'invalid_runtime_response', 'unauthorized'])(
|
||||
'treats %s as a hydration fault',
|
||||
async (code) => {
|
||||
await expect(
|
||||
isWebHostUnreachableAtStartup(new RemoteRuntimeClientError(code, 'failed'))
|
||||
).resolves.toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it('treats a connection failure on a ready transport as a hydration fault', async () => {
|
||||
stubWindow(true, [{ transport: 'ready', retired: false }])
|
||||
await expect(
|
||||
isWebHostUnreachableAtStartup(new RemoteRuntimeClientError('runtime_timeout', 'failed'))
|
||||
).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('is never true outside the web client', async () => {
|
||||
stubWindow(false, [])
|
||||
await expect(
|
||||
isWebHostUnreachableAtStartup(new RemoteRuntimeClientError('runtime_timeout', 'failed'))
|
||||
).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RemoteRuntimeClientError } from '../../../../../../shared/remote-runtime-client-error'
|
||||
import { RuntimeRpcCallQueueOverloadError } from '../../../../../../shared/runtime-rpc-call-queue'
|
||||
import { isWorktreeRemovalReplyLost } from './host-worktree-removal-state'
|
||||
|
||||
// Pins which delete failures wait on the host's listing (round-2 S5), so a dropped code is deliberate.
|
||||
describe('isWorktreeRemovalReplyLost', () => {
|
||||
it.each([
|
||||
'remote_runtime_unavailable',
|
||||
'remote_runtime_busy',
|
||||
'runtime_timeout',
|
||||
'runtime_unavailable',
|
||||
'reconnecting',
|
||||
'timeout'
|
||||
])('treats %s as a lost reply', (code) => {
|
||||
expect(isWorktreeRemovalReplyLost(new RemoteRuntimeClientError(code, 'transport failed'))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('treats a queue overload as never sent', () => {
|
||||
expect(isWorktreeRemovalReplyLost(new RuntimeRpcCallQueueOverloadError('global'))).toBe(false)
|
||||
expect(
|
||||
isWorktreeRemovalReplyLost(
|
||||
new Error(
|
||||
"Error invoking remote method 'x': Error: Remote runtime call queue is full; retry after current calls finish."
|
||||
)
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'invalid_runtime_response',
|
||||
'unauthorized',
|
||||
'worktree_not_found'
|
||||
])('does not treat %s as a lost reply', (code) => {
|
||||
expect(isWorktreeRemovalReplyLost(new RemoteRuntimeClientError(code, 'refused'))).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
"Error invoking remote method 'x': Error: Request timed out.",
|
||||
"Error invoking remote method 'x': Error: Remote Orca runtime connection interrupted.",
|
||||
"Error invoking remote method 'x': Error: Could not connect to the remote Orca runtime."
|
||||
])('treats the code-stripped message "%s" as a lost reply', (message) => {
|
||||
expect(isWorktreeRemovalReplyLost(new Error(message))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -38,4 +38,19 @@ describe('definitive agent-session create refusals', () => {
|
||||
)
|
||||
expect(isDefinitiveAgentSessionCreateRefusal('structured_agent_session_unsupported')).toBe(true)
|
||||
})
|
||||
|
||||
// Pins the RPC half (round-2 S5): transport and client-minted codes never allow a fallback.
|
||||
it.each([
|
||||
'runtime_unavailable',
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_timeout',
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'method_not_supported',
|
||||
'invalid_argument',
|
||||
null,
|
||||
undefined
|
||||
])('does not count %s as definitive', (code) => {
|
||||
expect(isDefinitiveAgentSessionCreateRefusal(code)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
RECOVERABLE_CODES,
|
||||
RECOVERABLE_MESSAGE_FRAGMENTS,
|
||||
isRecoverableRemoteRuntimeConnectionError,
|
||||
isRuntimeRpcQueueOverloadError,
|
||||
toRemoteRuntimeClientErrorLike
|
||||
@@ -61,4 +63,54 @@ describe('remote runtime client error classification', () => {
|
||||
const error = toRemoteRuntimeClientErrorLike(new Error(message))
|
||||
expect(isRecoverableRemoteRuntimeConnectionError(error)).toBe(true)
|
||||
})
|
||||
|
||||
// Pins the list so a dropped or added code is a deliberate, reviewed change (round-2 S5).
|
||||
it('pins the recoverable codes and message fragments', () => {
|
||||
expect([...RECOVERABLE_CODES].toSorted()).toEqual([
|
||||
'reconnecting',
|
||||
'remote_runtime_busy',
|
||||
'remote_runtime_unavailable',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'runtime_timeout',
|
||||
'runtime_unavailable',
|
||||
'timeout'
|
||||
])
|
||||
expect(RECOVERABLE_MESSAGE_FRAGMENTS).toEqual([
|
||||
'could not connect to the remote orca runtime',
|
||||
'remote orca runtime closed the connection',
|
||||
'remote orca runtime connection closed',
|
||||
'remote orca runtime is not connected',
|
||||
'remote runtime call queue is full',
|
||||
'remote runtime connection closed',
|
||||
'remote runtime connection wait limit reached',
|
||||
'remote runtime request limit reached',
|
||||
'remote runtime subscription limit reached',
|
||||
'remote runtime subscription memory limit reached',
|
||||
'remote runtime subscription closed before it started',
|
||||
'remote terminal stream is not connected',
|
||||
'timed out waiting for the remote orca runtime'
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'runtime_manually_disconnected',
|
||||
'ssh_user_disconnected',
|
||||
'remote_runtime_timeout',
|
||||
'runtime_busy'
|
||||
])('does not treat client code %s as recoverable', (code) => {
|
||||
expect(isRecoverableRemoteRuntimeConnectionError({ code, message: 'refused' })).toBe(false)
|
||||
})
|
||||
|
||||
it.each(RECOVERABLE_MESSAGE_FRAGMENTS)(
|
||||
'recovers a code-stripped message containing %s',
|
||||
(fragment) => {
|
||||
expect(
|
||||
isRecoverableRemoteRuntimeConnectionError(
|
||||
toRemoteRuntimeClientErrorLike(
|
||||
new Error(`Error invoking remote method 'x': Error: ${fragment}.`)
|
||||
)
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -98,6 +98,15 @@ describe('structuredAgentSessionSendEvidence', () => {
|
||||
]) {
|
||||
expect(structuredAgentSessionSendEvidence(thrown(rpcCode)).kind).toBe('uncertain')
|
||||
}
|
||||
for (const rpcCode of [
|
||||
'runtime_manually_disconnected',
|
||||
'runtime_rpc_queue_overloaded',
|
||||
'remote_runtime_busy',
|
||||
'forbidden',
|
||||
'capability_unsupported'
|
||||
]) {
|
||||
expect(structuredAgentSessionSendEvidence(thrown(rpcCode)).kind).toBe('uncertain')
|
||||
}
|
||||
const thrownRefusal = {
|
||||
response: { error: { data: { refusal: { code: 'agent_session_journal_unreadable' } } } }
|
||||
}
|
||||
@@ -110,8 +119,14 @@ describe('structuredAgentSessionSendEvidence', () => {
|
||||
).toBe('uncertain')
|
||||
})
|
||||
|
||||
// Pins the full turned-away list (round-2 S5): every other code, client-minted ones included, is uncertain.
|
||||
it('treats a call the host turned away before running it as never written', () => {
|
||||
for (const rpcCode of ['method_not_found', 'invalid_argument', 'unauthorized']) {
|
||||
for (const rpcCode of [
|
||||
'method_not_found',
|
||||
'method_not_supported',
|
||||
'invalid_argument',
|
||||
'unauthorized'
|
||||
]) {
|
||||
expect(structuredAgentSessionSendEvidence(thrown(rpcCode)).kind).toBe('not-recorded')
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user