mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 64 low-value file, terminal and language tests (#34482)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -681,7 +681,7 @@
|
||||
"invariant": "A prompt occurrence leaves the input region only when an accepted reply answers it (a current host's provider acknowledgment, or an older host's whole-write acceptance as before) or the user collapses it to a strip above the composer, which writes nothing and keeps the card mounted with its partial answer; status-backed occurrences are told apart by the host wait's start, so view toggles and remounts preserve them while real clear/replacement supersedes them, and a transcript-only one is dropped when the view stops observing it; obsolete results cannot change replacements; selector groups wait for complete writes before pacing; refusal or unknown handoff never causes replay; a card retires an unsubmitted composer send without writing under it or losing its text, and the hidden composer cannot send or interrupt; Collapse or Escape folds a card without writing, never during an in-flight answer, and a new occurrence shows expanded again; the phone blocks only Send while a card is expanded and shows one card per occurrence; cancellation stops unsent groups and active dictation can stop.",
|
||||
"oracle": "After acknowledged phone replies with stale host status, the exact card disappears and Send is enabled; unknown, refused or unverifiable-settlement replies retain enabled choices, while an older host's accepted reply dismisses like an acknowledged one; a same-occurrence view toggle retains pending acknowledgment and real replacements discard it. A slow 32 KiB SSH answer completes all chunks before Enter; cancellation preserves issued bytes but omits remaining groups. Paired settlement invokes the actual provider and lost callbacks remain unknown; older hosts deliver ordinary messages, launch and selector groups once and their accepted answers dismiss as before. An answered card stays hidden across a view remount; a collapsed card is a strip that frees the composer and expands back; an identical question in a new wait shows again; a composer send caught by a new card keeps its bubble without submitting or clearing the line under it.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/runtime/runtime-terminal-verified-input.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
|
||||
"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.stop.test.ts mobile/src/session/MobileNativeChatView.failure-banner.test.ts mobile/src/session/MobileNativeChatView.rows.test.ts mobile/src/session/MobileNativeChatView.composer-lock.test.ts mobile/src/session/MobileNativeChatView.tail-follow.test.ts mobile/src/session/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/native-chat/input/mobile-native-chat-permission-send.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts mobile/src/session/native-chat/mobile-native-chat-send.test.ts mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts --maxWorkers=1",
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts --maxWorkers=2",
|
||||
@@ -696,7 +696,6 @@
|
||||
"src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx",
|
||||
"src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx",
|
||||
"src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts",
|
||||
"src/renderer/src/runtime/runtime-terminal-verified-input.test.ts",
|
||||
"src/renderer/src/components/native-chat/NativeChatComposer.test.tsx",
|
||||
"src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx",
|
||||
"src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx",
|
||||
@@ -838,7 +837,7 @@
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/runtime/runtime-terminal-verified-input.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
|
||||
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/runtime/runtime-terminal-verified-input.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
|
||||
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-observed-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
|
||||
"result": "passed",
|
||||
"durationSeconds": 14.53,
|
||||
"summary": "391 tests passed across 29 explicit files after the round-4 review fix, including older-host acceptance, the inert hidden composer, collapse that keeps a partial answer and returns focus to the composer, pane-scoped card focus, wait-start occurrence identity and the pane remount signal."
|
||||
@@ -10569,12 +10568,6 @@
|
||||
"explicit stop, real exit status, and genuine successful completion remain distinct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
|
||||
"assertions": [
|
||||
"direct SSH terminals use strict main-process inspection rather than lax split IPC evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/terminal-pane/pty-connection-completion-and-notification.test.ts",
|
||||
"assertions": [
|
||||
@@ -17544,12 +17537,6 @@
|
||||
"malformed frames, malformed transformed output, disposal, late parse, renderer delivery failure, and ACK transport failure release credit or close the owning stream without reordering output"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/runtime/runtime-terminal-stream.test.ts",
|
||||
"assertions": [
|
||||
"drops output only from the armed stream when its replacement reuses the stream ID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts",
|
||||
"assertions": [
|
||||
|
||||
@@ -10677,7 +10677,6 @@
|
||||
"src/renderer/src/lib/monaco-languages/register-svelte.test.ts": 121,
|
||||
"src/renderer/src/lib/monaco-languages/register-vue.test.ts": 157,
|
||||
"src/renderer/src/lib/monaco-languages/register-zig.test.ts": 3891,
|
||||
"src/renderer/src/lib/monaco-languages/textmate-language-registration.test.ts": 37,
|
||||
"src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 233,
|
||||
"src/renderer/src/lib/monaco-languages/zig-highlighting.test.ts": 4871,
|
||||
"src/renderer/src/lib/monaco-setup-steps.test.ts": 48,
|
||||
@@ -11192,7 +11191,6 @@
|
||||
"src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts": 26,
|
||||
"src/renderer/src/runtime/remote-runtime-snapshot-outcome.test.ts": 226,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-color-push.test.ts": 179,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-end-verdict.test.ts": 278,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts": 214,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts": 998,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-paused-output-drop.test.ts": 169,
|
||||
@@ -11263,7 +11261,6 @@
|
||||
"src/renderer/src/runtime/runtime-terminal-artifact-read.test.ts": 196,
|
||||
"src/renderer/src/runtime/runtime-terminal-inspection.test.ts": 1950,
|
||||
"src/renderer/src/runtime/runtime-terminal-stream.test.ts": 638,
|
||||
"src/renderer/src/runtime/runtime-terminal-verified-input.test.ts": 1206,
|
||||
"src/renderer/src/runtime/runtime-usage-credentials-client.test.ts": 37,
|
||||
"src/renderer/src/runtime/runtime-worktree-selector.test.ts": 130,
|
||||
"src/renderer/src/runtime/server-browser-profile-reuse.test.ts": 1580,
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { registerTextMateLanguage } from './textmate-language-registration'
|
||||
|
||||
function createMonacoMock(registeredLanguages: { id: string }[] = []) {
|
||||
let tokensProviderFactory: { create: () => unknown } | undefined
|
||||
const monaco = {
|
||||
languages: {
|
||||
getLanguages: vi.fn(() => registeredLanguages),
|
||||
register: vi.fn(),
|
||||
setLanguageConfiguration: vi.fn(),
|
||||
registerTokensProviderFactory: vi.fn(
|
||||
(_languageId: string, factory: { create: () => unknown }) => {
|
||||
tokensProviderFactory = factory
|
||||
return { dispose: vi.fn() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
monaco,
|
||||
createTokensProvider() {
|
||||
if (!tokensProviderFactory) {
|
||||
throw new Error('Tokens provider factory was not registered')
|
||||
}
|
||||
return tokensProviderFactory.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('registerTextMateLanguage', () => {
|
||||
it('registers metadata and lazily installs the TextMate tokens provider', async () => {
|
||||
const { monaco, createTokensProvider } = createMonacoMock()
|
||||
const provider = {
|
||||
getInitialState: vi.fn(),
|
||||
tokenize: vi.fn()
|
||||
}
|
||||
const createTextMateTokensProvider = vi.fn(async () => provider)
|
||||
const loadProviderModule = vi.fn(async () => ({ createTextMateTokensProvider }))
|
||||
const loadGrammar = vi.fn()
|
||||
const configuration = { comments: { lineComment: '#' } }
|
||||
|
||||
registerTextMateLanguage(monaco as never, {
|
||||
language: {
|
||||
id: 'nim',
|
||||
extensions: ['.nim'],
|
||||
aliases: ['Nim']
|
||||
},
|
||||
configuration,
|
||||
loadGrammar,
|
||||
loadProviderModule
|
||||
})
|
||||
|
||||
expect(monaco.languages.register).toHaveBeenCalledWith({
|
||||
id: 'nim',
|
||||
extensions: ['.nim'],
|
||||
aliases: ['Nim']
|
||||
})
|
||||
expect(monaco.languages.setLanguageConfiguration).toHaveBeenCalledWith('nim', configuration)
|
||||
expect(monaco.languages.registerTokensProviderFactory).toHaveBeenCalledWith(
|
||||
'nim',
|
||||
expect.objectContaining({ create: expect.any(Function) })
|
||||
)
|
||||
expect(loadProviderModule).not.toHaveBeenCalled()
|
||||
|
||||
const providerPromise = createTokensProvider()
|
||||
expect(createTokensProvider()).toBe(providerPromise)
|
||||
|
||||
await expect(providerPromise).resolves.toBe(provider)
|
||||
expect(createTokensProvider()).toBe(providerPromise)
|
||||
expect(loadProviderModule).toHaveBeenCalledTimes(1)
|
||||
expect(createTextMateTokensProvider).toHaveBeenCalledTimes(1)
|
||||
expect(createTextMateTokensProvider).toHaveBeenCalledWith({ loadGrammar })
|
||||
})
|
||||
|
||||
it('does not register duplicate language ids', () => {
|
||||
const { monaco } = createMonacoMock([{ id: 'nim' }])
|
||||
|
||||
registerTextMateLanguage(monaco as never, {
|
||||
language: {
|
||||
id: 'nim',
|
||||
extensions: ['.nim']
|
||||
},
|
||||
loadGrammar: vi.fn()
|
||||
})
|
||||
|
||||
expect(monaco.languages.register).not.toHaveBeenCalled()
|
||||
expect(monaco.languages.registerTokensProviderFactory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -46,14 +46,4 @@ describe('Zig highlighting through production Monaco setup', () => {
|
||||
expect(tokens[6].map((token) => token.type)).toContain('keyword.zig')
|
||||
expect(tokens[6].map((token) => token.type)).not.toContain('string.multiline.zig')
|
||||
})
|
||||
|
||||
it('keeps unrelated languages and unclaimed Zig object notation unchanged', async () => {
|
||||
expect(detectLanguage('build.zig.zon')).toBe('plaintext')
|
||||
expect(detectLanguage('main.rs')).toBe('rust')
|
||||
expect(detectLanguage('main.c')).toBe('c')
|
||||
await monaco.editor.colorize('const answer = 42', 'typescript', {})
|
||||
expect(
|
||||
monaco.editor.tokenize('const answer = 42', 'typescript')[0].map((token) => token.type)
|
||||
).toEqual(expect.arrayContaining(['keyword.ts', 'number.ts']))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { stubRuntimeEnvironmentBridge } from './runtime-environment-bridge-fixture'
|
||||
|
||||
type RuntimeSubscriptionCallbacks = {
|
||||
onResponse: (response: unknown) => void
|
||||
}
|
||||
|
||||
describe('remote runtime terminal end verdict', () => {
|
||||
let callbacks: RuntimeSubscriptionCallbacks | null = null
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
callbacks = null
|
||||
stubRuntimeEnvironmentBridge({
|
||||
subscribe: vi.fn(async (_args, nextCallbacks: RuntimeSubscriptionCallbacks) => {
|
||||
callbacks = nextCallbacks
|
||||
queueMicrotask(() => {
|
||||
callbacks?.onResponse({ ok: true, result: { type: 'ready' } })
|
||||
})
|
||||
return { unsubscribe: vi.fn(), sendBinary: vi.fn() }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['an explicit host verdict', { verdict: 'exited' }, 'exited'],
|
||||
['a legacy bare end', {}, 'unverifiable'],
|
||||
['an unknown future verdict', { verdict: 'unknown' }, 'unverifiable']
|
||||
])('maps %s to %s', async (_case, fields, expected) => {
|
||||
const { getRemoteRuntimeTerminalMultiplexer } =
|
||||
await import('./remote-runtime-terminal-multiplexer')
|
||||
const onEnd = vi.fn()
|
||||
const stream = await getRemoteRuntimeTerminalMultiplexer('env-1').subscribeTerminal({
|
||||
terminal: 'terminal-1',
|
||||
client: { id: 'desktop-1', type: 'desktop' },
|
||||
callbacks: { onData: vi.fn(), onSnapshot: vi.fn(), onEnd }
|
||||
})
|
||||
|
||||
callbacks?.onResponse({
|
||||
ok: true,
|
||||
result: { type: 'end', streamId: stream.streamId, ...fields }
|
||||
})
|
||||
|
||||
expect(onEnd).toHaveBeenCalledWith(expected)
|
||||
})
|
||||
})
|
||||
@@ -22,13 +22,6 @@ const notFoundResponse = (id: string): unknown => ({
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
const failedResponse = (id: string): unknown => ({
|
||||
id,
|
||||
ok: false,
|
||||
error: { code: 'permission_denied', message: 'read-only file system' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
const statResponse = (id: string): unknown => ({
|
||||
id,
|
||||
ok: true,
|
||||
@@ -121,66 +114,6 @@ describe('drop staging ignore file', () => {
|
||||
expect(methods.indexOf('files.write')).toBeLessThan(methods.indexOf('files.commitUpload'))
|
||||
})
|
||||
|
||||
it('leaves an existing ignore file alone', async () => {
|
||||
stageOneFile()
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(statResponse('stat-orca-hit'))
|
||||
.mockResolvedValueOnce(statResponse('stat-drops-hit'))
|
||||
.mockResolvedValueOnce(statResponse('stat-ignore-hit'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-file-miss'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
|
||||
await importExternalPathsToRuntime(CONTEXT, ['/Users/me/shot.png'], '/remote/repo/.orca/drops')
|
||||
|
||||
expect(calledMethods().some((call) => call.method === 'files.write')).toBe(false)
|
||||
})
|
||||
|
||||
it('still uploads the drop when the ignore file cannot be written', async () => {
|
||||
stageOneFile()
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(statResponse('stat-orca-hit'))
|
||||
.mockResolvedValueOnce(statResponse('stat-drops-hit'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-ignore-miss'))
|
||||
.mockResolvedValueOnce(failedResponse('write-ignore-denied'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-file-miss'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
|
||||
const { results } = await importExternalPathsToRuntime(
|
||||
CONTEXT,
|
||||
['/Users/me/shot.png'],
|
||||
'/remote/repo/.orca/drops'
|
||||
)
|
||||
|
||||
expect(results).toEqual([expect.objectContaining({ status: 'imported' })])
|
||||
})
|
||||
|
||||
it('recognizes the drops folder of a Windows worktree', async () => {
|
||||
stageOneFile()
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(statResponse('stat-orca-hit'))
|
||||
.mockResolvedValueOnce(statResponse('stat-drops-hit'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-ignore-miss'))
|
||||
.mockResolvedValueOnce(okResponse('write-ignore'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-file-miss'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
|
||||
await importExternalPathsToRuntime(
|
||||
{ ...CONTEXT, worktreePath: 'C:\\Users\\me\\repo' },
|
||||
['/Users/me/shot.png'],
|
||||
'C:\\Users\\me\\repo\\.orca\\drops'
|
||||
)
|
||||
|
||||
expect(calledMethods().filter((call) => call.method === 'files.write')).toEqual([
|
||||
{
|
||||
method: 'files.write',
|
||||
params: expect.objectContaining({ relativePath: '.orca/drops/.gitignore', content: '*\n' })
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
// Why: the ignore step swallows its own errors, and a drop with nothing staged never re-checks.
|
||||
it('reports a host that went stale during the ignore step even when nothing was staged', async () => {
|
||||
fsStageExternalPathsForRuntimeUpload.mockResolvedValue({
|
||||
@@ -207,17 +140,4 @@ describe('drop staging ignore file', () => {
|
||||
})
|
||||
).rejects.toThrow('runtime owner generation changed')
|
||||
})
|
||||
|
||||
it('never writes an ignore file into a folder the user picked', async () => {
|
||||
stageOneFile()
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(statResponse('stat-uploads-hit'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-file-miss'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
|
||||
await importExternalPathsToRuntime(CONTEXT, ['/Users/me/shot.png'], '/remote/repo/uploads')
|
||||
|
||||
expect(calledMethods().some((call) => call.method === 'files.write')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signatur
|
||||
import { advanceRuntimeEnvironmentConnectionGeneration } from '@/store/slices/runtime-status-connection-generation'
|
||||
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
|
||||
import {
|
||||
fsWriteFile,
|
||||
installRuntimeFileClientEnvironment,
|
||||
runtimeEnvironmentCall,
|
||||
runtimeEnvironmentTransportCall,
|
||||
@@ -91,23 +90,6 @@ function writeCalls(): RuntimeRpcRequest[] {
|
||||
}
|
||||
|
||||
describe('writeRuntimeFile disk precondition', () => {
|
||||
it('sends the precondition to a local writer and returns its refusal', async () => {
|
||||
fsWriteFile.mockResolvedValue({ ok: false, diskConflict: 'changed' })
|
||||
|
||||
const result = await writeRuntimeFile(
|
||||
{ target: { kind: 'local' }, worktreeId: 'wt-1', worktreePath: '/repo' },
|
||||
'/repo/file.ts',
|
||||
'edited',
|
||||
undefined,
|
||||
precondition
|
||||
)
|
||||
|
||||
expect(result).toEqual({ ok: false, diskConflict: 'changed' })
|
||||
expect(fsWriteFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ diskPrecondition: precondition })
|
||||
)
|
||||
})
|
||||
|
||||
it('cannot write through an older server reached after a supporting server was probed', async () => {
|
||||
// The shared socket reconnected to an older server between the capability verdict and the send.
|
||||
serveHost({ enforcesPrecondition: true })
|
||||
@@ -131,26 +113,6 @@ describe('writeRuntimeFile disk precondition', () => {
|
||||
expect(unguardedWrites).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('lets a host that enforces the precondition compare at write time', async () => {
|
||||
serveHost({ enforcesPrecondition: true })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: false, diskConflict: 'missing' }))
|
||||
|
||||
const result = await writeRuntimeFile(
|
||||
remoteContext,
|
||||
'/remote/repo/file.ts',
|
||||
'edited',
|
||||
undefined,
|
||||
precondition
|
||||
)
|
||||
|
||||
expect(result).toEqual({ ok: false, diskConflict: 'missing' })
|
||||
expect(writeCalls()).toHaveLength(1)
|
||||
expect(writeCalls()[0]?.params).toMatchObject({ diskPrecondition: precondition })
|
||||
expect(runtimeEnvironmentTransportCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.read' })
|
||||
)
|
||||
})
|
||||
|
||||
it('compares from the client before writing through an older host', async () => {
|
||||
serveHost({ enforcesPrecondition: false, diskContent: 'original' })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: true }))
|
||||
@@ -168,28 +130,6 @@ describe('writeRuntimeFile disk precondition', () => {
|
||||
expect(writeCalls()[0]?.params).not.toHaveProperty('diskPrecondition')
|
||||
})
|
||||
|
||||
it("decides support from the write path's own status probe, one per save", async () => {
|
||||
serveHost({ enforcesPrecondition: false, diskContent: 'original' })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: true }))
|
||||
|
||||
for (let save = 0; save < 3; save += 1) {
|
||||
await writeRuntimeFile(
|
||||
remoteContext,
|
||||
'/remote/repo/file.ts',
|
||||
'edited',
|
||||
undefined,
|
||||
precondition
|
||||
)
|
||||
}
|
||||
|
||||
const statusCalls = runtimeEnvironmentTransportCall.mock.calls.filter(
|
||||
([args]) => args.method === 'status.get'
|
||||
)
|
||||
// One ownership probe per save plus the read client's one-time compatibility probe.
|
||||
expect(statusCalls).toHaveLength(4)
|
||||
expect(writeCalls()).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('refuses without writing when an older host holds newer disk content', async () => {
|
||||
serveHost({ enforcesPrecondition: false, diskContent: 'agent rewrite' })
|
||||
|
||||
@@ -205,22 +145,6 @@ describe('writeRuntimeFile disk precondition', () => {
|
||||
expect(writeCalls()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('skips writing through an older host whose changed disk already holds the text', async () => {
|
||||
serveHost({ enforcesPrecondition: false, diskContent: 'edited' })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: true }))
|
||||
|
||||
const result = await writeRuntimeFile(
|
||||
remoteContext,
|
||||
'/remote/repo/file.ts',
|
||||
'edited',
|
||||
undefined,
|
||||
precondition
|
||||
)
|
||||
|
||||
expect(result).toEqual({ ok: true, unchanged: true })
|
||||
expect(writeCalls()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it.each(['original', 'edited'])(
|
||||
'skips identical content on a v1 guarded host for an automatic save (baseline %s)',
|
||||
async (baseline) => {
|
||||
@@ -235,32 +159,6 @@ describe('writeRuntimeFile disk precondition', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the v1 host guard for non-identical automatic saves after comparing locally', async () => {
|
||||
serveHost({ enforcesPrecondition: true, identicalNoop: false, diskContent: 'original' })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: false, diskConflict: 'changed' }))
|
||||
expect(
|
||||
await writeRuntimeFile(remoteContext, '/remote/repo/file.ts', 'edited', undefined, {
|
||||
...precondition,
|
||||
requireLosslessUtf8: true
|
||||
})
|
||||
).toEqual({ ok: false, diskConflict: 'changed' })
|
||||
expect(writeCalls()[0]?.method).toBe('files.writeIfUnchanged')
|
||||
})
|
||||
|
||||
it('sends an explicit save straight to a v1 guarded host without reading first', async () => {
|
||||
let reads = 0
|
||||
serveHost({
|
||||
enforcesPrecondition: true,
|
||||
identicalNoop: false,
|
||||
diskContent: 'edited',
|
||||
onRead: () => (reads += 1)
|
||||
})
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: true }))
|
||||
await writeRuntimeFile(remoteContext, '/remote/repo/file.ts', 'edited', undefined, precondition)
|
||||
expect(reads).toBe(0)
|
||||
expect(writeCalls()[0]?.method).toBe('files.writeIfUnchanged')
|
||||
})
|
||||
|
||||
it('refuses an ambiguous automatic baseline before sending to a v1 host', async () => {
|
||||
serveHost({ enforcesPrecondition: true, identicalNoop: false, diskContent: 'a�' })
|
||||
expect(
|
||||
@@ -318,36 +216,6 @@ describe('writeRuntimeFile disk precondition', () => {
|
||||
expect(writeCalls()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('reports a file an older host can no longer send as changed', async () => {
|
||||
serveHost({
|
||||
enforcesPrecondition: false,
|
||||
readError: 'File too large: 60.0MB exceeds 50MB limit'
|
||||
})
|
||||
|
||||
const result = await writeRuntimeFile(
|
||||
remoteContext,
|
||||
'/remote/repo/file.ts',
|
||||
'edited',
|
||||
undefined,
|
||||
{
|
||||
...precondition,
|
||||
allowMissing: true
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({ ok: false, diskConflict: 'changed' })
|
||||
expect(writeCalls()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('binds a guarded write to the runtime whose capabilities were probed', async () => {
|
||||
serveHost({ enforcesPrecondition: true })
|
||||
runtimeEnvironmentCall.mockResolvedValue(rpcOk({ ok: true }))
|
||||
|
||||
await writeRuntimeFile(remoteContext, '/remote/repo/file.ts', 'edited', undefined, precondition)
|
||||
|
||||
expect(writeCalls()[0]).toMatchObject({ expectedEnvironmentRuntimeId: 'remote-runtime' })
|
||||
})
|
||||
|
||||
it('refuses to send when the runtime connection changes after the probe', async () => {
|
||||
serveHost({
|
||||
enforcesPrecondition: false,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
decodeLegacyQuickOpenInventory,
|
||||
pruneLegacyInventoryCache
|
||||
} from './runtime-legacy-inventory-budget'
|
||||
import { decodeLegacyQuickOpenInventory } from './runtime-legacy-inventory-budget'
|
||||
|
||||
function inventory(count = 1) {
|
||||
return {
|
||||
@@ -37,28 +34,4 @@ describe('complete legacy inventory retention', () => {
|
||||
value.files[0].relativePath = 'x'.repeat(65_537)
|
||||
expect(() => decodeLegacyQuickOpenInventory(value)).toThrow('too large')
|
||||
})
|
||||
it.each([
|
||||
null,
|
||||
{ files: [] },
|
||||
{ ...inventory(), totalCount: 0 },
|
||||
{ ...inventory(), files: [{ relativePath: 12 }] }
|
||||
])('rejects malformed responses %j', (value) => {
|
||||
expect(() => decodeLegacyQuickOpenInventory(value)).toThrow('Invalid remote file inventory')
|
||||
})
|
||||
it('evicts in access order under a shared byte allowance and releases removed accounting', () => {
|
||||
const cache = new Map([
|
||||
['one', { retainedBytes: 60 }],
|
||||
['two', { retainedBytes: 60 }]
|
||||
])
|
||||
pruneLegacyInventoryCache(cache, 8, 100)
|
||||
expect([...cache.keys()]).toEqual(['two'])
|
||||
cache.delete('two')
|
||||
cache.set('three', { retainedBytes: 100 })
|
||||
pruneLegacyInventoryCache(cache, 8, 100)
|
||||
expect([...cache.keys()]).toEqual(['three'])
|
||||
cache.set('three', { retainedBytes: 10 })
|
||||
cache.set('owner-changed', { retainedBytes: 90 })
|
||||
pruneLegacyInventoryCache(cache, 8, 100)
|
||||
expect(cache.size).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -164,20 +164,6 @@ describe('legacy Quick Open inventory expiry', () => {
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('starts the settled deadline when the inventory arrives', async () => {
|
||||
const load = queueLoad()
|
||||
const pending = search()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
load.resolve(listing())
|
||||
await pending
|
||||
vi.advanceTimersByTime(29_999)
|
||||
expect(cached()).toBe(true)
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(cached()).toBe(false)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('sweeps settled responses while leaving shared pending loads valid', async () => {
|
||||
call.mockResolvedValueOnce(listing('settled'))
|
||||
await search('settled')
|
||||
@@ -286,17 +272,6 @@ describe('legacy Quick Open inventory expiry', () => {
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('retries failed requests without keeping an expiry timer for their response', async () => {
|
||||
call.mockRejectedValueOnce(new Error('host failed'))
|
||||
await expect(search()).rejects.toThrow('host failed')
|
||||
expect(cached()).toBe(false)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
call.mockResolvedValueOnce(listing())
|
||||
await search()
|
||||
expect(cached()).toBe(true)
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('replaces a longer armed deadline after a backward clock change and a new settlement', async () => {
|
||||
const timeout = vi.spyOn(globalThis, 'setTimeout')
|
||||
call.mockImplementation(async () => listing())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readRuntimeFileContent, readRuntimeFilePreview } from './runtime-file-read-client'
|
||||
import { readRuntimeFileContent } from './runtime-file-read-client'
|
||||
import { writeRuntimeFile } from './runtime-file-mutation-client'
|
||||
import {
|
||||
installRuntimeFileClientEnvironment,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
type RuntimeRpcRequest
|
||||
} from './runtime-file-client-test-harness'
|
||||
import { REMOTE_FILE_CHANGED_DURING_READ_MESSAGE } from './runtime-paged-file-read'
|
||||
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
|
||||
|
||||
installRuntimeFileClientEnvironment()
|
||||
|
||||
@@ -168,19 +167,6 @@ describe('remote reads larger than one RPC reply', () => {
|
||||
expect(methodCalls('files.readChunk').length > 0).toBe(sizeMb * MB > PREVIEW_CAP)
|
||||
})
|
||||
|
||||
it('pages an image preview too large for one reply', async () => {
|
||||
const bytes = patternedBytes(5 * MB)
|
||||
serveRemoteFile('shot.png', { bytes })
|
||||
|
||||
const result = await readRuntimeFilePreview(
|
||||
{ target: remote('shot.png').target, worktreeId: 'wt-1', worktreePath: '/remote/repo' },
|
||||
'/remote/repo/shot.png'
|
||||
)
|
||||
|
||||
expect(result).toMatchObject({ isBinary: true, isImage: true, mimeType: 'image/png' })
|
||||
expect(Buffer.from(result.content, 'base64').equals(bytes)).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves a large PDF alone for readers that discard binary content', async () => {
|
||||
serveRemoteFile('paper.pdf', { bytes: patternedBytes(5 * MB) })
|
||||
|
||||
@@ -256,43 +242,6 @@ describe('remote reads larger than one RPC reply', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('pins every call of one paged read to the pairing it started on', async () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 7 }])
|
||||
const bytes = htmlBytes(2 * MB)
|
||||
serveRemoteFile('report.html', {
|
||||
bytes,
|
||||
readChunk: (params, file) => {
|
||||
// A re-pair mid-read must not retarget the remaining chunks.
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 8 }])
|
||||
return sliceChunk(params, file)
|
||||
}
|
||||
})
|
||||
|
||||
await readRuntimeFileContent(remote('report.html'))
|
||||
|
||||
const paged = [...methodCalls('files.stat'), ...methodCalls('files.readChunk')]
|
||||
expect(paged.length).toBeGreaterThan(2)
|
||||
expect(paged.every((call) => call.expectedEnvironmentPairingRevision === 7)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the pairing of the first read when a re-pair lands during it', async () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 7 }])
|
||||
serveRemoteFile('report.html', { bytes: htmlBytes(2 * MB) })
|
||||
const serve = runtimeEnvironmentCall.getMockImplementation()
|
||||
runtimeEnvironmentCall.mockImplementation((request: RuntimeRpcRequest) => {
|
||||
if (request.method === 'files.read') {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 8 }])
|
||||
}
|
||||
return serve?.(request)
|
||||
})
|
||||
|
||||
await readRuntimeFileContent(remote('report.html'))
|
||||
|
||||
const calls = runtimeEnvironmentCall.mock.calls.map(([request]) => request)
|
||||
expect(calls.length).toBeGreaterThan(3)
|
||||
expect(calls.every((call) => call.expectedEnvironmentPairingRevision === 7)).toBe(true)
|
||||
})
|
||||
|
||||
it('stops when the host never reports EOF', async () => {
|
||||
const bytes = htmlBytes(2 * MB)
|
||||
serveRemoteFile('report.html', {
|
||||
@@ -325,13 +274,6 @@ describe('remote saves larger than one request frame', () => {
|
||||
worktreePath: '/remote/repo'
|
||||
}
|
||||
|
||||
it('refuses a save the host would answer by closing the connection', async () => {
|
||||
await expect(
|
||||
writeRuntimeFile(context, '/remote/repo/report.html', htmlBytes(2 * MB).toString('utf8'))
|
||||
).rejects.toThrow('File is too large to save to a remote Orca server')
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('counts JSON escaping, not just characters', async () => {
|
||||
await expect(
|
||||
writeRuntimeFile(context, '/remote/repo/quotes.txt', '"'.repeat(400 * 1024))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { expect, it } from 'vitest'
|
||||
import { searchRuntimeFilePaths, listRuntimeFiles } from './runtime-file-search-client'
|
||||
import {
|
||||
installRuntimeFileClientEnvironment,
|
||||
@@ -7,30 +7,6 @@ import {
|
||||
} from './runtime-file-client-test-harness'
|
||||
installRuntimeFileClientEnvironment()
|
||||
|
||||
it('accepts a future compatible search version rather than falling back on exact-version inequality', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'compat',
|
||||
ok: true,
|
||||
_meta: { runtimeId: 'host' },
|
||||
result: {
|
||||
files: [{ relativePath: 'apps/late/.env' }],
|
||||
truncated: false,
|
||||
quickOpenSearchVersion: 4
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
searchRuntimeFilePaths(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'future-host',
|
||||
worktreePath: '/host/repo'
|
||||
},
|
||||
{ query: '.env late' }
|
||||
)
|
||||
).resolves.toEqual({ files: ['apps/late/.env'], truncated: false })
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('validates default-policy recent paths on a version-two host through complete legacy inventory', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
@@ -66,34 +42,6 @@ it('validates default-policy recent paths on a version-two host through complete
|
||||
expect(fsListFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends bounded candidates only after a compatible host advertises their semantics', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
id: 'compat',
|
||||
ok: true,
|
||||
_meta: { runtimeId: 'host' },
|
||||
result:
|
||||
method === 'files.searchPaths'
|
||||
? { files: [], truncated: false, quickOpenSearchVersion: 3 }
|
||||
: ['src/recent.ts']
|
||||
})
|
||||
)
|
||||
await expect(
|
||||
listRuntimeFiles(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'recent-new',
|
||||
worktreePath: '/host/repo'
|
||||
},
|
||||
{ rootPath: '/host/repo', candidatePaths: ['src/recent.ts'], maxResults: 1 }
|
||||
)
|
||||
).resolves.toEqual(['src/recent.ts'])
|
||||
expect(runtimeEnvironmentCall.mock.calls[1][0]).toMatchObject({
|
||||
method: 'files.listAll',
|
||||
params: { worktree: 'id:recent-new', candidatePaths: ['src/recent.ts'], maxResults: 1 }
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses to infer recent eligibility from a truncated old-host inventory', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
@@ -125,179 +73,6 @@ it('refuses to infer recent eligibility from a truncated old-host inventory', as
|
||||
expect(fsListFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['missing-search', 'old-search'] as const)(
|
||||
'translates only unavailable inventory errors after %s',
|
||||
async (route) => {
|
||||
const context = {
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'fallback-errors',
|
||||
worktreePath: '/host/repo'
|
||||
}
|
||||
for (const code of ['method_not_found', 'forbidden', 'remote_runtime_unavailable']) {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
id: 'compat',
|
||||
_meta: { runtimeId: 'host' },
|
||||
...(method === 'files.searchPaths' && route === 'old-search'
|
||||
? { ok: true, result: { files: [], truncated: false, quickOpenSearchVersion: 1 } }
|
||||
: {
|
||||
ok: false,
|
||||
error: {
|
||||
code: method === 'files.searchPaths' ? 'method_not_found' : code,
|
||||
message: 'inventory failed'
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
const search = searchRuntimeFilePaths(context, { query: 'two terms' })
|
||||
await (route === 'old-search'
|
||||
? expect(search).resolves.toEqual({ files: [], truncated: false })
|
||||
: expect(search).rejects.toThrow(
|
||||
code === 'method_not_found' ? 'Update the remote host' : 'inventory failed'
|
||||
))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves raw errors when joining a cached pending legacy inventory', async () => {
|
||||
const context = {
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'cached-errors',
|
||||
worktreePath: '/host/repo'
|
||||
}
|
||||
const inventory = Promise.withResolvers<unknown>()
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
method === 'files.list'
|
||||
? inventory.promise
|
||||
: Promise.resolve({
|
||||
id: 'compat',
|
||||
ok: false,
|
||||
_meta: { runtimeId: 'host' },
|
||||
error: { code: 'method_not_found', message: 'search unavailable' }
|
||||
})
|
||||
)
|
||||
const first = searchRuntimeFilePaths(context, { query: 'two terms' })
|
||||
const translated = expect(first).rejects.toThrow('Update the remote host')
|
||||
await vi.waitFor(() =>
|
||||
expect(runtimeEnvironmentCall.mock.calls.map(([request]) => request.method)).toContain(
|
||||
'files.list'
|
||||
)
|
||||
)
|
||||
const cached = searchRuntimeFilePaths(context, { query: 'other terms' })
|
||||
const raw = expect(cached).rejects.toThrow('raw cached failure')
|
||||
inventory.resolve({
|
||||
id: 'compat',
|
||||
ok: false,
|
||||
_meta: { runtimeId: 'host' },
|
||||
error: { code: 'method_not_found', message: 'raw cached failure' }
|
||||
})
|
||||
await Promise.all([translated, raw])
|
||||
expect(runtimeEnvironmentCall.mock.calls.map(([request]) => request.method)).toEqual([
|
||||
'files.searchPaths',
|
||||
'files.list'
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['method_not_found', 'file_inventory_capacity', 'remote_runtime_unavailable'])(
|
||||
'does not require optional inventory to return existing old-host punctuation matches (%s)',
|
||||
async (code) => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
id: 'compat',
|
||||
_meta: { runtimeId: 'host' },
|
||||
...(method === 'files.searchPaths'
|
||||
? {
|
||||
ok: true,
|
||||
result: {
|
||||
rootPath: '/host/repo',
|
||||
files: [{ relativePath: 'late/package-lock.json' }],
|
||||
truncated: false,
|
||||
quickOpenSearchVersion: 1
|
||||
}
|
||||
}
|
||||
: { ok: false, error: { code, message: 'inventory unavailable' } })
|
||||
})
|
||||
)
|
||||
await expect(
|
||||
searchRuntimeFilePaths(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: `punctuation-${code}`,
|
||||
worktreePath: '/host/repo'
|
||||
},
|
||||
{ query: 'package-lock' }
|
||||
)
|
||||
).resolves.toEqual({ files: ['late/package-lock.json'], truncated: false })
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledOnce()
|
||||
}
|
||||
)
|
||||
|
||||
it.each([{ includeIgnored: false }, { followSymlinks: true }])(
|
||||
'uses discovery preferences on a version-two paired host: %j',
|
||||
async (options) => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'compat',
|
||||
_meta: { runtimeId: 'host' },
|
||||
ok: true,
|
||||
result: {
|
||||
rootPath: '/repo',
|
||||
files: [{ relativePath: 'src/file.ts' }],
|
||||
truncated: false,
|
||||
quickOpenSearchVersion: 2
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
searchRuntimeFilePaths(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'v2-options',
|
||||
worktreePath: '/repo'
|
||||
},
|
||||
{ query: 'file', ...options }
|
||||
)
|
||||
).resolves.toEqual({ files: ['src/file.ts'], truncated: false })
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps inherited ignored-file visibility from disabling an older paired host', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
id: 'compat',
|
||||
_meta: { runtimeId: 'host' },
|
||||
ok: true,
|
||||
result:
|
||||
method === 'files.listAll'
|
||||
? ['src/file.ts']
|
||||
: {
|
||||
rootPath: '/repo',
|
||||
files: [{ relativePath: 'src/file.ts' }],
|
||||
truncated: false,
|
||||
quickOpenSearchVersion: 1
|
||||
}
|
||||
})
|
||||
)
|
||||
const context = {
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'legacy-ignored',
|
||||
worktreePath: '/repo'
|
||||
}
|
||||
await expect(
|
||||
searchRuntimeFilePaths(context, {
|
||||
query: 'file',
|
||||
includeIgnored: false,
|
||||
allowLegacyIncludeIgnored: true
|
||||
})
|
||||
).resolves.toEqual({ files: ['src/file.ts'], truncated: false })
|
||||
await expect(
|
||||
listRuntimeFiles(context, {
|
||||
rootPath: '/repo',
|
||||
includeIgnored: false,
|
||||
allowLegacyIncludeIgnored: true
|
||||
})
|
||||
).resolves.toEqual(['src/file.ts'])
|
||||
expect(runtimeEnvironmentCall.mock.calls.at(-1)?.[0].params).not.toHaveProperty('includeIgnored')
|
||||
})
|
||||
|
||||
it('does not reuse unfiltered recent inventory after enabling supported ignore filtering', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(({ method }) =>
|
||||
Promise.resolve({
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { writeRuntimeTerminalArtifactFile } from './runtime-terminal-artifact-write'
|
||||
import {
|
||||
fsWriteFile,
|
||||
runtimeEnvironmentCall,
|
||||
installRuntimeFileClientEnvironment,
|
||||
type RuntimeRpcRequest
|
||||
} from './runtime-file-client-test-harness'
|
||||
import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signature'
|
||||
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
|
||||
|
||||
installRuntimeFileClientEnvironment()
|
||||
|
||||
@@ -55,23 +53,6 @@ function callsTo(method: string): RuntimeRpcRequest[] {
|
||||
}
|
||||
|
||||
describe('saving a granted host file outside every workspace', () => {
|
||||
it('writes through the grant when the host still holds the opened text', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method }) =>
|
||||
method === 'files.readTerminalArtifact' ? read('# old') : ok({ ok: true })
|
||||
)
|
||||
|
||||
await expect(save('# new', '# old')).resolves.toEqual({ ok: true })
|
||||
|
||||
expect(callsTo('files.writeTerminalArtifact')[0]?.params).toEqual({
|
||||
worktree: 'id:wt-1',
|
||||
grantId: 'g-1',
|
||||
absolutePath: PATH,
|
||||
content: '# new'
|
||||
})
|
||||
// Why: a host path must never become a write of this computer's same path.
|
||||
expect(fsWriteFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses to overwrite text that changed on the host', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method }) =>
|
||||
method === 'files.readTerminalArtifact' ? read('# theirs') : ok({ ok: true })
|
||||
@@ -81,31 +62,6 @@ describe('saving a granted host file outside every workspace', () => {
|
||||
expect(callsTo('files.writeTerminalArtifact')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renews an expired grant and still checks the host text first', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method, params }) => {
|
||||
if (method === 'files.resolveTerminalPath') {
|
||||
return granted('g-2')
|
||||
}
|
||||
const grantId = grantIdOf(params)
|
||||
if (grantId === 'g-1') {
|
||||
return refused('terminal_file_grant_expired')
|
||||
}
|
||||
return method === 'files.readTerminalArtifact' ? read('# old') : ok({ ok: true })
|
||||
})
|
||||
|
||||
const onGrantRenewed = vi.fn()
|
||||
await expect(save('# new', '# old', onGrantRenewed)).resolves.toEqual({ ok: true })
|
||||
|
||||
expect(onGrantRenewed).toHaveBeenCalledWith('g-2')
|
||||
expect(callsTo('files.resolveTerminalPath')[0]?.params).toEqual({
|
||||
worktree: 'id:wt-1',
|
||||
pathText: PATH
|
||||
})
|
||||
expect(callsTo('files.writeTerminalArtifact').map((c) => c.params)).toEqual([
|
||||
expect.objectContaining({ grantId: 'g-2', content: '# new' })
|
||||
])
|
||||
})
|
||||
|
||||
// Why: a host change stales the grant; the renewed read must still raise the conflict banner.
|
||||
it('reports a host-side change behind a stale grant as a conflict', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method, params }) => {
|
||||
@@ -122,38 +78,6 @@ describe('saving a granted host file outside every workspace', () => {
|
||||
expect(callsTo('files.writeTerminalArtifact')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('reports a file deleted on its host as missing', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async () =>
|
||||
refused("ENOENT: no such file or directory, open '/private/tmp/handoff.md'")
|
||||
)
|
||||
|
||||
await expect(save('# new', '# old')).resolves.toEqual({ ok: false, diskConflict: 'missing' })
|
||||
expect(callsTo('files.writeTerminalArtifact')).toHaveLength(0)
|
||||
})
|
||||
|
||||
// Why: a same-id re-pair mid-save must not let a renewed grant write the replacement host's file.
|
||||
it('pins every step of a save to the pairing it started on', async () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 7 }])
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method, params }) => {
|
||||
if (method === 'files.resolveTerminalPath') {
|
||||
return granted('g-2')
|
||||
}
|
||||
if (grantIdOf(params) === 'g-1') {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 2, pairingRevision: 8 }])
|
||||
return refused('terminal_file_grant_expired')
|
||||
}
|
||||
return method === 'files.readTerminalArtifact' ? read('# old') : ok({ ok: true })
|
||||
})
|
||||
|
||||
await save('# new', '# old').catch(() => undefined)
|
||||
|
||||
const revisions = runtimeEnvironmentCall.mock.calls
|
||||
.map(([call]) => call)
|
||||
.filter((call) => call.method !== 'status.get')
|
||||
.map((call) => call.expectedEnvironmentPairingRevision)
|
||||
expect(new Set(revisions)).toEqual(new Set([7]))
|
||||
})
|
||||
|
||||
// Why: a quoted path can spell "not found"; only the error itself proves absence.
|
||||
it('surfaces a permission failure on a path that mentions not found', async () => {
|
||||
runtimeEnvironmentCall.mockImplementation(async () =>
|
||||
@@ -182,20 +106,6 @@ describe('saving a granted host file outside every workspace', () => {
|
||||
expect(callsTo('files.writeTerminalArtifact')).toHaveLength(0)
|
||||
})
|
||||
|
||||
// Why: a committed write whose reply was lost must still settle the save, or the next one conflicts.
|
||||
it('treats a lost reply as saved when the host already holds the new text', async () => {
|
||||
let written = false
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method }) => {
|
||||
if (method === 'files.writeTerminalArtifact') {
|
||||
written = true
|
||||
throw new Error('Request timed out')
|
||||
}
|
||||
return read(written ? '# new' : '# old')
|
||||
})
|
||||
|
||||
await expect(save('# new', '# old')).resolves.toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('reads back through a fresh grant when the dropped reply also revoked the old one', async () => {
|
||||
let written = false
|
||||
runtimeEnvironmentCall.mockImplementation(async ({ method, params }) => {
|
||||
|
||||
@@ -11,10 +11,6 @@ import { clearRuntimeCompatibilityCacheForTests } from './runtime-rpc-client'
|
||||
import { TERMINAL_INPUT_MAX_BYTES } from '../../../shared/terminal-input'
|
||||
import { useAppStore } from '../store'
|
||||
import type { RemoteForegroundEvidence } from '../../../shared/foreground-process-evidence'
|
||||
import {
|
||||
clientOnlyUnverifiableInspection,
|
||||
type ClientOnlyUnverifiableInspection
|
||||
} from '../../../shared/terminal-process-inspection'
|
||||
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const PANE_KEY = `tab-1:${LEAF_ID}`
|
||||
@@ -47,8 +43,6 @@ describe('runtime terminal owner routing', () => {
|
||||
const runtimeTransportCall = vi.fn()
|
||||
const localWrite = vi.fn()
|
||||
const localWriteAccepted = vi.fn()
|
||||
const localForeground = vi.fn()
|
||||
const localHasChildren = vi.fn()
|
||||
const localInspect = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -74,8 +68,6 @@ describe('runtime terminal owner routing', () => {
|
||||
pty: {
|
||||
write: localWrite,
|
||||
writeAccepted: localWriteAccepted,
|
||||
getForegroundProcess: localForeground,
|
||||
hasChildProcesses: localHasChildren,
|
||||
inspectProcess: localInspect
|
||||
}
|
||||
}
|
||||
@@ -105,81 +97,6 @@ describe('runtime terminal owner routing', () => {
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toBe(123)
|
||||
})
|
||||
|
||||
it('sends input through the PTY owning environment instead of the active one', async () => {
|
||||
expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.send',
|
||||
params: {
|
||||
terminal: 'terminal-1',
|
||||
text: 'x',
|
||||
client: { id: 'orca-desktop', type: 'desktop' }
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
expect(localWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('inspects the PTY owning environment instead of the active one', async () => {
|
||||
await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-1')).resolves.toMatchObject({
|
||||
foregroundProcess: 'bash',
|
||||
hasChildProcesses: true
|
||||
})
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.inspectProcess',
|
||||
params: { terminal: 'terminal-1' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(localForeground).not.toHaveBeenCalled()
|
||||
expect(localHasChildren).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Forward close guards’ real-process-read option or hosts return unverifiable and confirm every idle remote Windows close (#18591).
|
||||
it('forwards scanChildProcesses to the PTY owning environment', async () => {
|
||||
await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1', { scanChildProcesses: true })
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.inspectProcess',
|
||||
params: { terminal: 'terminal-1', scanChildProcesses: true },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards scanChildProcesses alongside the incarnation fence', async () => {
|
||||
await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1', {
|
||||
expectedIncarnationId: 'incarnation-1',
|
||||
scanChildProcesses: true
|
||||
})
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.inspectProcess',
|
||||
params: {
|
||||
terminal: 'terminal-1',
|
||||
expectedIncarnationId: 'incarnation-1',
|
||||
scanChildProcesses: true
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
||||
it('omits scanChildProcesses when the caller is only polling', async () => {
|
||||
await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1')
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.inspectProcess',
|
||||
params: { terminal: 'terminal-1' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
||||
it('maps an old host inspection to client-only unverifiable', async () => {
|
||||
runtimeCall.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -197,22 +114,6 @@ describe('runtime terminal owner routing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses strict main-process inspection for a direct SSH PTY', async () => {
|
||||
localInspect.mockResolvedValue({
|
||||
foregroundProcess: 'codex',
|
||||
hasChildProcesses: true,
|
||||
foregroundProcessEvidence: liveEvidence('pty-1', 'codex')
|
||||
})
|
||||
|
||||
await expect(inspectRuntimeTerminalProcess('ssh:host@@pty-1')).resolves.toMatchObject({
|
||||
foregroundProcess: 'codex',
|
||||
hasChildProcesses: true
|
||||
})
|
||||
expect(localInspect).toHaveBeenCalledExactlyOnceWith('ssh:host@@pty-1')
|
||||
expect(localForeground).not.toHaveBeenCalled()
|
||||
expect(localHasChildren).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps an old direct SSH host to client-only unverifiable', async () => {
|
||||
localInspect.mockResolvedValue({
|
||||
foregroundProcess: null,
|
||||
@@ -261,23 +162,6 @@ describe('runtime terminal owner routing', () => {
|
||||
).resolves.toMatchObject({ verdict: 'unverifiable', reason: 'timeout' })
|
||||
})
|
||||
|
||||
it('keeps client-only unverifiable free of host metadata', () => {
|
||||
type HostFieldsCannotBeConstructed = ClientOnlyUnverifiableInspection extends {
|
||||
authorityGeneration?: never
|
||||
observationEpoch?: never
|
||||
capturedAgeMs?: never
|
||||
ptyId?: never
|
||||
ptyIncarnationId?: never
|
||||
}
|
||||
? true
|
||||
: false
|
||||
const typeProof: HostFieldsCannotBeConstructed = true
|
||||
expect(typeProof).toBe(true)
|
||||
const result = clientOnlyUnverifiableInspection('transport_loss')
|
||||
expect(result).not.toHaveProperty('authorityGeneration')
|
||||
expect(result).not.toHaveProperty('foregroundProcessEvidence')
|
||||
})
|
||||
|
||||
it('still throws an unclassified programming error', async () => {
|
||||
runtimeCall.mockRejectedValueOnce(new Error('inspection invariant violated'))
|
||||
await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-bug')).rejects.toThrow(
|
||||
@@ -285,33 +169,6 @@ describe('runtime terminal owner routing', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('records accepted fire-and-forget runtime input against the owning pane key', async () => {
|
||||
runtimeCall.mockResolvedValue({
|
||||
ok: true,
|
||||
result: { send: { handle: 'terminal-1', accepted: true, bytesWritten: 1 } },
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
useAppStore.setState({
|
||||
settings: { experimentalAgentHibernation: true } as never,
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'remote:env-1@@terminal-1' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toEqual(
|
||||
expect.any(Number)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('attributes a delayed runtime acknowledgement to the current layout owner', async () => {
|
||||
const pendingSend = Promise.withResolvers<{
|
||||
ok: true
|
||||
@@ -357,75 +214,6 @@ describe('runtime terminal owner routing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not record declined fire-and-forget runtime input', async () => {
|
||||
runtimeCall.mockResolvedValue({
|
||||
ok: true,
|
||||
result: { send: { handle: 'terminal-1', accepted: false, bytesWritten: 0 } },
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
useAppStore.setState({
|
||||
settings: { experimentalAgentHibernation: true } as never,
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'remote:env-1@@terminal-1' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(runtimeCall).toHaveBeenCalled()
|
||||
})
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reports stale remote terminal handles as rejected during verified send', async () => {
|
||||
runtimeCall.mockResolvedValue({
|
||||
ok: false,
|
||||
error: { code: 'terminal_handle_stale', message: 'terminal_handle_stale' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified('remote:env-1@@terminal-stale', 'x', 'driving')
|
||||
).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('reports declined remote terminal sends as rejected during verified send', async () => {
|
||||
runtimeCall.mockResolvedValue({
|
||||
ok: true,
|
||||
result: { send: { handle: 'terminal-1', accepted: false, bytesWritten: 0 } },
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified('remote:env-1@@terminal-1', 'x', 'driving')
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'terminal.send',
|
||||
params: {
|
||||
terminal: 'terminal-1',
|
||||
text: 'x',
|
||||
client: { id: 'orca-desktop', type: 'desktop' }
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
||||
it('uses accepted local writes for verified input', async () => {
|
||||
localWriteAccepted.mockResolvedValue(true)
|
||||
|
||||
await expect(sendRuntimePtyInputVerified('local-pty', 'x', 'driving')).resolves.toBe(true)
|
||||
|
||||
expect(localWriteAccepted).toHaveBeenCalledWith('local-pty', 'x', 'driving')
|
||||
expect(localWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects oversized fire-and-forget local input before IPC writes', () => {
|
||||
const text = 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1)
|
||||
|
||||
@@ -435,15 +223,6 @@ describe('runtime terminal owner routing', () => {
|
||||
expect(localWriteAccepted).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects oversized fire-and-forget remote input before runtime RPC', () => {
|
||||
const text = 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1)
|
||||
|
||||
expect(sendRuntimePtyInput('remote:env-1@@terminal-1', text, 'driving')).toBe(false)
|
||||
|
||||
expect(runtimeTransportCall).not.toHaveBeenCalled()
|
||||
expect(localWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('yields while validating large fire-and-forget local input before IPC writes', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
@@ -632,110 +411,4 @@ describe('runtime terminal owner routing', () => {
|
||||
]
|
||||
).toBe(1_000_000)
|
||||
})
|
||||
|
||||
it('reindexes a PTY when the immutable layout identity changes', () => {
|
||||
const nextLeafId = '22222222-2222-4222-8222-222222222222'
|
||||
useAppStore.setState({
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'rebound-pty' }
|
||||
}
|
||||
}
|
||||
})
|
||||
recordRuntimeTerminalInputForPtyId('rebound-pty', 123)
|
||||
|
||||
useAppStore.setState({
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-2': {
|
||||
root: { type: 'leaf', leafId: nextLeafId },
|
||||
activeLeafId: nextLeafId,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [nextLeafId]: 'rebound-pty' }
|
||||
}
|
||||
}
|
||||
})
|
||||
recordRuntimeTerminalInputForPtyId('rebound-pty', 1_000)
|
||||
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({
|
||||
[PANE_KEY]: 123,
|
||||
[`tab-2:${nextLeafId}`]: 1_000
|
||||
})
|
||||
})
|
||||
|
||||
it('reindexes stale owners without caching misses within the same layout identity', () => {
|
||||
const nextLeafId = '22222222-2222-4222-8222-222222222222'
|
||||
useAppStore.setState({
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'rebound-pty' }
|
||||
}
|
||||
}
|
||||
})
|
||||
const layouts = useAppStore.getState().terminalLayoutsByTabId
|
||||
recordRuntimeTerminalInputForPtyId('rebound-pty', 123)
|
||||
recordRuntimeTerminalInputForPtyId('late-pty', 456)
|
||||
|
||||
const firstLayout = layouts['tab-1']
|
||||
expect(firstLayout).toBeDefined()
|
||||
if (!firstLayout) {
|
||||
return
|
||||
}
|
||||
firstLayout.ptyIdsByLeafId = Object.create({ [LEAF_ID]: 'rebound-pty' })
|
||||
Object.setPrototypeOf(layouts, { 'tab-1': firstLayout })
|
||||
delete layouts['tab-1']
|
||||
layouts['tab-2'] = {
|
||||
root: { type: 'leaf', leafId: nextLeafId },
|
||||
activeLeafId: nextLeafId,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [nextLeafId]: 'rebound-pty', [LEAF_ID]: 'late-pty' }
|
||||
}
|
||||
recordRuntimeTerminalInputForPtyId('rebound-pty', 1_000)
|
||||
recordRuntimeTerminalInputForPtyId('late-pty', 2_000)
|
||||
|
||||
expect(useAppStore.getState().terminalLayoutsByTabId).toBe(layouts)
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({
|
||||
[PANE_KEY]: 123,
|
||||
[`tab-2:${nextLeafId}`]: 1_000,
|
||||
[`tab-2:${LEAF_ID}`]: 2_000
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves a malformed first match instead of routing a duplicate PTY', () => {
|
||||
const nextLeafId = '22222222-2222-4222-8222-222222222222'
|
||||
useAppStore.setState({
|
||||
terminalLayoutsByTabId: {
|
||||
'legacy:tab': {
|
||||
root: { type: 'leaf', leafId: LEAF_ID },
|
||||
activeLeafId: LEAF_ID,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_ID]: 'duplicate-pty' }
|
||||
},
|
||||
'tab-2': {
|
||||
root: { type: 'leaf', leafId: nextLeafId },
|
||||
activeLeafId: nextLeafId,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [nextLeafId]: 'duplicate-pty' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recordRuntimeTerminalInputForPtyId('duplicate-pty', 123)
|
||||
|
||||
expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({})
|
||||
})
|
||||
|
||||
it('reports success after fallback fire-and-forget writes when local acceptance cannot be verified', async () => {
|
||||
localWriteAccepted.mockResolvedValue(false)
|
||||
|
||||
await expect(sendRuntimePtyInputVerified('local-pty', 'x', 'driving')).resolves.toBe(true)
|
||||
|
||||
expect(localWriteAccepted).toHaveBeenCalledWith('local-pty', 'x', 'driving')
|
||||
expect(localWrite).toHaveBeenCalledWith('local-pty', 'x', 'driving')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,36 +11,10 @@ import {
|
||||
_getRemoteRuntimeTerminalMultiplexerCountForTest,
|
||||
resetRemoteRuntimeTerminalMultiplexersForTests
|
||||
} from './remote-runtime-terminal-multiplexer'
|
||||
import {
|
||||
getRemoteRuntimePtyEnvironmentId,
|
||||
getRemoteRuntimeTerminalHandle,
|
||||
subscribeToRuntimeTerminalData
|
||||
} from './runtime-terminal-stream'
|
||||
import { toRemoteRuntimePtyId } from '../../../shared/remote-runtime/remote-runtime-pty-id'
|
||||
import { subscribeToRuntimeTerminalData } from './runtime-terminal-stream'
|
||||
|
||||
import { stubRuntimeEnvironmentBridge } from './runtime-environment-bridge-fixture'
|
||||
|
||||
describe('remote runtime terminal ids', () => {
|
||||
it('encodes and decodes the owning runtime environment', () => {
|
||||
const ptyId = toRemoteRuntimePtyId('terminal:one', 'env-1')
|
||||
|
||||
expect(ptyId).toBe('remote:env-1@@terminal%3Aone')
|
||||
expect(getRemoteRuntimePtyEnvironmentId(ptyId)).toBe('env-1')
|
||||
expect(getRemoteRuntimeTerminalHandle(ptyId)).toBe('terminal:one')
|
||||
})
|
||||
|
||||
it('keeps legacy remote ids readable', () => {
|
||||
expect(getRemoteRuntimePtyEnvironmentId('remote:terminal-1')).toBeNull()
|
||||
expect(getRemoteRuntimeTerminalHandle('remote:terminal-1')).toBe('terminal-1')
|
||||
})
|
||||
|
||||
it('treats malformed encoded ids as invalid instead of throwing', () => {
|
||||
const malformed = 'remote:%E0%A4%A@@terminal-1'
|
||||
|
||||
expect(getRemoteRuntimePtyEnvironmentId(malformed)).toBeNull()
|
||||
expect(getRemoteRuntimeTerminalHandle(malformed)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('remote runtime terminal data subscriptions', () => {
|
||||
const runtimeSubscribe = vi.fn()
|
||||
const sendBinary = vi.fn()
|
||||
@@ -347,168 +321,6 @@ describe('remote runtime terminal multiplex ACK gate', () => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('holds and releases ACKs for selected remote terminal streams only', async () => {
|
||||
const { getRemoteRuntimeTerminalMultiplexer, resetRemoteRuntimeTerminalMultiplexersForTests } =
|
||||
await import('./remote-runtime-terminal-multiplexer')
|
||||
resetRemoteRuntimeTerminalMultiplexersForTests()
|
||||
|
||||
const multiplexer = getRemoteRuntimeTerminalMultiplexer('env-ack-gate')
|
||||
const heldTerminal = await multiplexer.subscribeTerminal({
|
||||
terminal: 'terminal-held',
|
||||
client: { id: 'desktop-held', type: 'desktop' },
|
||||
callbacks: {
|
||||
onData: vi.fn(),
|
||||
onSnapshot: vi.fn()
|
||||
}
|
||||
})
|
||||
const liveTerminal = await multiplexer.subscribeTerminal({
|
||||
terminal: 'terminal-live',
|
||||
client: { id: 'desktop-live', type: 'desktop' },
|
||||
callbacks: {
|
||||
onData: vi.fn(),
|
||||
onSnapshot: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
await vi.waitFor(() => expect(sendBinary).toHaveBeenCalledTimes(2))
|
||||
const heldStreamId = heldTerminal.streamId
|
||||
const liveStreamId = liveTerminal.streamId
|
||||
const gate = (
|
||||
window as typeof window & {
|
||||
__remoteTerminalMultiplexAckGate?: {
|
||||
hold: (terminals: string[]) => void
|
||||
release: () => void
|
||||
snapshot: () => {
|
||||
heldTerminalCount: number
|
||||
heldStreamCount: number
|
||||
heldAckChars: number
|
||||
releasedAckChars: number
|
||||
}
|
||||
}
|
||||
}
|
||||
).__remoteTerminalMultiplexAckGate
|
||||
expect(gate).toBeDefined()
|
||||
gate?.hold(['terminal-held'])
|
||||
sendBinary.mockClear()
|
||||
|
||||
callbacks?.onBinary?.(
|
||||
encodeTerminalStreamFrame({
|
||||
opcode: TerminalStreamOpcode.Output,
|
||||
streamId: heldStreamId,
|
||||
seq: 1,
|
||||
payload: encodeTerminalStreamText('held-output')
|
||||
})
|
||||
)
|
||||
callbacks?.onBinary?.(
|
||||
encodeTerminalStreamFrame({
|
||||
opcode: TerminalStreamOpcode.Output,
|
||||
streamId: liveStreamId,
|
||||
seq: 2,
|
||||
payload: encodeTerminalStreamText('live-output')
|
||||
})
|
||||
)
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
sendBinary.mock.calls
|
||||
.map((call) => decodeTerminalStreamFrame(call[0]))
|
||||
.filter((frame) => frame?.opcode === TerminalStreamOpcode.Ack)
|
||||
).toHaveLength(1)
|
||||
)
|
||||
const immediateAckFrames = sendBinary.mock.calls
|
||||
.map((call) => decodeTerminalStreamFrame(call[0]))
|
||||
.filter((frame) => frame?.opcode === TerminalStreamOpcode.Ack)
|
||||
expect(immediateAckFrames).toHaveLength(1)
|
||||
expect(immediateAckFrames[0]?.streamId).toBe(liveStreamId)
|
||||
expect(gate?.snapshot()).toMatchObject({
|
||||
heldTerminalCount: 1,
|
||||
heldStreamCount: 1,
|
||||
heldAckChars: 'held-output'.length
|
||||
})
|
||||
|
||||
gate?.release()
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
sendBinary.mock.calls
|
||||
.map((call) => decodeTerminalStreamFrame(call[0]))
|
||||
.filter((frame) => frame?.opcode === TerminalStreamOpcode.Ack)
|
||||
).toHaveLength(2)
|
||||
)
|
||||
const allAckFrames = sendBinary.mock.calls
|
||||
.map((call) => decodeTerminalStreamFrame(call[0]))
|
||||
.filter((frame) => frame?.opcode === TerminalStreamOpcode.Ack)
|
||||
const releasedAck = allAckFrames.find((frame) => frame?.streamId === heldStreamId)
|
||||
expect(releasedAck && decodeTerminalStreamJson(releasedAck.payload)).toEqual({
|
||||
bytes: 'held-output'.length
|
||||
})
|
||||
expect(gate?.snapshot()).toMatchObject({
|
||||
heldTerminalCount: 0,
|
||||
heldStreamCount: 0,
|
||||
heldAckChars: 0,
|
||||
releasedAckChars: 'held-output'.length
|
||||
})
|
||||
|
||||
heldTerminal.close()
|
||||
liveTerminal.close()
|
||||
})
|
||||
|
||||
it('drops output only from the armed stream when its replacement reuses the stream ID', async () => {
|
||||
const { getRemoteRuntimeTerminalMultiplexer, resetRemoteRuntimeTerminalMultiplexersForTests } =
|
||||
await import('./remote-runtime-terminal-multiplexer')
|
||||
resetRemoteRuntimeTerminalMultiplexersForTests()
|
||||
|
||||
const oldData = vi.fn()
|
||||
const oldStream = await getRemoteRuntimeTerminalMultiplexer(
|
||||
'env-output-drop-gate'
|
||||
).subscribeTerminal({
|
||||
terminal: 'terminal-reused-stream-id',
|
||||
client: { id: 'desktop-old', type: 'desktop' },
|
||||
callbacks: { onData: oldData, onSnapshot: vi.fn() }
|
||||
})
|
||||
const gate = (
|
||||
window as typeof window & {
|
||||
__remoteTerminalMultiplexAckGate?: {
|
||||
dropOutputUntilResubscribe: (terminals: string[]) => number
|
||||
release: () => void
|
||||
}
|
||||
}
|
||||
).__remoteTerminalMultiplexAckGate
|
||||
expect(gate?.dropOutputUntilResubscribe(['terminal-reused-stream-id'])).toBe(1)
|
||||
|
||||
callbacks?.onBinary?.(
|
||||
encodeTerminalStreamFrame({
|
||||
opcode: TerminalStreamOpcode.Output,
|
||||
streamId: oldStream.streamId,
|
||||
seq: 7,
|
||||
payload: encodeTerminalStreamText('dropped')
|
||||
})
|
||||
)
|
||||
expect(oldData).not.toHaveBeenCalled()
|
||||
oldStream.close()
|
||||
|
||||
const replacementData = vi.fn()
|
||||
const replacement = await getRemoteRuntimeTerminalMultiplexer(
|
||||
'env-output-drop-gate'
|
||||
).subscribeTerminal({
|
||||
terminal: 'terminal-reused-stream-id',
|
||||
client: { id: 'desktop-new', type: 'desktop' },
|
||||
callbacks: { onData: replacementData, onSnapshot: vi.fn() }
|
||||
})
|
||||
expect(replacement.streamId).toBe(oldStream.streamId)
|
||||
callbacks?.onBinary?.(
|
||||
encodeTerminalStreamFrame({
|
||||
opcode: TerminalStreamOpcode.Output,
|
||||
streamId: replacement.streamId,
|
||||
seq: 9,
|
||||
payload: encodeTerminalStreamText('delivered')
|
||||
})
|
||||
)
|
||||
|
||||
expect(replacementData).toHaveBeenCalledWith('delivered', { seq: 9, rawLength: 9 })
|
||||
gate?.release()
|
||||
replacement.close()
|
||||
})
|
||||
|
||||
it('applies mid-session recovery snapshots without re-subscribing', async () => {
|
||||
const { getRemoteRuntimeTerminalMultiplexer } =
|
||||
await import('./remote-runtime-terminal-multiplexer')
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { sendRuntimePtyInputVerified } from './runtime-terminal-verified-input'
|
||||
import {
|
||||
createCompatibleRuntimeStatusResponseIfNeeded,
|
||||
type RuntimeEnvironmentCallRequest
|
||||
} from './runtime-compatibility-test-fixture'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from './runtime-rpc-client'
|
||||
|
||||
const REMOTE_PTY = 'remote:env-1@@terminal-1'
|
||||
const runtimeSend = vi.fn()
|
||||
const localWrite = vi.fn()
|
||||
const localWriteAccepted = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
runtimeEnvironments: {
|
||||
call: (args: RuntimeEnvironmentCallRequest) =>
|
||||
createCompatibleRuntimeStatusResponseIfNeeded(args) ?? runtimeSend(args)
|
||||
},
|
||||
pty: { write: localWrite, writeAccepted: localWriteAccepted }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function hostReply(send: Record<string, unknown>) {
|
||||
runtimeSend.mockResolvedValue({
|
||||
ok: true,
|
||||
result: { send: { handle: 'terminal-1', bytesWritten: 1, ...send } },
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
}
|
||||
|
||||
describe('verified input that requires provider settlement', () => {
|
||||
it.each([
|
||||
['older host whole-write', { accepted: true }, true],
|
||||
['acknowledged', { accepted: true, writeSettlement: { outcome: 'accepted' } }, true],
|
||||
[
|
||||
'refused',
|
||||
{ accepted: false, writeSettlement: { outcome: 'refused', reason: 'endpoint_disconnected' } },
|
||||
false
|
||||
]
|
||||
] as const)('reads the %s remote verdict', async (_label, send, accepted) => {
|
||||
hostReply(send)
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving', {
|
||||
requireWriteSettlement: true
|
||||
})
|
||||
).resolves.toBe(accepted)
|
||||
expect(runtimeSend).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'terminal.send',
|
||||
params: expect.objectContaining({ requireWriteSettlement: true })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a remote send whose settlement is unverifiable, without a fallback write', async () => {
|
||||
hostReply({
|
||||
accepted: false,
|
||||
writeSettlement: {
|
||||
outcome: 'unverifiable',
|
||||
reason: 'transport_settlement_lost',
|
||||
bytesHandedToTransport: true
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving', {
|
||||
requireWriteSettlement: true
|
||||
})
|
||||
).rejects.toThrow('acknowledgment unavailable')
|
||||
expect(localWrite).not.toHaveBeenCalled()
|
||||
expect(runtimeSend).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('local verified input', () => {
|
||||
it('keeps the fire-and-forget fallback for ordinary input the provider cannot accept', async () => {
|
||||
localWriteAccepted.mockResolvedValue(false)
|
||||
await expect(sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', 'x', 'driving')).resolves.toBe(
|
||||
true
|
||||
)
|
||||
expect(localWriteAccepted).toHaveBeenCalledWith('ssh:conn-1@@pty-1', 'x', 'driving')
|
||||
expect(localWrite).toHaveBeenCalledWith('ssh:conn-1@@pty-1', 'x', 'driving')
|
||||
})
|
||||
|
||||
it('never falls back to an unacknowledged write when settlement is required', async () => {
|
||||
const settled = { requireWriteSettlement: true } as const
|
||||
localWriteAccepted.mockResolvedValueOnce(true)
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', 'answer', 'driving', settled)
|
||||
).resolves.toBe(true)
|
||||
localWriteAccepted.mockResolvedValueOnce(false)
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', '1', 'driving', settled)
|
||||
).resolves.toBe(false)
|
||||
localWriteAccepted.mockRejectedValueOnce(new Error('acknowledgment unavailable'))
|
||||
await expect(
|
||||
sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', '\r', 'driving', settled)
|
||||
).rejects.toThrow('acknowledgment unavailable')
|
||||
expect(localWriteAccepted).toHaveBeenCalledWith('ssh:conn-1@@pty-1', '\r', 'driving', settled)
|
||||
expect(localWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('owner of the PTY', () => {
|
||||
it('never dials a server for a remote id that names no owner', async () => {
|
||||
localWriteAccepted.mockResolvedValue(false)
|
||||
// Why: this id used to reach whichever server was focused, even when another one owned it.
|
||||
await sendRuntimePtyInputVerified('remote:terminal-1', 'x', 'driving', {
|
||||
requireWriteSettlement: true
|
||||
})
|
||||
expect(runtimeSend).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends to the server the id names', async () => {
|
||||
hostReply({ accepted: true })
|
||||
await sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving')
|
||||
expect(runtimeSend).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-1', method: 'terminal.send' })
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user