mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
test: remove mock-only suites and native text-entry harness
This commit is contained in:
@@ -5365,7 +5365,7 @@
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/shared/remote-runtime/remote-runtime-client.test.ts src/main/ipc/runtime-environments/browser-hosting/runtime-environment-browser-client-host-handler.test.ts",
|
||||
"SKIP_BUILD=1 ORCA_CROSS_VERSION_PACKAGED_EXECUTABLE=/Applications/Orca.app/Contents/MacOS/Orca pnpm exec playwright test tests/e2e/packaged-mixed-version-browser-placement.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/network/browser-network-tunnel-paired-runtime.integration.test.ts src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/browser/network/execution-routes/browser-network-execution-route.test.ts src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts src/main/browser/network/execution-routes/wsl-browser-network-relay-launch.test.ts src/main/runtime/browser/network/runtime-browser-network-execution-host.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/cdp/screencast/browser-screencast-stream.test.ts src/main/runtime/browser/screencast/orca-runtime-browser-screencast-fanout.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/cdp/screencast/browser-screencast-stream.test.ts",
|
||||
"ORCA_RUN_DOCKER_SSH_BROWSER_E2E=1 pnpm exec vitest run --config config/vitest.config.ts tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts",
|
||||
"Manual iOS 26.5 simulator: pair current mobile code to packaged Orca 1.4.184; create Browser; navigate to https://example.com; require one visible Example Domain tab on the server-hosted surface",
|
||||
"ORCA_RUN_DOCKER_SSH_BROWSER_E2E=1 node_modules/.bin/vitest run --config config/vitest.config.ts tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts"
|
||||
@@ -5384,7 +5384,6 @@
|
||||
"src/main/browser/network/execution-routes/wsl-browser-network-relay-launch.test.ts",
|
||||
"src/main/runtime/browser/network/runtime-browser-network-execution-host.test.ts",
|
||||
"src/main/browser/cdp/screencast/browser-screencast-stream.test.ts",
|
||||
"src/main/runtime/browser/screencast/orca-runtime-browser-screencast-fanout.test.ts",
|
||||
"tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
@@ -6169,7 +6168,7 @@
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/terminal/surface/orca-runtime-terminal-close-continuity.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts",
|
||||
"pnpm dlx node@24 ./node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/agent-hooks/server/testing/authority/server-pane-authority.test.ts src/main/ipc/agent-hooks.test.ts src/main/ipc/agent-pane-authority-ownership.test.ts src/main/persistence/restoring-sessions/persistence-pane-identity-migration.test.ts src/main/persistence/loading-store/terminal-bindings/persistence-pty-binding-reconciliation.test.ts src/renderer/src/store/slices/agent-status/agent-pane-authority.test.ts src/renderer/src/store/terminals/layout/terminal-pane-detach-agent-identity.test.ts src/renderer/src/store/terminals/retirement/terminal-tab-retirement-store.test.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts",
|
||||
"pnpm dlx node@24 ./node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/agent-hooks/server/testing/authority/server-pane-authority.test.ts src/main/ipc/agent-pane-authority-ownership.test.ts src/main/persistence/restoring-sessions/persistence-pane-identity-migration.test.ts src/main/persistence/loading-store/terminal-bindings/persistence-pty-binding-reconciliation.test.ts src/renderer/src/store/slices/agent-status/agent-pane-authority.test.ts src/renderer/src/store/terminals/layout/terminal-pane-detach-agent-identity.test.ts src/renderer/src/store/terminals/retirement/terminal-tab-retirement-store.test.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/pty/process-retirement/pty-descendant-termination.test.ts src/main/daemon/terminal-host/testing/terminal-host.test.ts src/main/providers/local-pty/local-pty-provider-shutdown.test.ts src/main/runtime/workspaces/removal/worktree-teardown.test.ts",
|
||||
"pnpm run test:e2e -- tests/e2e/terminal-tab-close-restart-persistence.spec.ts --workers=1",
|
||||
"pnpm run test:e2e -- tests/e2e/terminal-parked-close-retirement.spec.ts --workers=1",
|
||||
@@ -6181,7 +6180,6 @@
|
||||
"src/main/runtime/terminal/surface/orca-runtime-terminal-close-continuity.test.ts",
|
||||
"src/main/runtime/construction/orca-runtime.test.ts",
|
||||
"src/main/agent-hooks/server/testing/authority/server-pane-authority.test.ts",
|
||||
"src/main/ipc/agent-hooks.test.ts",
|
||||
"src/main/ipc/agent-pane-authority-ownership.test.ts",
|
||||
"src/main/persistence/restoring-sessions/persistence-pane-identity-migration.test.ts",
|
||||
"src/main/persistence/loading-store/terminal-bindings/persistence-pty-binding-reconciliation.test.ts",
|
||||
|
||||
@@ -75,19 +75,6 @@ const checks = [
|
||||
],
|
||||
enabled: process.platform === 'win32'
|
||||
},
|
||||
{
|
||||
name: 'Windows text entry tests',
|
||||
command: 'powershell.exe',
|
||||
args: [
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
'native/computer-use-windows/runtime-text-entry.test.ps1'
|
||||
],
|
||||
enabled: process.platform === 'win32'
|
||||
},
|
||||
{
|
||||
name: 'Windows native control tests',
|
||||
command: 'powershell.exe',
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
param([string]$RuntimePath = (Join-Path $PSScriptRoot "runtime.ps1"))
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Assert-TestEqual($Actual, $Expected, [string]$Label) {
|
||||
if ($Actual -cne $Expected) { throw "$Label expected '$Expected', received '$Actual'" }
|
||||
}
|
||||
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public static class OrcaDesktopWin32 {
|
||||
public static readonly List<string> Calls = new List<string>();
|
||||
public static IntPtr Foreground;
|
||||
public static string EditText;
|
||||
public static int[] Selection;
|
||||
public static int EditOwner;
|
||||
public static bool IgnoreInsertion;
|
||||
public static long Limit;
|
||||
|
||||
public static void Reset(string text, int start, int end) {
|
||||
Calls.Clear();
|
||||
Foreground = new IntPtr(202);
|
||||
EditText = text;
|
||||
Selection = new[] { start, end };
|
||||
EditOwner = 4242;
|
||||
IgnoreInsertion = false;
|
||||
Limit = 30000;
|
||||
}
|
||||
public static int GetProcessIntegrityLevel(int processId) { return 0x2000; }
|
||||
public static IntPtr GetForegroundWindow() { return Foreground; }
|
||||
public static bool SetForegroundWindow(IntPtr hwnd) { Calls.Add("activate"); return false; }
|
||||
public static bool IsIconic(IntPtr hwnd) { return false; }
|
||||
public static bool ShowWindow(IntPtr hwnd, int command) { Calls.Add("restore"); return true; }
|
||||
public static bool IsWindow(IntPtr hwnd) { return true; }
|
||||
public static int GetWindowLong(IntPtr hwnd, int index) { return 0x0004; }
|
||||
public static uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId) {
|
||||
processId = hwnd == new IntPtr(303) ? (uint)EditOwner : (hwnd == new IntPtr(101) ? 4242u : 1u);
|
||||
return 1;
|
||||
}
|
||||
public static string GetEditText(IntPtr hwnd, int maxLength) { return EditText; }
|
||||
public static int[] GetEditSelection(IntPtr hwnd) { return Selection; }
|
||||
public static bool ReplaceEditSelection(IntPtr hwnd, string text) {
|
||||
Calls.Add("replace:" + text);
|
||||
if (!IgnoreInsertion) {
|
||||
EditText = EditText.Substring(0, Selection[0]) + text + EditText.Substring(Selection[1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static bool SendUnicodeText(string text) { Calls.Add("keys:" + text); return true; }
|
||||
public static long? SendControlMessage(IntPtr hwnd, uint message, IntPtr wParam, IntPtr lParam) { return message == 0x00D5 ? Limit : 0; }
|
||||
}
|
||||
"@
|
||||
|
||||
$tokens = $null
|
||||
$errors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile($RuntimePath, [ref]$tokens, [ref]$errors)
|
||||
if ($errors.Count) { throw ($errors | Out-String) }
|
||||
$functions = @(
|
||||
"Test-OrcaWindowFocused", "Wait-OrcaWindowFocused", "Assert-OrcaKeyboardFocus", "New-OrcaForegroundRefusal",
|
||||
"Assert-OrcaWindowTarget", "Get-OrcaRequiredString", "Invoke-OrcaOperation",
|
||||
"Assert-OrcaInputAllowed", "Test-OrcaTargetTookForeground", "Get-OrcaProperty", "Test-OrcaSensitiveElement",
|
||||
"Test-OrcaEditClass", "Get-OrcaNativeEdit", "Assert-OrcaWindowOwner", "Test-OrcaNativeClass", "ConvertTo-OrcaEditNewlines",
|
||||
"Invoke-OrcaEditTextInsertion", "Invoke-OrcaElementTextInsertion", "Send-OrcaText",
|
||||
"Test-OrcaElementDisabled", "Assert-OrcaStillFocused"
|
||||
)
|
||||
foreach ($definition in $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)) {
|
||||
if ($definition.Name -in $functions) { . ([scriptblock]::Create($definition.Extent.Text)) }
|
||||
}
|
||||
|
||||
$EditReadbackLimit = 1048576
|
||||
function Find-OrcaProcess { [pscustomobject]@{ MainWindowHandle = 101; Id = 4242; ProcessName = "Fixture" } }
|
||||
function Get-OrcaAppName($Process) { $Process.ProcessName }
|
||||
function Get-OrcaRootElement { $null }
|
||||
function Get-OrcaWindowFrame { @{ x = 100; y = 200 } }
|
||||
function Find-OrcaElement($RootElement, $Record) { if ($null -eq $Record) { $null } else { $Record.live } }
|
||||
function Invoke-OrcaValueTextInsertion { $script:valueWrites++; $null }
|
||||
function Send-OrcaKey { [OrcaDesktopWin32]::Calls.Add("sendkeys") }
|
||||
function Send-OrcaHotkey { [OrcaDesktopWin32]::Calls.Add("sendkeys") }
|
||||
function Send-OrcaPasteText { [OrcaDesktopWin32]::Calls.Add("sendkeys") }
|
||||
function New-OrcaSnapshot { [pscustomobject]@{} }
|
||||
function Test-OrcaTextPatternReadOnly { $false }
|
||||
function Restore-OrcaWindow { [OrcaDesktopWin32]::Calls.Add("activate"); $null }
|
||||
|
||||
function New-TestElement([string]$ClassName, [int64]$Handle, [bool]$Enabled = $true, [string]$ControlType = "ControlType.Document") {
|
||||
$live = [pscustomobject]@{
|
||||
Current = [pscustomobject]@{
|
||||
ClassName = $ClassName; NativeWindowHandle = $Handle; IsPassword = $false; IsEnabled = $Enabled; Name = "Body"
|
||||
AutomationId = ""; LocalizedControlType = "document"; ControlType = [pscustomobject]@{ ProgrammaticName = $ControlType }
|
||||
}
|
||||
}
|
||||
[pscustomobject]@{ index = 7; live = $live }
|
||||
}
|
||||
|
||||
function Invoke-TestOperation($Operation) {
|
||||
$message = $null
|
||||
$result = $null
|
||||
try { $result = Invoke-OrcaOperation $Operation } catch { $message = $_.Exception.Message }
|
||||
[pscustomobject]@{ result = $result; message = $message }
|
||||
}
|
||||
|
||||
# Background keyboard verbs refuse before any key leaves, and say how to enter text without the foreground.
|
||||
foreach ($operation in @(
|
||||
[pscustomobject]@{ tool = "type_text"; text = "hello" },
|
||||
[pscustomobject]@{ tool = "paste_text"; text = "hello" },
|
||||
[pscustomobject]@{ tool = "press_key"; key = "Return" },
|
||||
[pscustomobject]@{ tool = "hotkey"; key = "Ctrl+A" }
|
||||
)) {
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
$operation | Add-Member -NotePropertyName app -NotePropertyValue "Fixture"
|
||||
$outcome = Invoke-TestOperation $operation
|
||||
if ($outcome.message -notlike "background_unavailable: *takes the user's keyboard; nothing was sent.*type-text --element-index*Ask the user before retrying with --restore-window.") {
|
||||
throw "$($operation.tool) background refusal, received '$($outcome.message)'"
|
||||
}
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "$($operation.tool) background refusal sends nothing"
|
||||
}
|
||||
|
||||
# Typing into a native edit replaces its selection in the background and reads the result back.
|
||||
[OrcaDesktopWin32]::Reset("one`r`nthree", 3, 3)
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "`ntwo"; element = (New-TestElement "Edit" 303) })
|
||||
if ($null -ne $outcome.message) { throw "edit insertion failed: $($outcome.message)" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls -join "|") "replace:`r`ntwo" "edit insertion sends one EM_REPLACESEL with CRLF line breaks"
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::EditText) "one`r`ntwo`r`nthree" "edit insertion result"
|
||||
Assert-TestEqual $outcome.result.action.actionName "EM_REPLACESEL" "edit insertion action"
|
||||
Assert-TestEqual $outcome.result.action.delivery "background" "edit insertion delivery"
|
||||
Assert-TestEqual $outcome.result.action.verification.state "verified" "edit insertion verification"
|
||||
|
||||
# A field that ignores the insertion is an error, not a typed result.
|
||||
[OrcaDesktopWin32]::Reset("same", 4, 4)
|
||||
[OrcaDesktopWin32]::IgnoreInsertion = $true
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "x"; element = (New-TestElement "Edit" 303) })
|
||||
if ($outcome.message -notlike "text_insertion_unconfirmed:*") { throw "ignored insertion, received '$($outcome.message)'" }
|
||||
|
||||
# Replacing a selection with the same text changes nothing yet is the requested result.
|
||||
[OrcaDesktopWin32]::Reset("abc", 0, 3)
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "abc"; element = (New-TestElement "Edit" 303) })
|
||||
if ($null -ne $outcome.message) { throw "identical replacement failed: $($outcome.message)" }
|
||||
Assert-TestEqual $outcome.result.action.verification.state "verified" "identical replacement verification"
|
||||
|
||||
# An edit cuts text past its limit, so an insertion that would overflow it is refused before anything is sent.
|
||||
[OrcaDesktopWin32]::Reset("old", 3, 3)
|
||||
[OrcaDesktopWin32]::Limit = 5
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "abc"; element = (New-TestElement "Edit" 303) })
|
||||
if ($outcome.message -notlike "element 7 text is not settable: the result would be 6 characters, past the field's 5-character limit") { throw "over-limit insertion, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::EditText) "old" "over-limit insertion leaves the field untouched"
|
||||
|
||||
# A password field can't be read back, yet its limit still holds.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
[OrcaDesktopWin32]::Limit = 2
|
||||
$secret = New-TestElement "Edit" 303
|
||||
$secret.live.Current.IsPassword = $true
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "abc"; element = $secret })
|
||||
if ($outcome.message -notlike "*past the field's 2-character limit") { throw "over-limit password insertion, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "over-limit password insertion sends nothing"
|
||||
|
||||
# Edit messages would bypass a disabled field's lock, so a disabled field is refused untouched.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "x"; element = (New-TestElement "Edit" 303 $false) })
|
||||
if ($outcome.message -notlike "element 7 text is not settable: the field is disabled") { throw "disabled edit, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "disabled edit sends nothing"
|
||||
|
||||
# A recycled window handle owned by another process is never messaged.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
[OrcaDesktopWin32]::EditOwner = 999
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "x"; element = (New-TestElement "Edit" 303) })
|
||||
if ($outcome.message -notlike "stale element:*") { throw "recycled edit handle, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "recycled edit handle sends nothing"
|
||||
|
||||
# An element with no background text route refuses instead of typing at whatever has focus.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "x"; element = (New-TestElement "Chrome_RenderWidgetHostHWND" 404) })
|
||||
if ($outcome.message -notlike "background_unavailable: element 7 accepts text only as keys*") { throw "no text route, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "no text route sends nothing"
|
||||
|
||||
# List and tree items take ValuePattern too, where a write would rename them, so they are refused untouched.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
$script:valueWrites = 0
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "x"; element = (New-TestElement "" 0 $true "ControlType.ListItem") })
|
||||
if ($outcome.message -notlike "element 7 text is not settable: it is not a text field") { throw "list item, received '$($outcome.message)'" }
|
||||
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count + $script:valueWrites) 0 "list item sends nothing"
|
||||
|
||||
# Foreground typing re-checks focus right before SendInput, so a user's switch never receives the text.
|
||||
[OrcaDesktopWin32]::Reset("", 0, 0)
|
||||
[OrcaDesktopWin32]::Foreground = [IntPtr]101
|
||||
$outcome = Invoke-TestOperation ([pscustomobject]@{ tool = "type_text"; app = "Fixture"; text = "caf$([char]0x00E9)"; restoreWindow = $true })
|
||||
if ($null -ne $outcome.message) { throw "foreground typing failed: $($outcome.message)" }
|
||||
Assert-TestEqual (@([OrcaDesktopWin32]::Calls | Where-Object { $_ -like "keys:*" }) -join "|") "keys:caf$([char]0x00E9)" "foreground typing sends Unicode input"
|
||||
Write-Output "windows-text-entry-tests-ok"
|
||||
@@ -1,155 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type * as AgentHookServerModule from '../agent-hooks/server/agent-hook-server'
|
||||
import { makePaneKey } from '../../shared/workspace-layout/stable-pane-id'
|
||||
|
||||
// Renderer dismissals must evict persisted status as well as the live row.
|
||||
|
||||
const dropStatusEntry = vi.fn()
|
||||
const dropPersistedStatusEntry = vi.fn()
|
||||
const dropPersistedStatusEntries = vi.fn(() => [] as string[])
|
||||
const dropStatusEntriesByTabPrefix = vi.fn()
|
||||
const retirePaneAuthority = vi.fn()
|
||||
const transferPaneAuthority = vi.fn()
|
||||
const canTransferPaneAuthority = vi.fn(() => true)
|
||||
const getStatusSnapshot = vi.fn()
|
||||
const inferInterrupt = vi.fn()
|
||||
const clearMigrationUnsupportedPtysByTabPrefix = vi.fn()
|
||||
const clearMigrationUnsupportedPtysForPaneKey = vi.fn()
|
||||
const onHandlers = new Map<string, (event: unknown, ...args: unknown[]) => void>()
|
||||
const handleHandlers = new Map<string, (event: unknown, ...args: unknown[]) => unknown>()
|
||||
const removeHandler = vi.fn()
|
||||
const removeAllListeners = vi.fn()
|
||||
const PANE_KEY = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
|
||||
const CHILD_PANE_KEY = makePaneKey('tab-2', '22222222-2222-4222-8222-222222222222')
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: (channel: string, handler: (event: unknown, ...args: unknown[]) => unknown) => {
|
||||
handleHandlers.set(channel, handler)
|
||||
},
|
||||
on: (channel: string, handler: (event: unknown, ...args: unknown[]) => void) => {
|
||||
onHandlers.set(channel, handler)
|
||||
},
|
||||
removeHandler,
|
||||
removeAllListeners
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../agent-hooks/server/agent-hook-server', async () => {
|
||||
// Use the real pane-key validator so invalid-input coverage tracks its constraints.
|
||||
const actual = await vi.importActual<typeof AgentHookServerModule>(
|
||||
'../agent-hooks/server/agent-hook-server'
|
||||
)
|
||||
return {
|
||||
...actual,
|
||||
agentHookServer: {
|
||||
dropStatusEntry,
|
||||
dropPersistedStatusEntry,
|
||||
dropPersistedStatusEntries,
|
||||
dropStatusEntriesByTabPrefix,
|
||||
retirePaneAuthority,
|
||||
transferPaneAuthority,
|
||||
canTransferPaneAuthority,
|
||||
getStatusSnapshot,
|
||||
inferInterrupt
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../agent-hooks/observations/migration-unsupported-pty-state', () => ({
|
||||
clearMigrationUnsupportedPtysByTabPrefix,
|
||||
clearMigrationUnsupportedPtysForPaneKey,
|
||||
getMigrationUnsupportedPtySnapshot: vi.fn(() => [])
|
||||
}))
|
||||
|
||||
vi.mock('../claude/hook-service', () => ({
|
||||
claudeHookService: { getStatus: vi.fn(() => ({ agent: 'claude', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../openclaude/hook-service', () => ({
|
||||
openClaudeHookService: { getStatus: vi.fn(() => ({ agent: 'openclaude', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../codex/hooks/installation/hook-service', () => ({
|
||||
codexHookService: { getStatus: vi.fn(() => ({ agent: 'codex', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../gemini/hook-service', () => ({
|
||||
geminiHookService: { getStatus: vi.fn(() => ({ agent: 'gemini', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../antigravity/hook-service', () => ({
|
||||
antigravityHookService: { getStatus: vi.fn(() => ({ agent: 'antigravity', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../amp/hook-service', () => ({
|
||||
ampHookService: { getStatus: vi.fn(() => ({ agent: 'amp', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../cursor/hook-service', () => ({
|
||||
cursorHookService: { getStatus: vi.fn(() => ({ agent: 'cursor', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../droid/hook-service', () => ({
|
||||
droidHookService: { getStatus: vi.fn(() => ({ agent: 'droid', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../command-code/hook-service', () => ({
|
||||
commandCodeHookService: { getStatus: vi.fn(() => ({ agent: 'command-code', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../grok/hook-service', () => ({
|
||||
grokHookService: { getStatus: vi.fn(() => ({ agent: 'grok', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../copilot/hook-service', () => ({
|
||||
copilotHookService: { getStatus: vi.fn(() => ({ agent: 'copilot', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../hermes/hook-service', () => ({
|
||||
hermesHookService: { getStatus: vi.fn(() => ({ agent: 'hermes', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../devin/hook-service', () => ({
|
||||
devinHookService: { getStatus: vi.fn(() => ({ agent: 'devin', state: 'absent' })) }
|
||||
}))
|
||||
vi.mock('../kimi/hook-service', () => ({
|
||||
kimiHookService: { getStatus: vi.fn(() => ({ agent: 'kimi', state: 'absent' })) }
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
dropStatusEntry.mockReset()
|
||||
dropPersistedStatusEntry.mockReset()
|
||||
dropPersistedStatusEntries.mockReset()
|
||||
dropPersistedStatusEntries.mockReturnValue([])
|
||||
dropStatusEntriesByTabPrefix.mockReset()
|
||||
retirePaneAuthority.mockReset()
|
||||
transferPaneAuthority.mockReset()
|
||||
canTransferPaneAuthority.mockReset()
|
||||
canTransferPaneAuthority.mockReturnValue(true)
|
||||
getStatusSnapshot.mockReset()
|
||||
inferInterrupt.mockReset()
|
||||
clearMigrationUnsupportedPtysByTabPrefix.mockReset()
|
||||
clearMigrationUnsupportedPtysForPaneKey.mockReset()
|
||||
onHandlers.clear()
|
||||
handleHandlers.clear()
|
||||
removeHandler.mockReset()
|
||||
removeAllListeners.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('agent pane authority IPC', () => {
|
||||
it('rejects unowned and oversized pane authority transfers', async () => {
|
||||
const { registerAgentHookHandlers } = await import('./agent-hooks')
|
||||
registerAgentHookHandlers()
|
||||
const transfer = onHandlers.get('agentStatus:transferPaneAuthority')!
|
||||
|
||||
canTransferPaneAuthority.mockReturnValue(false)
|
||||
transfer({}, { fromPaneKey: PANE_KEY, toPaneKey: CHILD_PANE_KEY, ptyId: 'forged-pty' })
|
||||
transfer({}, { fromPaneKey: PANE_KEY, toPaneKey: CHILD_PANE_KEY })
|
||||
canTransferPaneAuthority.mockReturnValue(true)
|
||||
transfer({}, { fromPaneKey: PANE_KEY, toPaneKey: CHILD_PANE_KEY, ptyId: 'x'.repeat(513) })
|
||||
transfer(
|
||||
{},
|
||||
{
|
||||
fromPaneKey: `${'x'.repeat(180)}:11111111-1111-4111-8111-111111111111`,
|
||||
toPaneKey: CHILD_PANE_KEY,
|
||||
ptyId: 'pty-1'
|
||||
}
|
||||
)
|
||||
|
||||
expect(transferPaneAuthority).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { Store } from '../persistence/loading-store/store'
|
||||
import type * as RepoWorktrees from '../workspaces/catalog/repo-worktrees'
|
||||
import { invalidateAuthorizedRootsCache } from './worktrees/removal/registered-worktree-roots-cache'
|
||||
|
||||
type Handler = (event: unknown, args: unknown) => unknown
|
||||
|
||||
const { handlers, startNotebookKernelMock, userData } = vi.hoisted(() => ({
|
||||
handlers: new Map<string, Handler>(),
|
||||
startNotebookKernelMock: vi.fn(),
|
||||
userData: { path: '' }
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => userData.path },
|
||||
ipcMain: { handle: (channel: string, handler: Handler) => handlers.set(channel, handler) }
|
||||
}))
|
||||
vi.mock('../workspaces/catalog/repo-worktrees', async () => {
|
||||
const actual = await vi.importActual<typeof RepoWorktrees>('../workspaces/catalog/repo-worktrees')
|
||||
return { ...actual, listRepoWorktreeGraph: vi.fn(async () => []) }
|
||||
})
|
||||
vi.mock('../notebook/notebook-kernel', () => ({ startNotebookKernel: startNotebookKernelMock }))
|
||||
|
||||
import { registerNotebookHandlers } from './notebook'
|
||||
|
||||
let base: string
|
||||
let project: string
|
||||
|
||||
function storeWithProject(projectPath: string): Store {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: authorization reads only these Store members.
|
||||
return {
|
||||
getRepos: () => [
|
||||
{ id: 'repo', path: projectPath, displayName: 'project', badgeColor: '#000', addedAt: 0 }
|
||||
],
|
||||
getProjects: () => [],
|
||||
getProjectGroups: () => [],
|
||||
getFolderWorkspaces: () => [],
|
||||
getSettings: () => ({ nestWorkspaces: false, workspaceDir: '' })
|
||||
} as unknown as Store
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
invalidateAuthorizedRootsCache()
|
||||
handlers.clear()
|
||||
startNotebookKernelMock.mockReset().mockReturnValue({
|
||||
kernel: { execute: vi.fn(), interrupt: vi.fn(), shutdown: vi.fn() },
|
||||
ready: Promise.resolve({ status: 'ready' }),
|
||||
exited: new Promise(() => {})
|
||||
})
|
||||
base = await mkdtemp(join(await realpath(tmpdir()), 'orca-notebook-link-'))
|
||||
project = join(base, 'project')
|
||||
userData.path = join(base, 'user-data')
|
||||
await mkdir(join(project, 'analysis'), { recursive: true })
|
||||
await mkdir(join(userData.path, 'floating-workspace'), { recursive: true })
|
||||
await writeFile(join(project, 'analysis', 'real.ipynb'), '{}')
|
||||
registerNotebookHandlers(storeWithProject(project))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(base, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'runs a notebook opened through a project link in its real folder, as the project check resolves it',
|
||||
async () => {
|
||||
await symlink(join(project, 'analysis', 'real.ipynb'), join(project, 'nb.ipynb'))
|
||||
const owner = Object.assign(new EventEmitter(), { send: vi.fn(), isDestroyed: () => false })
|
||||
|
||||
await handlers.get('notebook:startKernel')!(
|
||||
{ sender: owner },
|
||||
{ filePath: join(project, 'nb.ipynb'), python: '/py' }
|
||||
)
|
||||
|
||||
expect(startNotebookKernelMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ cwd: join(project, 'analysis') })
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'runs a linked notebook outside every project in its real folder, as before',
|
||||
async () => {
|
||||
const notes = join(base, 'notes')
|
||||
await mkdir(join(notes, 'analysis'), { recursive: true })
|
||||
await writeFile(join(notes, 'analysis', 'real.ipynb'), '{}')
|
||||
await symlink(join(notes, 'analysis', 'real.ipynb'), join(notes, 'nb.ipynb'))
|
||||
const owner = Object.assign(new EventEmitter(), { send: vi.fn(), isDestroyed: () => false })
|
||||
|
||||
await handlers.get('notebook:startKernel')!(
|
||||
{ sender: owner },
|
||||
{ filePath: join(notes, 'nb.ipynb'), python: '/py' }
|
||||
)
|
||||
|
||||
expect(startNotebookKernelMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ cwd: join(notes, 'analysis') })
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1,136 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { WebContents } from 'electron'
|
||||
import type {
|
||||
BrowserScreencastOptions,
|
||||
BrowserScreencastSession
|
||||
} from '../../../browser/cdp/screencast/browser-screencast-stream-types'
|
||||
import {
|
||||
createSinglePageBrowserCommandsHost,
|
||||
createStreamedGuest
|
||||
} from '../client-pages/single-page-browser-commands-host-test-double'
|
||||
import { RuntimeBrowserCommands } from '../commands/browser-commands'
|
||||
|
||||
const { webContentsFromId, startBrowserScreencast } = vi.hoisted(() => ({
|
||||
webContentsFromId: vi.fn(),
|
||||
startBrowserScreencast:
|
||||
vi.fn<
|
||||
(guest: WebContents, options: BrowserScreencastOptions) => Promise<BrowserScreencastSession>
|
||||
>()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
|
||||
webContents: { fromId: webContentsFromId }
|
||||
}))
|
||||
vi.mock('../../../browser/cdp/screencast/browser-screencast-stream', () => ({
|
||||
startBrowserScreencast
|
||||
}))
|
||||
|
||||
function createSession() {
|
||||
let close!: () => void
|
||||
const done = new Promise<void>((resolve) => {
|
||||
close = resolve
|
||||
})
|
||||
return {
|
||||
stop: vi.fn(() => close()),
|
||||
done,
|
||||
updateViewport: async () => {},
|
||||
updateFrameBudget: async () => {}
|
||||
}
|
||||
}
|
||||
|
||||
const params = { page: 'page-1', format: 'jpeg' } as const
|
||||
|
||||
describe('screencast ownership', () => {
|
||||
beforeEach(() => {
|
||||
webContentsFromId.mockReset()
|
||||
webContentsFromId.mockReturnValue(createStreamedGuest())
|
||||
startBrowserScreencast.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it.each(['older', 'newer'])(
|
||||
'keeps the newer viewer when the %s reconnect finishes first',
|
||||
async (firstReady) => {
|
||||
const session = createSession()
|
||||
let finishUpdate = (): void => {}
|
||||
let updateStarted = (): void => {}
|
||||
const started = new Promise<void>((resolve) => {
|
||||
updateStarted = resolve
|
||||
})
|
||||
vi.spyOn(session, 'updateFrameBudget').mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finishUpdate = resolve
|
||||
updateStarted()
|
||||
})
|
||||
)
|
||||
startBrowserScreencast.mockResolvedValue(session)
|
||||
const commands = new RuntimeBrowserCommands(createSinglePageBrowserCommandsHost())
|
||||
const device = { pairedDeviceId: 'phone-1' }
|
||||
const first = await commands.browserScreencast(params, { ...device, sendBinary: () => true })
|
||||
const olderSend = vi.fn(() => true)
|
||||
const newerSend = vi.fn(() => true)
|
||||
const reconnectParams = { ...params, maxWidth: 640 }
|
||||
const olderJoin = commands.browserScreencast(reconnectParams, {
|
||||
...device,
|
||||
sendBinary: olderSend
|
||||
})
|
||||
let finishViewport = (): void => {}
|
||||
let viewportStarted = (): void => {}
|
||||
const viewportPending = new Promise<void>((resolve) => {
|
||||
viewportStarted = resolve
|
||||
})
|
||||
if (firstReady === 'older') {
|
||||
vi.spyOn(session, 'updateViewport').mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finishViewport = resolve
|
||||
viewportStarted()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await started
|
||||
const newerJoin = commands.browserScreencast(
|
||||
{
|
||||
...reconnectParams,
|
||||
...(firstReady === 'older' ? { viewportWidth: 640, viewportHeight: 480 } : {})
|
||||
},
|
||||
{
|
||||
...device,
|
||||
sendBinary: newerSend
|
||||
}
|
||||
)
|
||||
if (firstReady === 'older') {
|
||||
await viewportPending
|
||||
finishUpdate()
|
||||
await olderJoin
|
||||
finishViewport()
|
||||
}
|
||||
const newer = await newerJoin
|
||||
await first.session.done
|
||||
finishUpdate()
|
||||
const older = await olderJoin
|
||||
await older.session.done
|
||||
const frame = new Uint8Array([1, 2, 3])
|
||||
startBrowserScreencast.mock.calls[0]?.[1].onFrame(frame)
|
||||
expect(olderSend).not.toHaveBeenCalled()
|
||||
expect(newerSend).toHaveBeenCalledWith(frame, { dropWhenBacklogged: true })
|
||||
expect(session.stop).not.toHaveBeenCalled()
|
||||
newer.session.stop()
|
||||
await newer.session.done
|
||||
expect(session.stop).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
finishUpdate()
|
||||
finishViewport()
|
||||
session.stop()
|
||||
await session.done
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createSinglePageBrowserCommandsHost,
|
||||
createStreamedGuest
|
||||
} from '../client-pages/single-page-browser-commands-host-test-double'
|
||||
|
||||
const { webContentsFromId, startBrowserScreencast } = vi.hoisted(() => ({
|
||||
webContentsFromId: vi.fn(),
|
||||
startBrowserScreencast: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
|
||||
webContents: { fromId: webContentsFromId }
|
||||
}))
|
||||
vi.mock('../../../browser/cdp/screencast/browser-screencast-stream', () => ({
|
||||
startBrowserScreencast
|
||||
}))
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('RuntimeBrowserCommands screencast fanout', () => {
|
||||
beforeEach(() => {
|
||||
webContentsFromId.mockReset()
|
||||
webContentsFromId.mockReturnValue(createStreamedGuest())
|
||||
startBrowserScreencast.mockReset()
|
||||
})
|
||||
|
||||
it('replays the joiner snapshot that its pre-ready gate refused', async () => {
|
||||
const { RuntimeBrowserCommands } = await import('../commands/browser-commands')
|
||||
const done = deferred()
|
||||
const snapshot = new Uint8Array([9, 9, 9])
|
||||
let onFrame: (bytes: Uint8Array<ArrayBufferLike>) => boolean | void = () => true
|
||||
// updateViewport emits before the joining viewer's ready gate opens.
|
||||
const updateViewport = vi.fn(async () => {
|
||||
onFrame(snapshot)
|
||||
})
|
||||
startBrowserScreencast.mockImplementation(async (_guest: unknown, options: never) => {
|
||||
onFrame = (options as { onFrame: typeof onFrame }).onFrame
|
||||
return {
|
||||
stop: vi.fn(() => done.resolve()),
|
||||
done: done.promise,
|
||||
updateViewport,
|
||||
updateFrameBudget: vi.fn(async () => {})
|
||||
}
|
||||
})
|
||||
const commands = new RuntimeBrowserCommands(createSinglePageBrowserCommandsHost())
|
||||
const creatorSend = vi.fn(() => true)
|
||||
await commands.browserScreencast(
|
||||
{
|
||||
worktree: 'id:wt-1',
|
||||
page: 'page-1',
|
||||
format: 'jpeg',
|
||||
viewportWidth: 1200,
|
||||
viewportHeight: 800
|
||||
},
|
||||
{ sendBinary: creatorSend }
|
||||
)
|
||||
let gateOpen = false
|
||||
const joinerSend = vi.fn(() => gateOpen)
|
||||
const joiner = await commands.browserScreencast(
|
||||
{
|
||||
worktree: 'id:wt-1',
|
||||
page: 'page-1',
|
||||
format: 'jpeg',
|
||||
viewportWidth: 800,
|
||||
viewportHeight: 600
|
||||
},
|
||||
{ sendBinary: joinerSend }
|
||||
)
|
||||
|
||||
expect(creatorSend).toHaveBeenCalledWith(snapshot, { dropWhenBacklogged: true })
|
||||
expect(joinerSend).toHaveBeenCalledWith(snapshot, { dropWhenBacklogged: true })
|
||||
joinerSend.mockClear()
|
||||
gateOpen = true
|
||||
joiner.flushPendingFrame()
|
||||
expect(joinerSend).toHaveBeenCalledExactlyOnceWith(snapshot, { dropWhenBacklogged: true })
|
||||
|
||||
joinerSend.mockClear()
|
||||
joiner.flushPendingFrame()
|
||||
expect(joinerSend).not.toHaveBeenCalled()
|
||||
joiner.session.stop()
|
||||
await joiner.session.done
|
||||
})
|
||||
})
|
||||
@@ -64,26 +64,13 @@ function request(scope: BulkTabCloseScope) {
|
||||
act(actions[scope])
|
||||
}
|
||||
|
||||
async function confirmRequest(confirmed = true) {
|
||||
async function confirmRequest() {
|
||||
await act(async () => {
|
||||
settle(confirmed)
|
||||
settle(true)
|
||||
})
|
||||
}
|
||||
|
||||
describe('group bulk close confirmation', () => {
|
||||
it.each(['editors', 'others', 'right', 'left'] as const)(
|
||||
'waits for confirmation and leaves tabs untouched on cancelling %s',
|
||||
async (scope) => {
|
||||
request(scope)
|
||||
expect(confirm).toHaveBeenCalledOnce()
|
||||
expect(closeItem).not.toHaveBeenCalled()
|
||||
expect(closeMany).not.toHaveBeenCalled()
|
||||
await confirmRequest(false)
|
||||
expect(closeItem).not.toHaveBeenCalled()
|
||||
expect(closeMany).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['others', 'right', 'left'] as const)(
|
||||
'abandons %s when the anchor is removed while the dialog is open',
|
||||
async (scope) => {
|
||||
@@ -100,17 +87,6 @@ describe('group bulk close confirmation', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['editors', 'others', 'right', 'left'] as const)(
|
||||
'abandons %s when the original group disappears',
|
||||
async (scope) => {
|
||||
request(scope)
|
||||
act(() => useAppStore.setState({ groupsByWorktree: {} }))
|
||||
await confirmRequest()
|
||||
expect(closeMany).not.toHaveBeenCalled()
|
||||
expect(closeItem).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('skips editors pinned while confirmation is pending', async () => {
|
||||
request('editors')
|
||||
act(() =>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SshConnectionState } from '../../../shared/ssh-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { createWebFileMutationMethods } from './web-file-mutation-methods'
|
||||
|
||||
function resolvedFile(
|
||||
id: string,
|
||||
hostId: Worktree['hostId'],
|
||||
relativePath: string
|
||||
): { worktree: Pick<Worktree, 'id' | 'hostId'>; relativePath: string } {
|
||||
return { worktree: { id, hostId }, relativePath }
|
||||
}
|
||||
|
||||
function connectedSshState(targetId: string, connectionGeneration: number): SshConnectionState {
|
||||
return {
|
||||
targetId,
|
||||
status: 'connected',
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
connectionGeneration
|
||||
}
|
||||
}
|
||||
|
||||
describe('paired web file mutation methods', () => {
|
||||
const assertMutationSupported = vi.fn(async (): Promise<readonly string[]> => [])
|
||||
const callRuntimeResult = vi.fn(async () => ({ ok: true }))
|
||||
const getSshState = vi.fn(async () => connectedSshState('hub-private-target', 17))
|
||||
const filesByPath = new Map([
|
||||
['/hub/repo/readme.md', resolvedFile('wt-local', 'local', 'readme.md')],
|
||||
['/hub/repo/new.md', resolvedFile('wt-local', 'local', 'new.md')],
|
||||
['/hub/repo/copy.md', resolvedFile('wt-local', 'local', 'copy.md')],
|
||||
['/hub/repo/new-dir', resolvedFile('wt-local', 'local', 'new-dir')],
|
||||
['/ssh/repo/source.md', resolvedFile('wt-ssh', 'ssh:hub-private-target', 'source.md')],
|
||||
['/ssh/repo/renamed.md', resolvedFile('wt-ssh', 'ssh:hub-private-target', 'renamed.md')],
|
||||
['/ssh/repo/copy.md', resolvedFile('wt-ssh', 'ssh:hub-private-target', 'copy.md')],
|
||||
['/ssh/repo/dir', resolvedFile('wt-ssh', 'ssh:hub-private-target', 'dir')]
|
||||
])
|
||||
const resolveFilePath = vi.fn(async (filePath: string) => {
|
||||
const file = filesByPath.get(filePath)
|
||||
if (!file) {
|
||||
throw new Error(`Unknown test path: ${filePath}`)
|
||||
}
|
||||
return file
|
||||
})
|
||||
const captureSession = vi.fn(() => ({
|
||||
assertMutationSupported,
|
||||
callRuntimeResult,
|
||||
getSshState,
|
||||
resolveFilePath
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
assertMutationSupported.mockClear()
|
||||
callRuntimeResult.mockClear()
|
||||
getSshState.mockClear()
|
||||
resolveFilePath.mockClear()
|
||||
captureSession.mockClear()
|
||||
})
|
||||
|
||||
it('keeps path resolution, capability, and mutation on one captured pairing session', async () => {
|
||||
const replacementCall = vi.fn(async () => ({ ok: true }))
|
||||
const replacementSession = {
|
||||
assertMutationSupported: vi.fn(async (): Promise<readonly string[]> => []),
|
||||
callRuntimeResult: replacementCall,
|
||||
getSshState,
|
||||
resolveFilePath
|
||||
}
|
||||
const capturedSession = {
|
||||
assertMutationSupported: vi.fn(async (): Promise<readonly string[]> => {
|
||||
captureSession.mockReturnValue(replacementSession)
|
||||
return []
|
||||
}),
|
||||
callRuntimeResult,
|
||||
getSshState,
|
||||
resolveFilePath
|
||||
}
|
||||
captureSession.mockReturnValueOnce(capturedSession)
|
||||
const methods = createWebFileMutationMethods({ captureSession })
|
||||
|
||||
await methods.writeFile({ filePath: '/hub/repo/readme.md', content: 'bound' })
|
||||
|
||||
expect(captureSession).toHaveBeenCalledTimes(1)
|
||||
expect(callRuntimeResult).toHaveBeenCalledWith('files.write', {
|
||||
worktree: 'id:wt-local',
|
||||
relativePath: 'readme.md',
|
||||
content: 'bound',
|
||||
expectedExecutionHostId: 'local'
|
||||
})
|
||||
expect(replacementCall).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user