mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(native-chat): say why Claude refused to start a chat, including Full access as root (#33812)
* fix(native-chat): say why Claude refused to start a chat, including Full access as root Claude Code exits before initialize when Full access (--dangerously-skip-permissions) meets a root uid, which is common on SSH hosts. Orca reported only that Claude stopped before it finished starting, and retried four times. Classify that refusal as fullAccessAsRoot with a next step, and show the CLI's own startup words behind Details on the failure row and on the unsent message. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep message Details to start failures; cover the exit path Route the exit-error module and delivery notices to the managed e2e, explain its timeout, and drop the exec non-null assertion. Match the zh Full access label. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the refused fixture so the web typecheck passes Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,18 @@ export const PR_E2E_SOURCE_ROUTES = [
|
||||
file
|
||||
))
|
||||
},
|
||||
{
|
||||
// A Claude chat a managed host's CLI refuses to start names why (#33768).
|
||||
id: 'ssh.orcad-claude-chat-start-failure',
|
||||
specs: ['tests/e2e/ssh-orcad-claude-chat-start-failure.spec.ts'],
|
||||
matches: (file) =>
|
||||
MANAGED_SSH_HARNESS.test(file) ||
|
||||
file === 'tests/e2e/fixtures/structured-claude-stub.cjs' ||
|
||||
(isProductSource(file) &&
|
||||
/^src\/(?:main\/(?:claude\/claude-(?:stream-json-connection|stream-json-exit-error|structured-session-exit-lifecycle)|native-chat\/agent-session-wire\/structured-agent-session-failure-text)|renderer\/src\/components\/native-chat\/structured-agent-session-delivery-notices)\.ts$/.test(
|
||||
file
|
||||
))
|
||||
},
|
||||
{
|
||||
// A reconnect the host cannot image must still bring an idle full-screen program up to date (#26887).
|
||||
id: 'ssh.orcad-reconnect-idle-frame',
|
||||
|
||||
@@ -22,6 +22,7 @@ import { openTestJournalHostDatabase } from '../native-chat/agent-session-journa
|
||||
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
|
||||
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
|
||||
import { providerExecutableMissing } from '../provider-process/provider-executable-missing'
|
||||
import { providerStartupFailureFact } from '../native-chat/agent-session-wire/structured-agent-session-failure-text'
|
||||
|
||||
// Exercise the real SDK against a scripted child CLI.
|
||||
const FAKE_CLI = join(__dirname, '__fixtures__', 'claude-agent-sdk-scripted-cli.mjs')
|
||||
@@ -609,6 +610,24 @@ describe('Claude stream-json connection', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("names Claude's root refusal of Full access, keeping its words as the detail", async () => {
|
||||
const refusal =
|
||||
'--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons'
|
||||
const scenario = scriptScenario([{ stderr: `${refusal}\n` }, { exit: 1 }])
|
||||
let exit: Error | null = null
|
||||
await open(launchFor(scenario), {
|
||||
onExit: (error) => {
|
||||
exit = error
|
||||
}
|
||||
})
|
||||
|
||||
await until(() => exit, 'the exit error')
|
||||
expect(providerStartupFailureFact(exit)).toEqual({
|
||||
kind: 'fullAccessAsRoot',
|
||||
detail: { text: expect.stringContaining(refusal), audience: 'log' }
|
||||
})
|
||||
})
|
||||
|
||||
it.runIf(process.platform !== 'win32')(
|
||||
'proves a natural SDK exit and cleans up its descendant before recovery',
|
||||
async () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { PipedProcessSpawner } from '@orca/process-host/process-spec'
|
||||
import { providerDiagnostic, withProviderDiagnostic } from '../../shared/agent-session-failure'
|
||||
import type * as ClaudeAgentSdk from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { CanUseTool, OnUserDialog, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
@@ -20,8 +19,8 @@ import {
|
||||
createClaudeUserMessageQueue
|
||||
} from './claude-agent-sdk-user-message-queue'
|
||||
import type { ClaudeStructuredSdkOptions } from './claude-structured-launch-resolution'
|
||||
import { providerStderrForDisplay } from '../provider-process/provider-spawn-failure-report'
|
||||
import { withMissingProviderExecutable } from '../provider-process/provider-executable-missing'
|
||||
import { claudeStreamJsonExitError, type ClaudeExitStatus } from './claude-stream-json-exit-error'
|
||||
|
||||
export { ClaudeControlRequestError }
|
||||
|
||||
@@ -90,25 +89,6 @@ export type ClaudeStreamJsonConnection = ClaudeControlSurface & {
|
||||
close: () => Promise<boolean>
|
||||
}
|
||||
|
||||
type ExitStatus = { code: number | null; signal: NodeJS.Signals | null }
|
||||
|
||||
function exitError(stderrTail: string, status: ExitStatus | null, cause?: Error): Error {
|
||||
const detail = providerStderrForDisplay(stderrTail).trim()
|
||||
// Retain exit status and stderr through wrappers for refused-start diagnostics.
|
||||
const how =
|
||||
status?.signal !== null && status?.signal !== undefined
|
||||
? ` (signal ${status.signal})`
|
||||
: status?.code !== null && status?.code !== undefined
|
||||
? ` (code ${status.code})`
|
||||
: ''
|
||||
const message = `claude stream-json exited${how}${detail ? `: ${detail}` : ''}`
|
||||
const diagnostic = providerDiagnostic([how.trim(), detail].filter(Boolean).join('\n'), 'log')
|
||||
return withProviderDiagnostic(
|
||||
cause ? new Error(message, { cause }) : new Error(message),
|
||||
diagnostic
|
||||
)
|
||||
}
|
||||
|
||||
export async function openClaudeStreamJsonConnection(
|
||||
launch: ClaudeStreamJsonLaunch,
|
||||
handlers: ClaudeStreamJsonConnectionHandlers = {},
|
||||
@@ -145,7 +125,7 @@ export async function openClaudeStreamJsonConnection(
|
||||
if (!child || !managed) {
|
||||
throw new Error('the claude agent SDK returned without spawning a child')
|
||||
}
|
||||
let exitStatus: ExitStatus | null = null
|
||||
let exitStatus: ClaudeExitStatus | null = null
|
||||
let closing = false
|
||||
let terminalError: Error | null = null
|
||||
let faultReported = false
|
||||
@@ -190,7 +170,7 @@ export async function openClaudeStreamJsonConnection(
|
||||
const handleUnexpectedEnd = (cause?: Error): void => {
|
||||
resumeReading()
|
||||
if (!terminalError) {
|
||||
const error = exitError(managed.stderrTail(), exitStatus, cause)
|
||||
const error = claudeStreamJsonExitError(managed.stderrTail(), exitStatus, cause)
|
||||
terminalError = managed.executableMissing ? withMissingProviderExecutable(error) : error
|
||||
}
|
||||
inbox.fail(terminalError)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { providerDiagnostic, withProviderDiagnostic } from '../../shared/agent-session-failure'
|
||||
import { providerStderrForDisplay } from '../provider-process/provider-spawn-failure-report'
|
||||
import { AgentSessionAcquisitionRefusal } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
|
||||
|
||||
export type ClaudeExitStatus = { code: number | null; signal: NodeJS.Signals | null }
|
||||
|
||||
// Claude Code's own startup refusal: as root it exits before initialize rather than skip permissions.
|
||||
const CLAUDE_ROOT_FULL_ACCESS_REFUSAL = /--dangerously-skip-permissions cannot be used with root/
|
||||
|
||||
export function claudeStreamJsonExitError(
|
||||
stderrTail: string,
|
||||
status: ClaudeExitStatus | null,
|
||||
cause?: Error
|
||||
): Error {
|
||||
const detail = providerStderrForDisplay(stderrTail).trim()
|
||||
// Retain exit status and stderr through wrappers for refused-start diagnostics.
|
||||
const how =
|
||||
status?.signal !== null && status?.signal !== undefined
|
||||
? ` (signal ${status.signal})`
|
||||
: status?.code !== null && status?.code !== undefined
|
||||
? ` (code ${status.code})`
|
||||
: ''
|
||||
const message = `claude stream-json exited${how}${detail ? `: ${detail}` : ''}`
|
||||
const diagnostic = providerDiagnostic([how.trim(), detail].filter(Boolean).join('\n'), 'log')
|
||||
// The typed refusal replaces this error in failure facts, so it carries the diagnostic too.
|
||||
const refusal = CLAUDE_ROOT_FULL_ACCESS_REFUSAL.test(detail)
|
||||
? withProviderDiagnostic(
|
||||
Object.assign(
|
||||
new AgentSessionAcquisitionRefusal(message, 'fullAccessAsRoot'),
|
||||
cause ? { cause } : {}
|
||||
),
|
||||
diagnostic
|
||||
)
|
||||
: undefined
|
||||
const inner = refusal ?? cause
|
||||
return withProviderDiagnostic(
|
||||
inner ? new Error(message, { cause: inner }) : new Error(message),
|
||||
diagnostic
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
identityFor,
|
||||
PROVIDER_SESSION_ID
|
||||
} from './claude-structured-session-test-support'
|
||||
import { claudeStreamJsonExitError } from './claude-stream-json-exit-error'
|
||||
|
||||
async function startedClaude(sink: StructuredAgentSessionEventSink) {
|
||||
const claude = fakeClaude()
|
||||
@@ -36,6 +37,23 @@ describe('what a started Claude session says ended it', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("names Claude's root refusal of Full access rather than a bare exit", async () => {
|
||||
const { connection, ended } = await startedClaude({
|
||||
appendItem: () => {},
|
||||
appendTombstone: () => {},
|
||||
publish: () => {}
|
||||
})
|
||||
const refusal =
|
||||
'--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons'
|
||||
|
||||
connection.handlers.onExit?.(claudeStreamJsonExitError(refusal, { code: 1, signal: null }))
|
||||
|
||||
expect(await ended()).toMatchObject({
|
||||
cause: 'unexpected-exit',
|
||||
failure: { kind: 'fullAccessAsRoot', detail: { text: `(code 1)\n${refusal}` } }
|
||||
})
|
||||
})
|
||||
|
||||
it('blames Orca when a journal fault makes Orca close the child', async () => {
|
||||
const { connection, ended } = await startedClaude({
|
||||
appendItem: () => {},
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { agentSessionFailureFact, providerDiagnosticOf } from '../../shared/agent-session-failure'
|
||||
import { providerExitObserved } from '../native-chat/agent-session-wire/structured-agent-session-failure-text'
|
||||
import {
|
||||
providerExitObserved,
|
||||
providerStartupFailureFact,
|
||||
startupAcquisitionRefusal
|
||||
} from '../native-chat/agent-session-wire/structured-agent-session-failure-text'
|
||||
import { settledClaudeTurnEndLeaf } from './claude-structured-resume-point'
|
||||
import { claudeHeldSends } from './claude-command-lifecycle'
|
||||
import {
|
||||
@@ -83,10 +87,12 @@ export function settleClaudeUnexpectedExit(
|
||||
type: 'ended',
|
||||
sessionId,
|
||||
reason: exit.error.message,
|
||||
// Distinguish provider exit from a host fault that forced cleanup.
|
||||
failure: providerExitObserved(exit.error)
|
||||
? agentSessionFailureFact('providerExited', { detail: providerDiagnosticOf(exit.error) })
|
||||
: agentSessionFailureFact('hostFault'),
|
||||
// A typed startup refusal keeps its next step; else distinguish provider exit from a host fault.
|
||||
failure: startupAcquisitionRefusal(exit.error)
|
||||
? providerStartupFailureFact(exit.error)
|
||||
: providerExitObserved(exit.error)
|
||||
? agentSessionFailureFact('providerExited', { detail: providerDiagnosticOf(exit.error) })
|
||||
: agentSessionFailureFact('hostFault'),
|
||||
cause: 'unexpected-exit',
|
||||
fence: exit.session.fence,
|
||||
acquisitionGeneration: exit.session.acquisitionGeneration,
|
||||
|
||||
@@ -37,6 +37,7 @@ const TYPED_START_REFUSALS = [
|
||||
'claudeAccountSetupFailed',
|
||||
'agentCommandNotRunnable',
|
||||
'runtimeNotInstalled',
|
||||
'fullAccessAsRoot',
|
||||
'conversationOpenInTerminal',
|
||||
'conversationOpenElsewhere'
|
||||
] as const satisfies readonly (AgentSessionFailureKind &
|
||||
@@ -80,7 +81,7 @@ export function startupAcquisitionRefusal(
|
||||
)
|
||||
}
|
||||
|
||||
/** A failed start keeps its typed refusal or observed exit and private provider diagnostic. */
|
||||
/** A failed start keeps its typed refusal or observed exit, and the provider diagnostic its Details show. */
|
||||
export function providerStartupFailureFact(cause?: unknown): SubmissionRejectionFact {
|
||||
cause = startupAcquisitionRefusal(cause) ?? cause
|
||||
const installation = agentCliInstallationProblemOf(cause)
|
||||
@@ -148,7 +149,7 @@ function refusedStartFailureFact(
|
||||
}
|
||||
|
||||
export type StructuredAgentSessionStartFailureCause =
|
||||
/** Provider words stay host-side; newSession distinguishes first launch from restart. */
|
||||
/** Provider words reach Details, never the sentence; newSession distinguishes first launch from restart. */
|
||||
| {
|
||||
refusal: AgentSessionWireRefusal
|
||||
diagnostic?: ProviderDiagnostic
|
||||
|
||||
@@ -24,8 +24,8 @@ import { useNativeChatRowNavigation } from './use-native-chat-row-navigation'
|
||||
|
||||
/** Unconfirmed sending status or a failed-delivery notice with an optional dismissal. */
|
||||
export type NativeChatDeliveryNotice =
|
||||
| { sending: true; text?: never; onDismiss?: never }
|
||||
| { sending?: never; text: string; onDismiss?: () => void }
|
||||
| { sending: true; text?: never; onDismiss?: never; detail?: never }
|
||||
| { sending?: never; text: string; onDismiss?: () => void; detail?: string }
|
||||
|
||||
export type MessageRowProps = {
|
||||
message: NativeChatMessage
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
nativeChatClaudeSignInLabel,
|
||||
useNativeChatClaudeSignInView
|
||||
} from './native-chat-claude-sign-in'
|
||||
import { readWholeAgentSessionFailureFact } from '../../../../shared/agent-session-failure'
|
||||
import {
|
||||
agentSessionFailureDetailText,
|
||||
readWholeAgentSessionFailureFact
|
||||
} from '../../../../shared/agent-session-failure'
|
||||
import { agentSessionFailureSentence } from '../../../../shared/agent-session-failure-words'
|
||||
import { sayAgentSessionFailureTranslated } from './agent-session-failure-words-text'
|
||||
import { nativeChatAntigravitySignInContent } from './native-chat-antigravity-settings'
|
||||
@@ -173,10 +176,8 @@ export function NativeChatNoticeRow({
|
||||
sayAgentSessionFailureTranslated
|
||||
)
|
||||
: agentSessionGoalControlWords(block.text)
|
||||
const detail =
|
||||
failure?.detail?.audience === 'person' && failure.detail.text.trim() !== text.trim()
|
||||
? failure.detail.text
|
||||
: undefined
|
||||
const failureDetail = agentSessionFailureDetailText(failure)
|
||||
const detail = failureDetail?.trim() !== text.trim() ? failureDetail : undefined
|
||||
const tone = named ? 'notice' : look.kind === 'line' ? look.tone : null
|
||||
const Icon =
|
||||
tone === 'warning'
|
||||
|
||||
@@ -305,32 +305,59 @@ export function ProviderFrameRow({
|
||||
}
|
||||
const frame = block.providerFrame
|
||||
return (
|
||||
<details className="group text-xs text-chat-foreground-faint">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-1 font-mono hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<span className="transition-transform motion-reduce:transition-none group-open:rotate-90">
|
||||
›
|
||||
</span>
|
||||
{frame ? (
|
||||
<NativeChatDetailsDisclosure
|
||||
lead={
|
||||
frame ? (
|
||||
<span className="font-medium text-chat-foreground-strong">{frame.provider}</span>
|
||||
) : null}
|
||||
<span className="truncate">{summary ?? nativeChatProviderFrameSummary(block)}</span>
|
||||
{frame?.payload.truncated ? (
|
||||
) : null
|
||||
}
|
||||
summary={summary ?? nativeChatProviderFrameSummary(block)}
|
||||
trail={
|
||||
frame?.payload.truncated ? (
|
||||
<span>
|
||||
·{' '}
|
||||
{translate('components.native-chat.providerFrame.byteLength', '{{value0}} bytes', {
|
||||
value0: frame.payload.byteLength
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{detail}
|
||||
{detail && frame ? '\n\n' : ''}
|
||||
{frame?.payload.head}
|
||||
{frame?.payload.truncated ? '\n…' : ''}
|
||||
</NativeChatDetailsDisclosure>
|
||||
)
|
||||
}
|
||||
|
||||
/** A collapsed monospace body, for provider frames and the diagnostics behind a failure. */
|
||||
export function NativeChatDetailsDisclosure({
|
||||
summary,
|
||||
lead,
|
||||
trail,
|
||||
children
|
||||
}: {
|
||||
summary: string
|
||||
lead?: React.ReactNode
|
||||
trail?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<details className="group text-xs text-chat-foreground-faint">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-1 font-mono hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<span className="transition-transform motion-reduce:transition-none group-open:rotate-90">
|
||||
›
|
||||
</span>
|
||||
{lead}
|
||||
<span className="truncate">{summary}</span>
|
||||
{trail}
|
||||
</summary>
|
||||
<pre
|
||||
data-native-chat-code-content
|
||||
className="scrollbar-sleek mt-1 max-h-64 overflow-auto whitespace-pre-wrap rounded-md border border-border bg-muted p-2 font-mono text-chat-code text-foreground"
|
||||
>
|
||||
{detail}
|
||||
{detail && frame ? '\n\n' : ''}
|
||||
{frame?.payload.head}
|
||||
{frame?.payload.truncated ? '\n…' : ''}
|
||||
{children}
|
||||
</pre>
|
||||
</details>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,10 @@ import { NativeChatCodeBlock } from './NativeChatCodeBlock'
|
||||
import { NativeChatCopyButton } from './NativeChatCopyButton'
|
||||
import { NativeChatMessageTimestamp } from './NativeChatMessageTimestamp'
|
||||
import { NativeChatAgentMessageSenders } from './NativeChatAgentMessageSenders'
|
||||
import { NativeChatImageAttachments } from './NativeChatTranscriptChrome'
|
||||
import {
|
||||
NativeChatDetailsDisclosure,
|
||||
NativeChatImageAttachments
|
||||
} from './NativeChatTranscriptChrome'
|
||||
import type { deriveNativeChatRowContent } from '../../../../shared/native-chat-row-content'
|
||||
|
||||
const USER_META_REVEAL =
|
||||
@@ -159,6 +162,15 @@ export function NativeChatUserMessageRow({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{deliveryNotice?.detail ? (
|
||||
<div className="max-w-[85%]">
|
||||
<NativeChatDetailsDisclosure
|
||||
summary={translate('components.native-chat.notices.details', 'Details')}
|
||||
>
|
||||
{deliveryNotice.detail}
|
||||
</NativeChatDetailsDisclosure>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,12 @@ const PIECES: Record<AgentSessionFailureCopyId, (values: AgentSessionFailureCopy
|
||||
COPY.runtimeNotInstalled,
|
||||
values
|
||||
),
|
||||
fullAccessAsRoot: (values) =>
|
||||
translate(
|
||||
'components.native-chat.failureWords.fullAccessAsRoot',
|
||||
COPY.fullAccessAsRoot,
|
||||
values
|
||||
),
|
||||
conversationOpenElsewhere: () =>
|
||||
translate(
|
||||
'components.native-chat.failureWords.conversationOpenElsewhere',
|
||||
|
||||
+26
@@ -101,6 +101,32 @@ describe('the line under each of the chat own messages', () => {
|
||||
expect(notice?.onDismiss).toBeUndefined()
|
||||
})
|
||||
|
||||
it("keeps the CLI's own startup words behind the rejected message unless a start row has them", () => {
|
||||
const exited: AgentSessionFailureFact = {
|
||||
kind: 'providerStartFailed',
|
||||
detail: { text: '(code 1)\nclaude: unknown option --foo', audience: 'log' }
|
||||
}
|
||||
const submission = row('a', 'rejected', { reason: 'Written by the host.', rejection: exited })
|
||||
expect(notices({ submissions: [submission] }).get(agentJournalSubmissionKey('a'))).toEqual({
|
||||
text: 'Claude stopped before it finished starting. Send your message to try again.',
|
||||
detail: '(code 1)\nclaude: unknown option --foo'
|
||||
})
|
||||
expect(
|
||||
notices({ submissions: [submission], startFailures: [exited] }).get(
|
||||
agentJournalSubmissionKey('a')
|
||||
)?.detail
|
||||
).toBeUndefined()
|
||||
// A refusal's sentence already quotes the provider's words.
|
||||
const rejected: AgentSessionFailureFact = {
|
||||
kind: 'providerRejected',
|
||||
detail: { text: 'Too long', audience: 'person' }
|
||||
}
|
||||
const refused = row('b', 'rejected', { rejection: rejected })
|
||||
expect(notices({ submissions: [refused] }).get(agentJournalSubmissionKey('b'))?.detail).toBe(
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('says nothing for a message a Stop withdrew: it went back to the composer', () => {
|
||||
expect(
|
||||
texts(notices({ submissions: [row('a', 'rejected', { rejection: { kind: 'cancelled' } })] }))
|
||||
|
||||
+17
-2
@@ -1,4 +1,10 @@
|
||||
import type { AgentSessionFailureFact } from '../../../../shared/agent-session-failure'
|
||||
import {
|
||||
agentSessionFailureDetailText,
|
||||
readWholeAgentSessionFailureFact,
|
||||
type AgentSessionFailureFact
|
||||
} from '../../../../shared/agent-session-failure'
|
||||
import { agentSessionFailureStatedByRow } from '../../../../shared/agent-session-visible-failures'
|
||||
import { classifyDispatchRejection } from '../../../../shared/structured-agent-session-dispatch-rejection'
|
||||
import { agentJournalSubmissionKey } from '../../../../shared/agent-session-journal-item-key'
|
||||
import type { AgentJournalSubmission } from '../../../../shared/agent-session-journal-types'
|
||||
import { structuredAgentSessionRecordedRejectionParts } from '../../../../shared/structured-agent-session-rejection-words'
|
||||
@@ -44,6 +50,14 @@ export function structuredAgentSessionDeliveryNotices(args: {
|
||||
for (const submission of submissions) {
|
||||
const id = agentJournalSubmissionKey(submission.clientMessageId)
|
||||
if (submission.dispatchState === 'rejected' && shown.has(id)) {
|
||||
// A failed start's words, unless its row carries them; other sentences quote their own.
|
||||
const fact = readWholeAgentSessionFailureFact(submission.rejection)
|
||||
const detail =
|
||||
fact &&
|
||||
classifyDispatchRejection({ reason: null, rejection: fact }).category === 'startFailed' &&
|
||||
!agentSessionFailureStatedByRow(fact, args.startFailures)
|
||||
? agentSessionFailureDetailText(fact)
|
||||
: undefined
|
||||
notices.set(id, {
|
||||
text: agentSessionWriteNoticeText(
|
||||
structuredAgentSessionRecordedRejectionParts(
|
||||
@@ -51,7 +65,8 @@ export function structuredAgentSessionDeliveryNotices(args: {
|
||||
{ agentName },
|
||||
args.startFailures
|
||||
)
|
||||
)
|
||||
),
|
||||
...(detail ? { detail } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -3188,6 +3188,7 @@
|
||||
"runCommandAgain": "Run /{{command}} again.",
|
||||
"runCommandWhenDone": "Run /{{command}} when it's done.",
|
||||
"runtimeNotInstalled": "{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
|
||||
"fullAccessAsRoot": "{{agent}} won't use Full access while it runs as root. Choose another permission for {{agent}} in Settings → Agents for the computer running this chat, then start a new chat.",
|
||||
"conversationOpenElsewhere": "Codex is still holding this conversation open. Try again in a moment, or close other Codex terminals.",
|
||||
"conversationOpenInTerminal": "{{agent}} is already running this conversation in another terminal. Quit {{agent}} there or close that terminal to continue here.",
|
||||
"sendAgainToTryOnceMore": "Send your message again to try once more.",
|
||||
|
||||
@@ -19042,6 +19042,7 @@
|
||||
"claudeAccountSetupFailed": "The selected Claude account could not be set up. Try again or choose another account.",
|
||||
"agentCommandNotRunnable": "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.",
|
||||
"runtimeNotInstalled": "{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
|
||||
"fullAccessAsRoot": "{{agent}} won't use Full access while it runs as root. Choose another permission for {{agent}} in Settings → Agents for the computer running this chat, then start a new chat.",
|
||||
"conversationOpenElsewhere": "Codex is still holding this conversation open. Try again in a moment, or close other Codex terminals.",
|
||||
"conversationOpenInTerminal": "{{agent}} is already running this conversation in another terminal. Quit {{agent}} there or close that terminal to continue here.",
|
||||
"chooseClaudeAccount": "Choose or add one in Claude Accounts settings.",
|
||||
|
||||
@@ -18654,6 +18654,7 @@
|
||||
"claudeAccountSetupFailed": "No se pudo configurar la cuenta de Claude seleccionada. Vuelva a intentarlo o elija otra cuenta.",
|
||||
"agentCommandNotRunnable": "{{agent}}: el Comando en Ajustes → Agentes debe ser la ruta o el nombre de un programa que Orca pueda encontrar, sin argumentos ni variables. Cámbielo o restablézcalo con Reiniciar.",
|
||||
"runtimeNotInstalled": "{{agent}} no está instalado en el equipo que ejecuta este chat. Instálelo desde el chat en una versión actual de Orca.",
|
||||
"fullAccessAsRoot": "{{agent}} no usará Acceso total mientras se ejecute como root. Elija otro permiso para {{agent}} en Configuración → Agentes para el equipo que ejecuta este chat y luego inicie un chat nuevo.",
|
||||
"conversationOpenElsewhere": "Codex sigue manteniendo abierta esta conversación. Inténtalo de nuevo en un momento o cierra otras terminales de Codex.",
|
||||
"conversationOpenInTerminal": "{{agent}} ya está ejecutando esta conversación en otra terminal. Sal de {{agent}} allí o cierra esa terminal para continuar aquí.",
|
||||
"chooseClaudeAccount": "Elija o agregue una en la configuración de Cuentas de Claude.",
|
||||
|
||||
@@ -18794,6 +18794,7 @@
|
||||
"claudeAccountSetupFailed": "Le compte Claude sélectionné n'a pas pu être configuré. Réessayez ou choisissez un autre compte.",
|
||||
"agentCommandNotRunnable": "{{agent}} : la Commande dans Paramètres → Agents doit être le chemin ou le nom d'un programme qu'Orca peut trouver, sans arguments ni variables. Modifiez-la ou réinitialisez-la.",
|
||||
"runtimeNotInstalled": "{{agent}} n'est pas installé sur l'ordinateur qui exécute ce chat. Installez-le depuis le chat dans une version récente d'Orca.",
|
||||
"fullAccessAsRoot": "{{agent}} n'utilisera pas l'Accès complet tant qu'il s'exécute en tant que root. Choisissez une autre autorisation pour {{agent}} dans Paramètres → Agents pour l'ordinateur qui exécute ce chat, puis démarrez un nouveau chat.",
|
||||
"conversationOpenElsewhere": "Codex maintient encore cette conversation ouverte. Réessayez dans un instant ou fermez les autres terminaux Codex.",
|
||||
"conversationOpenInTerminal": "{{agent}} exécute déjà cette conversation dans un autre terminal. Quittez {{agent}} là-bas ou fermez ce terminal pour continuer ici.",
|
||||
"chooseClaudeAccount": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude.",
|
||||
|
||||
@@ -18684,6 +18684,7 @@
|
||||
"claudeAccountSetupFailed": "選択した Claude アカウントを設定できませんでした。もう一度試すか、別のアカウントを選択してください。",
|
||||
"agentCommandNotRunnable": "[設定] → [Agent] にある {{agent}} の [コマンド] には、Orca が見つけられるプログラムのパスまたは名前を、引数や変数なしで指定する必要があります。変更するか、[リセット] してください。",
|
||||
"runtimeNotInstalled": "{{agent}} はこのチャットを実行しているコンピューターにインストールされていません。最新バージョンの Orca でチャットからインストールしてください。",
|
||||
"fullAccessAsRoot": "{{agent}} は root として実行されている間はフルアクセスを使用しません。このチャットを実行しているコンピューターの [設定 → Agent] で {{agent}} に別の権限を選択してから、新しいチャットを開始してください。",
|
||||
"conversationOpenElsewhere": "Codex がまだこの会話を開いたままにしています。少し待ってからもう一度試すか、ほかの Codex ターミナルを閉じてください。",
|
||||
"conversationOpenInTerminal": "{{agent}} は別のターミナルですでにこの会話を実行しています。そちらの {{agent}} を終了するか、そのターミナルを閉じて、ここで続けてください。",
|
||||
"chooseClaudeAccount": "Claude アカウントの設定で選択または追加してください。",
|
||||
|
||||
@@ -18684,6 +18684,7 @@
|
||||
"claudeAccountSetupFailed": "선택한 Claude 계정을 설정할 수 없습니다. 다시 시도하거나 다른 계정을 선택하세요.",
|
||||
"agentCommandNotRunnable": "{{agent}}의 설정 → 에이전트 → 명령은 Orca가 찾을 수 있는 프로그램 경로나 이름이어야 하며, 인수나 변수를 포함할 수 없습니다. 변경하거나 재설정하세요.",
|
||||
"runtimeNotInstalled": "{{agent}}이(가) 이 채팅을 실행하는 컴퓨터에 설치되어 있지 않습니다. 최신 버전의 Orca에서 채팅을 통해 설치하세요.",
|
||||
"fullAccessAsRoot": "{{agent}}은(는) root로 실행되는 동안 전체 액세스를 사용하지 않습니다. 이 채팅을 실행하는 컴퓨터의 설정 → 에이전트에서 {{agent}}에 다른 권한을 선택한 다음 새 채팅을 시작하세요.",
|
||||
"conversationOpenElsewhere": "Codex가 아직 이 대화를 열어 두고 있습니다. 잠시 후 다시 시도하거나 다른 Codex 터미널을 닫으세요.",
|
||||
"conversationOpenInTerminal": "{{agent}}이(가) 다른 터미널에서 이미 이 대화를 실행 중입니다. 그곳에서 {{agent}}을(를) 종료하거나 해당 터미널을 닫은 후 여기서 계속하세요.",
|
||||
"chooseClaudeAccount": "Claude 계정 설정에서 선택하거나 추가하세요.",
|
||||
|
||||
@@ -19633,6 +19633,7 @@
|
||||
"claudeAccountSetupFailed": "Não foi possível configurar a conta do Claude selecionada. Tente novamente ou escolha outra conta.",
|
||||
"agentCommandNotRunnable": "O Comando de {{agent}} em Configurações → Agentes deve ser o caminho ou nome de um programa que o Orca possa encontrar, sem argumentos nem variáveis. Altere-o ou redefina-o.",
|
||||
"runtimeNotInstalled": "{{agent}} não está instalado no computador que executa este chat. Instale-o pelo chat em uma versão atual do Orca.",
|
||||
"fullAccessAsRoot": "{{agent}} não usará Acesso total enquanto for executado como root. Escolha outra permissão para {{agent}} em Configurações → Agentes para o computador que executa este chat e inicie um novo chat.",
|
||||
"chooseClaudeAccount": "Escolha ou adicione uma nas configurações de Contas do Claude.",
|
||||
"chooseClaudeAccountThenRunCommand": "Escolha ou adicione uma nas configurações de Contas do Claude e execute /{{command}} novamente.",
|
||||
"chooseClaudeAccountThenSend": "Escolha ou adicione uma nas configurações de Contas do Claude e envie sua mensagem novamente.",
|
||||
|
||||
@@ -18649,6 +18649,7 @@
|
||||
"claudeAccountSetupFailed": "无法设置所选 Claude 账户。请重试或选择其他账户。",
|
||||
"agentCommandNotRunnable": "{{agent}} 在设置 → 智能体中的命令必须是 Orca 能找到的程序路径或名称,且不能包含参数或变量。请修改或重置。",
|
||||
"runtimeNotInstalled": "运行此聊天的计算机上未安装 {{agent}}。请在最新版本的 Orca 中从聊天里安装。",
|
||||
"fullAccessAsRoot": "{{agent}} 以 root 身份运行时不会使用完全访问。请在运行此聊天的计算机的“设置”→“智能体”中为 {{agent}} 选择其他权限,然后开始新的聊天。",
|
||||
"conversationOpenElsewhere": "Codex 仍在保持此对话打开。请稍后重试,或关闭其他 Codex 终端。",
|
||||
"conversationOpenInTerminal": "{{agent}} 已在另一个终端中运行此对话。请退出那里的 {{agent}} 或关闭该终端,然后在这里继续。",
|
||||
"chooseClaudeAccount": "请在 Claude 账户设置中选择或添加一个。",
|
||||
|
||||
@@ -78,6 +78,8 @@ export const AGENT_SESSION_FAILURE_COPY = {
|
||||
"{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.",
|
||||
runtimeNotInstalled:
|
||||
"{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
|
||||
fullAccessAsRoot:
|
||||
"{{agent}} won't use Full access while it runs as root. Choose another permission for {{agent}} in Settings → Agents for the computer running this chat, then start a new chat.",
|
||||
conversationOpenElsewhere:
|
||||
'Codex is still holding this conversation open. Try again in a moment, or close other Codex terminals.',
|
||||
conversationOpenInTerminal:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ProviderDiagnostic } from './agent-session-failure'
|
||||
import type {
|
||||
AgentSessionFailureCopyId,
|
||||
AgentSessionFailureCopyValues,
|
||||
AgentSessionFailureSay
|
||||
} from './agent-session-failure-copy'
|
||||
|
||||
/** Person-facing provider text is quoted, but bounded so the sentence stays one. */
|
||||
const MAX_QUOTED_DETAIL_CHARS = 512
|
||||
|
||||
/** The quoted lead when the provider wrote the detail for a person; the plain lead otherwise. */
|
||||
export function quotingPersonDetail(
|
||||
say: AgentSessionFailureSay,
|
||||
lead: AgentSessionFailureCopyId,
|
||||
quotedLead: AgentSessionFailureCopyId,
|
||||
detail: ProviderDiagnostic | undefined,
|
||||
values: AgentSessionFailureCopyValues = {}
|
||||
): string {
|
||||
const quoted =
|
||||
detail?.audience === 'person'
|
||||
? detail.text
|
||||
.slice(0, MAX_QUOTED_DETAIL_CHARS)
|
||||
.trim()
|
||||
.replace(/[.\s]+$/, '')
|
||||
: ''
|
||||
return quoted ? say(quotedLead, { ...values, detail: quoted }) : say(lead, values)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
isSubmissionRejectionFact,
|
||||
type AgentSessionFailureFact,
|
||||
type AgentSessionFailureKind,
|
||||
type ProviderDiagnostic,
|
||||
type SubmissionRejectionFact,
|
||||
type SubmissionRejectionKind
|
||||
} from './agent-session-failure'
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
agentCliInstallationFailureWords,
|
||||
sayAgentSessionFailureEnglish,
|
||||
type AgentSessionFailureCopyId,
|
||||
type AgentSessionFailureCopyValues,
|
||||
type AgentSessionFailureSay
|
||||
} from './agent-session-failure-copy'
|
||||
import { AGENT_SESSION_GOAL_CONTROL_COPY } from './agent-session-goal-control-copy'
|
||||
@@ -27,6 +25,7 @@ import {
|
||||
import { commandRefusedByReason } from './agent-session-command-refusal-words'
|
||||
import { agent, attachmentProblemSentence } from './agent-session-attachment-failure-words'
|
||||
import { joinSentences } from './sentence-joining'
|
||||
import { quotingPersonDetail } from './agent-session-failure-quoted-detail'
|
||||
import { requestTimeoutSeconds } from './agent-session-request-timeout'
|
||||
import {
|
||||
DISPATCH_REJECTED_CANCELLED,
|
||||
@@ -70,9 +69,6 @@ export type AgentSessionFailureWordsContext = {
|
||||
usageLimitReset?: AgentSessionUsageLimitResetWords
|
||||
}
|
||||
|
||||
/** Person-facing provider text is quoted, but bounded so the sentence stays one. */
|
||||
const MAX_QUOTED_DETAIL_CHARS = 512
|
||||
|
||||
type Sentence = (
|
||||
context: AgentSessionFailureWordsContext,
|
||||
fact: AgentSessionFailureFact,
|
||||
@@ -84,23 +80,6 @@ function namingAgent(id: AgentSessionFailureCopyId): Sentence {
|
||||
return (context, _fact, _surface, say) => say(id, agent(say, context))
|
||||
}
|
||||
|
||||
function quotingPersonDetail(
|
||||
say: AgentSessionFailureSay,
|
||||
lead: AgentSessionFailureCopyId,
|
||||
quotedLead: AgentSessionFailureCopyId,
|
||||
detail: ProviderDiagnostic | undefined,
|
||||
values: AgentSessionFailureCopyValues = {}
|
||||
): string {
|
||||
const quoted =
|
||||
detail?.audience === 'person'
|
||||
? detail.text
|
||||
.slice(0, MAX_QUOTED_DETAIL_CHARS)
|
||||
.trim()
|
||||
.replace(/[.\s]+$/, '')
|
||||
: ''
|
||||
return quoted ? say(quotedLead, { ...values, detail: quoted }) : say(lead, values)
|
||||
}
|
||||
|
||||
/** The next step after a start or restart that failed: the command, or the message, again. */
|
||||
function startRetry(
|
||||
say: AgentSessionFailureSay,
|
||||
@@ -186,6 +165,7 @@ const FAILURE_SENTENCES = {
|
||||
claudeAccountSetupFailed: (_context, _fact, _surface, say) => say('claudeAccountSetupFailed'),
|
||||
agentCommandNotRunnable: namingAgent('agentCommandNotRunnable'),
|
||||
runtimeNotInstalled: namingAgent('runtimeNotInstalled'),
|
||||
fullAccessAsRoot: namingAgent('fullAccessAsRoot'),
|
||||
conversationOpenInTerminal: namingAgent('conversationOpenInTerminal'),
|
||||
conversationOpenElsewhere: (_context, _fact, _surface, say) => say('conversationOpenElsewhere'),
|
||||
managedAccountUnsupported: (context, _fact, _surface, say) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
MAX_PROVIDER_DIAGNOSTIC_CHARS,
|
||||
agentSessionFailureDetailText,
|
||||
providerDiagnostic,
|
||||
providerDiagnosticOf,
|
||||
agentSessionFailureFact,
|
||||
@@ -46,6 +47,16 @@ describe('provider diagnostics', () => {
|
||||
).toBe(undefined)
|
||||
expect(providerDiagnosticOf('a string')).toBeUndefined()
|
||||
})
|
||||
|
||||
it("puts a start's log tail behind Details, but not the raw frame a retry row already shows", () => {
|
||||
const tail = { text: '(code 1)\nclaude: unknown option', audience: 'log' as const }
|
||||
expect(agentSessionFailureDetailText({ kind: 'providerStartFailed', detail: tail })).toBe(
|
||||
tail.text
|
||||
)
|
||||
expect(agentSessionFailureDetailText({ kind: 'providerRetrying', detail: tail })).toBe(
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reading a failure fact', () => {
|
||||
|
||||
@@ -40,6 +40,8 @@ export const AGENT_SESSION_FAILURE_KINDS = [
|
||||
'claudeAccountSetupFailed',
|
||||
'agentCommandNotRunnable',
|
||||
'runtimeNotInstalled',
|
||||
/** The agent refuses Full access while it runs as root. */
|
||||
'fullAccessAsRoot',
|
||||
'conversationOpenInTerminal',
|
||||
'conversationOpenElsewhere',
|
||||
'providerExited',
|
||||
@@ -363,3 +365,14 @@ export function withProviderDiagnostic<TError extends Error>(
|
||||
): TError {
|
||||
return diagnostic ? Object.assign(error, { providerDiagnostic: diagnostic }) : error
|
||||
}
|
||||
|
||||
/** The detail behind a failure's Details: provider words, or a log tail such as a CLI's startup stderr.
|
||||
* A retry's log is the raw frame its row already shows. */
|
||||
export function agentSessionFailureDetailText(
|
||||
fact: AgentSessionFailureFact | undefined
|
||||
): string | undefined {
|
||||
const detail = fact?.detail
|
||||
return detail && !(detail.audience === 'log' && fact.kind === 'providerRetrying')
|
||||
? detail.text
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ export const AGENT_SESSION_REFUSAL_REASONS = {
|
||||
'agentCommandNotRunnable',
|
||||
/** The runtime Orca installs for the agent is not installed on this host. */
|
||||
'runtimeNotInstalled',
|
||||
/** The agent refuses Full access while it runs as root. */
|
||||
'fullAccessAsRoot',
|
||||
/** A terminal may still be writing the chat's conversation, so its agent did not start. */
|
||||
'conversationOpenInTerminal',
|
||||
'conversationOpenElsewhere',
|
||||
|
||||
@@ -116,6 +116,7 @@ const REASON_WORDS = {
|
||||
claudeAccountSetupFailed: { fact: 'claudeAccountSetupFailed', action: 'retry' },
|
||||
agentCommandNotRunnable: { fact: 'agentCommandNotRunnable', action: 'actFirst' },
|
||||
runtimeNotInstalled: { fact: 'runtimeNotInstalled', action: 'actFirst' },
|
||||
fullAccessAsRoot: { fact: 'fullAccessAsRoot', action: 'actFirst' },
|
||||
conversationOpenInTerminal: { fact: 'conversationOpenInTerminal', action: 'actFirst' },
|
||||
conversationOpenElsewhere: { fact: 'conversationOpenElsewhere', action: 'actFirst' },
|
||||
attachFailed: codeWords('retry')
|
||||
|
||||
@@ -80,6 +80,7 @@ const KIND_CATEGORY = {
|
||||
claudeAccountSetupFailed: 'startFailed',
|
||||
agentCommandNotRunnable: 'startFailed',
|
||||
runtimeNotInstalled: 'startFailed',
|
||||
fullAccessAsRoot: 'startFailed',
|
||||
conversationOpenInTerminal: 'startFailed',
|
||||
conversationOpenElsewhere: 'startFailed',
|
||||
restartFailed: 'startFailed',
|
||||
@@ -115,6 +116,7 @@ const KIND_VERDICT = {
|
||||
claudeAccountSetupFailed: 'failure',
|
||||
agentCommandNotRunnable: 'failure',
|
||||
runtimeNotInstalled: 'failure',
|
||||
fullAccessAsRoot: 'failure',
|
||||
conversationOpenInTerminal: 'failure',
|
||||
conversationOpenElsewhere: 'failure',
|
||||
restartFailed: 'failure',
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// A Claude chat on a managed SSH host whose CLI refuses to start says why, not only that it stopped.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { callEnvironment } from './helpers/paired-host-terminal'
|
||||
import {
|
||||
launchManagedWorkspace,
|
||||
skipUnlessManagedDockerHost,
|
||||
withManagedHost
|
||||
} from './helpers/orcad-managed-workspace'
|
||||
|
||||
skipUnlessManagedDockerHost()
|
||||
test.skip(process.platform === 'win32', 'The Docker SSH host uses POSIX tooling')
|
||||
|
||||
const STUB = readFileSync(
|
||||
path.join(process.cwd(), 'tests', 'e2e', 'fixtures', 'structured-claude-stub.cjs')
|
||||
)
|
||||
const ROOT_REFUSAL =
|
||||
'--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons'
|
||||
|
||||
// The Docker host's SSH user is root, and Claude Code refuses Full access as root this way.
|
||||
const WRAPPER = `#!/bin/sh
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--dangerously-skip-permissions" ] && [ "$(id -u)" = 0 ]; then
|
||||
echo "${ROOT_REFUSAL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exec /usr/local/bin/node /usr/local/lib/orca-e2e-claude-stub.cjs /tmp/orca-e2e-claude-launches.jsonl "$@"
|
||||
`
|
||||
|
||||
test('a Claude chat that Claude refuses to start as root names the reason', async (// oxlint-disable-next-line no-empty-pattern -- Owns its app launch through a restart session.
|
||||
{}, testInfo) => {
|
||||
// Converting the Docker host to a managed server takes minutes before the chat opens.
|
||||
test.setTimeout(10 * 60_000)
|
||||
await withManagedHost(testInfo, async (fixture) => {
|
||||
const exec = fixture.host.exec
|
||||
if (!exec) {
|
||||
throw new Error('the Docker host runs commands as its SSH user')
|
||||
}
|
||||
exec(
|
||||
`echo ${STUB.toString('base64')} | base64 -d > /usr/local/lib/orca-e2e-claude-stub.cjs && ` +
|
||||
`echo ${Buffer.from(WRAPPER).toString('base64')} | base64 -d > /usr/local/bin/claude && ` +
|
||||
'chmod 755 /usr/local/bin/claude'
|
||||
)
|
||||
const { page, environmentId } = await launchManagedWorkspace(fixture)
|
||||
await page.evaluate(async () => {
|
||||
await window.__store?.getState().updateSettings({
|
||||
experimentalNativeChat: true,
|
||||
defaultTuiAgent: 'claude'
|
||||
})
|
||||
})
|
||||
await page.getByRole('button', { name: 'New tab', exact: true }).first().click()
|
||||
await page.getByText('Claude', { exact: true }).first().click({ timeout: 60_000 })
|
||||
const chat = page.locator('[data-native-chat-root="true"]')
|
||||
await expect(chat).toBeVisible({ timeout: 60_000 })
|
||||
const composer = chat.getByRole('textbox')
|
||||
await composer.click()
|
||||
await page.keyboard.type('hello')
|
||||
await chat.getByRole('button', { name: 'Send', exact: true }).click()
|
||||
await expect(chat).toContainText("Claude won't use Full access while it runs as root.", {
|
||||
timeout: 120_000
|
||||
})
|
||||
// Claude's own words sit behind Details on the start-failure row.
|
||||
await chat.getByText('Details', { exact: true }).first().click()
|
||||
await expect(chat).toContainText(ROOT_REFUSAL)
|
||||
await page.screenshot({ path: testInfo.outputPath('claude-root-start-failure.png') })
|
||||
|
||||
// The sentence's next step works: with another permission on that host, a new chat starts Claude.
|
||||
await callEnvironment(page, environmentId, 'settings.mutateAgentLaunch', {
|
||||
type: 'permission',
|
||||
choice: 'ask',
|
||||
agent: 'claude'
|
||||
})
|
||||
await page.getByRole('button', { name: 'New tab', exact: true }).first().click()
|
||||
await page.getByText('Claude', { exact: true }).first().click({ timeout: 60_000 })
|
||||
const next = page.locator('[data-native-chat-root="true"]').last()
|
||||
await next.getByRole('textbox').click()
|
||||
await page.keyboard.type('again')
|
||||
await next.getByRole('button', { name: 'Send', exact: true }).click()
|
||||
await expect(next.getByText('Stub reply.')).toBeVisible({ timeout: 120_000 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user