mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 25 low-value tests and redundant parity suite (#34443)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11205,7 +11205,6 @@
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts": 173,
|
||||
"src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts": 702,
|
||||
"src/renderer/src/runtime/remote-server-install-failure-probe.test.ts": 28,
|
||||
"src/renderer/src/runtime/remote-server-parity.test.ts": 1246,
|
||||
"src/renderer/src/runtime/remote-server-restart-wait.test.ts": 72,
|
||||
"src/renderer/src/runtime/remote-server-update-coordinator.test.ts": 163,
|
||||
"src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts": 39,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { orcadPosixNodeRuntimeGlobs } from '../managed-server/install/orcad-node-runtime-globs'
|
||||
import {
|
||||
buildWslBrowserNetworkGuestInstallScript,
|
||||
buildWslBrowserNetworkGuestLaunchScript,
|
||||
resolveWslBrowserNetworkRelayBundle
|
||||
} from './wsl-browser-network-relay-launch'
|
||||
import { buildWslBrowserNetworkGuestInstallScript } from './wsl-browser-network-relay-launch'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
@@ -20,34 +15,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('WSL browser network relay launch', () => {
|
||||
it('builds a shell-valid exact-version launcher with a Node 18 floor', () => {
|
||||
const script = buildWslBrowserNetworkGuestLaunchScript('0.1.0+abc123')
|
||||
|
||||
execFileSync('sh', ['-n'], { input: script })
|
||||
expect(script).toContain('.orca-wsl/browser-network/0.1.0+abc123')
|
||||
expect(script).toContain('Number(process.versions.node.split(".")[0])>=18')
|
||||
expect(script).toContain('wsl-browser-network-relay.js')
|
||||
expect(() => buildWslBrowserNetworkGuestLaunchScript("bad'version")).toThrow(
|
||||
'browser_tunnel_execution_host_unavailable'
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to orcad pinned runtimes after system node, under a launcher-stamped version', () => {
|
||||
const forLine = buildWslBrowserNetworkGuestLaunchScript('0.1.0+abc')
|
||||
.split('\n')
|
||||
.find((line) => line.startsWith('for c in '))
|
||||
const [orcadRuntime, orcadMachineRuntimes] = orcadPosixNodeRuntimeGlobs()
|
||||
expect(forLine).toContain(`"$HOME/.local/bin/node" ${orcadRuntime} ${orcadMachineRuntimes}; do`)
|
||||
|
||||
const relayRoot = mkdtempSync(join(tmpdir(), 'orca-wsl-browser-network-bundle-'))
|
||||
roots.push(relayRoot)
|
||||
vi.stubEnv('ORCA_RELAY_PATH', relayRoot)
|
||||
mkdirSync(join(relayRoot, 'wsl'))
|
||||
writeFileSync(join(relayRoot, 'wsl', 'wsl-browser-network-relay.js'), '')
|
||||
writeFileSync(join(relayRoot, 'wsl', '.browser-network-version'), '0.1.0+abc\n')
|
||||
expect(resolveWslBrowserNetworkRelayBundle()?.version).toMatch(/^0\.1\.0\+abc\.[0-9a-f]{8}$/)
|
||||
})
|
||||
|
||||
it('installs exact bundle bytes and writes the version marker last', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-browser-network-'))
|
||||
roots.push(root)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup'
|
||||
import { getManagedWslCliDir } from './wsl-managed-cli'
|
||||
|
||||
const roots: string[] = []
|
||||
@@ -37,32 +36,9 @@ describe('managed WSL CLI provisioning', () => {
|
||||
expect(getManagedWslCliDir({ ...update, userDataPath: host.userDataPath })).not.toBe(directory)
|
||||
})
|
||||
|
||||
it('provides nothing when the packaged CLI runtime is missing', () => {
|
||||
const host = fixture()
|
||||
rmSync(join(host.resourcesPath, 'bin', 'orca.exe'))
|
||||
expect(getManagedWslCliDir(host)).toBeNull()
|
||||
})
|
||||
|
||||
it('provides nothing when user data cannot hold the CLI', () => {
|
||||
const host = fixture()
|
||||
writeFileSync(host.userDataPath, 'a file, not a directory')
|
||||
expect(getManagedWslCliDir(host)).toBeNull()
|
||||
})
|
||||
|
||||
it('runs the development CLI directly with the dev launcher env', () => {
|
||||
const host = fixture()
|
||||
const appPath = host.resourcesPath
|
||||
const cliEntryPath = join(appPath, 'out', 'cli', 'index.js')
|
||||
mkdirSync(join(appPath, 'out', 'cli'), { recursive: true })
|
||||
writeFileSync(cliEntryPath, 'fixture')
|
||||
installFakeAppEnvironment({ getPath: () => host.userDataPath, getAppPath: () => appPath })
|
||||
const directory = getManagedWslCliDir({ ...host, isPackaged: false }) ?? ''
|
||||
expect(readFileSync(join(directory, 'orca-dev'), 'utf8')).toContain(process.execPath)
|
||||
const bridge = readFileSync(join(directory, 'orca-wsl-bridge.ps1'), 'utf8')
|
||||
expect(bridge.startsWith('\uFEFF')).toBe(true)
|
||||
expect(bridge).toContain(host.userDataPath)
|
||||
expect(bridge).toContain(cliEntryPath)
|
||||
expect(bridge).toContain('$env:ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT')
|
||||
expect(bridge).toContain('Remove-Item Env:NODE_OPTIONS')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,35 +18,6 @@ beforeAll(() => {
|
||||
})
|
||||
afterAll(() => rmSync(sandbox.home, { recursive: true, force: true }))
|
||||
|
||||
describe('CodeBuddy managed hooks', () => {
|
||||
it('preserves user configuration through repeated install and removal', () => {
|
||||
const path = join(sandbox.home, '.codebuddy', 'settings.json')
|
||||
const userHook = { hooks: [{ type: 'command', command: 'echo user-hook' }] }
|
||||
const settings = {
|
||||
model: 'custom',
|
||||
hooks: { Stop: [userHook] },
|
||||
statusLine: { command: 'user-status' }
|
||||
}
|
||||
writeFileSync(path, JSON.stringify(settings))
|
||||
expect(codebuddyHookService.install().state).toBe('installed')
|
||||
expect(codebuddyHookService.install().state).toBe('installed')
|
||||
const installed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
for (const event of CODEBUDDY_HOOK_EVENTS) {
|
||||
expect(installed.hooks[event]).toHaveLength(event === 'Stop' ? 2 : 1)
|
||||
}
|
||||
expect(installed.statusLine).toEqual(settings.statusLine)
|
||||
expect(codebuddyHookService.remove().state).toBe('not_installed')
|
||||
expect(JSON.parse(readFileSync(path, 'utf8'))).toMatchObject(settings)
|
||||
})
|
||||
|
||||
it('leaves malformed user settings untouched', () => {
|
||||
const path = join(sandbox.home, '.codebuddy', 'settings.json')
|
||||
writeFileSync(path, '{broken')
|
||||
expect(codebuddyHookService.install().state).toBe('error')
|
||||
expect(readFileSync(path, 'utf8')).toBe('{broken')
|
||||
})
|
||||
})
|
||||
|
||||
describe('CodeBuddy managed hooks on Windows', () => {
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
afterEach(() => Object.defineProperty(process, 'platform', platform))
|
||||
|
||||
@@ -39,19 +39,6 @@ describe('withGitHubCheckDetailsTimeout', () => {
|
||||
expect(operationSignal?.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('arms the renderer deadline before starting the operation', async () => {
|
||||
let timerCountWhenStarted = 0
|
||||
|
||||
const result = withGitHubCheckDetailsTimeout(async () => {
|
||||
timerCountWhenStarted = vi.getTimerCount()
|
||||
return 'details'
|
||||
})
|
||||
|
||||
await expect(result).resolves.toBe('details')
|
||||
expect(timerCountWhenStarted).toBe(1)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps the timeout error when abort-aware work rejects synchronously', async () => {
|
||||
const stalled = withGitHubCheckDetailsTimeout(
|
||||
(signal) =>
|
||||
@@ -76,10 +63,4 @@ describe('withGitHubCheckDetailsTimeout', () => {
|
||||
withGitHubCheckDetailsTimeout(() => Promise.reject(new Error(message)))
|
||||
).rejects.toMatchObject({ message: 'Localized timeout.' })
|
||||
})
|
||||
|
||||
it('preserves unrelated operation failures', async () => {
|
||||
await expect(
|
||||
withGitHubCheckDetailsTimeout(() => Promise.reject(new Error('authentication failed')))
|
||||
).rejects.toThrow('authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,58 +25,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('loadGitLabJobLogDetails', () => {
|
||||
it('returns null for a check that is not a GitLab job, touching no transport', async () => {
|
||||
const githubCheck: PRCheckDetail = {
|
||||
name: 'verify',
|
||||
status: 'completed',
|
||||
conclusion: 'failure',
|
||||
url: 'https://github.com/acme/orca/runs/1',
|
||||
checkRunId: 7
|
||||
}
|
||||
|
||||
await expect(
|
||||
loadGitLabJobLogDetails({ repoPath: '/repo', check: githubCheck })
|
||||
).resolves.toBeNull()
|
||||
expect(jobTrace).not.toHaveBeenCalled()
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('asks main for a bounded excerpt over local IPC', async () => {
|
||||
jobTrace.mockResolvedValue({ ok: true, trace: 'ERROR: Job failed: exit code 1' })
|
||||
|
||||
const details = await loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo-1',
|
||||
check: gitLabCheck
|
||||
})
|
||||
|
||||
expect(jobTrace).toHaveBeenCalledWith({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo-1',
|
||||
jobId: 42,
|
||||
projectRef: null,
|
||||
logExcerpt: true
|
||||
})
|
||||
expect(details?.jobs[0]?.logTail).toContain('ERROR: Job failed: exit code 1')
|
||||
})
|
||||
|
||||
it('targets the MR project so a fork pipeline trace is not looked up in the wrong project', async () => {
|
||||
jobTrace.mockResolvedValue({ ok: true, trace: 'boom' })
|
||||
|
||||
await loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo-1',
|
||||
check: gitLabCheck,
|
||||
projectRef: { host: 'gitlab.example.test', path: 'contributor/orca' }
|
||||
})
|
||||
|
||||
expect(jobTrace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectRef: { host: 'gitlab.example.test', path: 'contributor/orca' }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does not request a trace for a manual job that cannot have one', async () => {
|
||||
const manualCheck: PRCheckDetail = {
|
||||
name: 'deploy: production',
|
||||
@@ -98,57 +46,6 @@ describe('loadGitLabJobLogDetails', () => {
|
||||
expect(details?.summary).toBe('No log is available for this GitLab job.')
|
||||
})
|
||||
|
||||
// Canceled-before-start jobs are completed/cancelled; fetch and show main’s empty trace without error (#7732).
|
||||
it('explains an empty trace for a job canceled before it produced output', async () => {
|
||||
jobTrace.mockResolvedValue({ ok: true, trace: '' })
|
||||
const canceledCheck: PRCheckDetail = {
|
||||
name: 'unit',
|
||||
status: 'completed',
|
||||
conclusion: 'cancelled',
|
||||
url: null,
|
||||
gitlabJobId: 5
|
||||
}
|
||||
|
||||
const details = await loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
check: canceledCheck
|
||||
})
|
||||
|
||||
expect(details?.jobs).toEqual([])
|
||||
expect(details?.summary).toBe('No log is available for this GitLab job.')
|
||||
})
|
||||
|
||||
it('routes to the owning runtime environment and still requests the excerpt', async () => {
|
||||
callRuntimeRpc.mockResolvedValue({ ok: true, trace: 'remote failure' })
|
||||
|
||||
await loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo-1',
|
||||
repoOwnerExecutionHostId: 'runtime:env-9',
|
||||
check: gitLabCheck
|
||||
})
|
||||
|
||||
expect(jobTrace).not.toHaveBeenCalled()
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-9' },
|
||||
'gitlab.jobTrace',
|
||||
// Why: bounding in main keeps the response under the 1 MB transport frame cap.
|
||||
expect.objectContaining({ repo: 'id:repo-1', jobId: 42, projectRef: null, logExcerpt: true }),
|
||||
{ timeoutMs: 65_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('surfaces the GitLab error verbatim so the panel does not claim "no details"', async () => {
|
||||
jobTrace.mockResolvedValue({ ok: false, error: '403 Forbidden' })
|
||||
|
||||
await expect(
|
||||
loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
check: gitLabCheck
|
||||
})
|
||||
).rejects.toThrow('403 Forbidden')
|
||||
})
|
||||
|
||||
it('gives up on a local IPC call that never settles, matching the remote timeout', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
@@ -165,15 +62,4 @@ describe('loadGitLabJobLogDetails', () => {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to a translated message when GitLab reports a blank error', async () => {
|
||||
jobTrace.mockResolvedValue({ ok: false, error: ' ' })
|
||||
|
||||
await expect(
|
||||
loadGitLabJobLogDetails({
|
||||
repoPath: '/repo',
|
||||
check: gitLabCheck
|
||||
})
|
||||
).rejects.toThrow('Failed to load the GitLab job log.')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ vi.mock('./runtime-rpc-client', () => ({
|
||||
}))
|
||||
|
||||
import { gitLabApiFor } from './gitlab-owner-api'
|
||||
import { forgeCredentialTarget, forgeOwnerHostIdForWorkspace } from './forge-credential-target'
|
||||
import { forgeCredentialTarget } from './forge-credential-target'
|
||||
|
||||
const glListIssues = vi.fn()
|
||||
|
||||
@@ -20,46 +20,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('gitLabApiFor', () => {
|
||||
it('reads a server-owned repo on that server', async () => {
|
||||
const selector = {
|
||||
repoPath: '/work/app',
|
||||
repoId: 'server-repo',
|
||||
repoOwnerExecutionHostId: 'runtime:owner-server'
|
||||
}
|
||||
await gitLabApiFor(selector).listIssues({ ...selector, state: 'opened' })
|
||||
|
||||
expect(glListIssues).not.toHaveBeenCalled()
|
||||
expect(rpc.call).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'owner-server' },
|
||||
'gitlab.listIssues',
|
||||
expect.objectContaining({ repo: 'id:server-repo', state: 'opened' }),
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it("loads a server-owned repo's reviewers from that server", async () => {
|
||||
const reviewer = {
|
||||
id: 7,
|
||||
username: 'ana',
|
||||
name: 'Ana',
|
||||
avatarUrl: 'https://gitlab.example.com/ana.png'
|
||||
}
|
||||
rpc.call.mockResolvedValue([reviewer])
|
||||
const selector = {
|
||||
repoPath: '/work/app',
|
||||
repoId: 'server-repo',
|
||||
repoOwnerExecutionHostId: 'runtime:owner-server'
|
||||
}
|
||||
|
||||
await expect(gitLabApiFor(selector).listAssignableUsers(selector)).resolves.toEqual([reviewer])
|
||||
expect(rpc.call).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'owner-server' },
|
||||
'gitlab.listAssignableUsers',
|
||||
expect.objectContaining({ repo: 'id:server-repo' }),
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('offers no reviewers from a server older than the reviewer RPC', async () => {
|
||||
rpc.call.mockRejectedValue(
|
||||
Object.assign(new Error('Unknown method'), { code: 'method_not_found' })
|
||||
@@ -86,17 +46,6 @@ describe('gitLabApiFor', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['ssh:devbox', 'local'])(
|
||||
'uses this computer for a %s repo',
|
||||
async (repoOwnerExecutionHostId) => {
|
||||
const selector = { repoPath: '/work/app', repoId: 'repo', repoOwnerExecutionHostId }
|
||||
await gitLabApiFor(selector).listIssues(selector)
|
||||
|
||||
expect(rpc.call).not.toHaveBeenCalled()
|
||||
expect(glListIssues).toHaveBeenCalledOnce()
|
||||
}
|
||||
)
|
||||
|
||||
it('lets a task source name the host and the server-side repo id', async () => {
|
||||
const selector = {
|
||||
repoPath: '/work/app',
|
||||
@@ -133,25 +82,4 @@ describe('gitLabApiFor', () => {
|
||||
expect(rpc.call).not.toHaveBeenCalled()
|
||||
expect(glListIssues).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends an SSH workspace behind a server to that server, a direct SSH one to this computer', async () => {
|
||||
const repo = { executionHostId: 'runtime:owner-server' as const }
|
||||
const nested = forgeOwnerHostIdForWorkspace(repo, {
|
||||
hostId: 'ssh:devbox',
|
||||
runtimeOwnerEnvironmentId: 'owner-server'
|
||||
})
|
||||
const selector = { repoPath: '/work/app', repoId: 'repo', repoOwnerExecutionHostId: nested }
|
||||
await gitLabApiFor(selector).listIssues(selector)
|
||||
|
||||
expect(rpc.call).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'owner-server' },
|
||||
'gitlab.listIssues',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
)
|
||||
expect(forgeOwnerHostIdForWorkspace({ connectionId: 'devbox' }, { hostId: 'ssh:devbox' })).toBe(
|
||||
'ssh:devbox'
|
||||
)
|
||||
expect(forgeOwnerHostIdForWorkspace({ connectionId: 'devbox' }, null)).toBe('ssh:devbox')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
// Exercise deterministic server-authoritative reconcile against local tab ordering/focus/disconnect semantics (remote-server-parity-test-plan.md). Live checklist covers geometry, sidebar, screencast, mobile and two-host UI flows.
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
|
||||
import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id'
|
||||
import type { Tab } from '../../../shared/tab-types'
|
||||
import {
|
||||
applyFreshWebSessionTabsSnapshot,
|
||||
applyWebSessionTabsSnapshot
|
||||
} from './web-session-tabs-sync/snapshot-api'
|
||||
import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle'
|
||||
import type { WebSessionTabsSyncState } from './web-session-tabs-sync/state'
|
||||
import {
|
||||
recordWebSessionFocusIntent,
|
||||
resetWebSessionFocusIntentForTests
|
||||
} from './web-session-focus-intent'
|
||||
|
||||
const WT = 'repo::/worktree'
|
||||
const ENV = 'web-env-1'
|
||||
const NOW = 1_700_000_000_000
|
||||
const LEAF_A = '11111111-1111-4111-8111-111111111111'
|
||||
const LEAF_B = '22222222-2222-4222-8222-222222222222'
|
||||
const LEAF_C = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
function makeState(overrides: Partial<WebSessionTabsSyncState> = {}): WebSessionTabsSyncState {
|
||||
return {
|
||||
activeBrowserTabId: null,
|
||||
activeBrowserTabIdByWorktree: {},
|
||||
activeFileId: null,
|
||||
activeFileIdByWorktree: {},
|
||||
activeGroupIdByWorktree: {},
|
||||
activeTabId: null,
|
||||
activeTabIdByWorktree: {},
|
||||
activeTabType: 'terminal',
|
||||
activeTabTypeByWorktree: {},
|
||||
activeWorktreeId: WT,
|
||||
agentStatusByPaneKey: {},
|
||||
agentStatusEpoch: 0,
|
||||
browserCertificateFailuresByPageId: {},
|
||||
browserPagesByWorkspace: {},
|
||||
browserTabsByWorktree: {},
|
||||
groupsByWorktree: {},
|
||||
layoutByWorktree: {},
|
||||
openFiles: [],
|
||||
editorBuffers: {},
|
||||
ptyIdsByTabId: {},
|
||||
remoteBrowserPageHandlesByPageId: {},
|
||||
tabBarOrderByWorktree: {},
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
unreadTerminalTabs: {},
|
||||
sortEpoch: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
type HostTerminal = { parentTab: string; leaf: string; active: boolean; title?: string }
|
||||
|
||||
function terminalSnapshotTab(t: HostTerminal): RuntimeMobileSessionTabsResult['tabs'][number] {
|
||||
return {
|
||||
type: 'terminal',
|
||||
id: `${t.parentTab}::${t.leaf}`,
|
||||
title: t.title ?? t.parentTab,
|
||||
parentTabId: t.parentTab,
|
||||
leafId: t.leaf,
|
||||
isActive: t.active,
|
||||
status: 'ready',
|
||||
terminal: `${t.parentTab}-pty`
|
||||
}
|
||||
}
|
||||
|
||||
function makeSnapshot(
|
||||
terminals: HostTerminal[],
|
||||
overrides: Partial<RuntimeMobileSessionTabsResult> = {}
|
||||
): RuntimeMobileSessionTabsResult {
|
||||
const tabs = terminals.map(terminalSnapshotTab)
|
||||
const active = terminals.find((t) => t.active)
|
||||
return {
|
||||
worktree: WT,
|
||||
publicationEpoch: `epoch-${overrides.snapshotVersion ?? 1}`,
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: 'host-group-1',
|
||||
activeTabId: active ? `${active.parentTab}::${active.leaf}` : null,
|
||||
activeTabType: active ? 'terminal' : null,
|
||||
tabGroups: [
|
||||
{
|
||||
id: 'host-group-1',
|
||||
activeTabId: active?.parentTab ?? null,
|
||||
tabOrder: terminals.map((t) => t.parentTab)
|
||||
}
|
||||
],
|
||||
tabs,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
// Seed realistic prior terminal/unified-tab/group state for snapshot reconciliation.
|
||||
function localTerminal(parentTab: string, sortOrder: number, active: boolean) {
|
||||
const id = toWebTerminalSurfaceTabId(parentTab)
|
||||
const unified: Tab = {
|
||||
id,
|
||||
entityId: id,
|
||||
groupId: 'host-group-1',
|
||||
worktreeId: WT,
|
||||
contentType: 'terminal',
|
||||
label: parentTab,
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder,
|
||||
createdAt: NOW + sortOrder,
|
||||
isPreview: false,
|
||||
isPinned: false
|
||||
}
|
||||
return {
|
||||
id,
|
||||
parentTab,
|
||||
active,
|
||||
storeTab: {
|
||||
id,
|
||||
ptyId: `remote:${ENV}@@${parentTab}-pty`,
|
||||
worktreeId: WT,
|
||||
title: parentTab,
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder,
|
||||
createdAt: NOW + sortOrder
|
||||
},
|
||||
unified
|
||||
}
|
||||
}
|
||||
|
||||
function stateWithLocalTerminals(
|
||||
locals: ReturnType<typeof localTerminal>[]
|
||||
): WebSessionTabsSyncState {
|
||||
const active = locals.find((l) => l.active) ?? locals[0]
|
||||
return makeState({
|
||||
activeTabId: active?.id ?? null,
|
||||
activeTabIdByWorktree: active ? { [WT]: active.id } : {},
|
||||
activeTabType: 'terminal',
|
||||
activeTabTypeByWorktree: { [WT]: 'terminal' },
|
||||
tabsByWorktree: { [WT]: locals.map((l) => l.storeTab) },
|
||||
unifiedTabsByWorktree: { [WT]: locals.map((l) => l.unified) },
|
||||
tabBarOrderByWorktree: { [WT]: locals.map((l) => l.id) },
|
||||
groupsByWorktree: {
|
||||
[WT]: [
|
||||
{
|
||||
id: 'host-group-1',
|
||||
worktreeId: WT,
|
||||
activeTabId: active?.id ?? null,
|
||||
tabOrder: locals.map((l) => l.id),
|
||||
recentTabIds: locals.map((l) => l.id)
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function groupOrder(patch: Partial<WebSessionTabsSyncState>): string[] {
|
||||
return patch.groupsByWorktree?.[WT]?.[0]?.tabOrder ?? []
|
||||
}
|
||||
function groupActive(patch: Partial<WebSessionTabsSyncState>): string | null {
|
||||
return patch.groupsByWorktree?.[WT]?.[0]?.activeTabId ?? null
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetWebSessionTabsSnapshotFreshnessForTests()
|
||||
resetWebSessionFocusIntentForTests()
|
||||
})
|
||||
|
||||
describe('parity §3: remote terminal create appends rightmost (matches local)', () => {
|
||||
it('a newly created remote terminal is ordered last, after existing tabs', () => {
|
||||
const prior = stateWithLocalTerminals([
|
||||
localTerminal('host-tab-1', 0, true),
|
||||
localTerminal('host-tab-2', 1, false)
|
||||
])
|
||||
recordWebSessionFocusIntent({ environmentId: ENV }, WT, `host-tab-3::${LEAF_C}`)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
prior,
|
||||
makeSnapshot([
|
||||
{ parentTab: 'host-tab-1', leaf: LEAF_A, active: false },
|
||||
{ parentTab: 'host-tab-2', leaf: LEAF_B, active: false },
|
||||
{ parentTab: 'host-tab-3', leaf: LEAF_C, active: true }
|
||||
]),
|
||||
ENV,
|
||||
NOW + 10
|
||||
) as Partial<WebSessionTabsSyncState>
|
||||
|
||||
const order = groupOrder(patch)
|
||||
// Local rule: new tab appends to the end. The new one must be last.
|
||||
expect(order.at(-1)).toBe(toWebTerminalSurfaceTabId('host-tab-3'))
|
||||
expect(order).toEqual([
|
||||
toWebTerminalSurfaceTabId('host-tab-1'),
|
||||
toWebTerminalSurfaceTabId('host-tab-2'),
|
||||
toWebTerminalSurfaceTabId('host-tab-3')
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parity §4: remote create focuses new tab; echoes never steal focus', () => {
|
||||
it('a client-initiated create focuses the new terminal (intent honored)', () => {
|
||||
const prior = stateWithLocalTerminals([localTerminal('host-tab-1', 0, true)])
|
||||
recordWebSessionFocusIntent({ environmentId: ENV }, WT, `host-tab-2::${LEAF_B}`)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
prior,
|
||||
makeSnapshot([
|
||||
{ parentTab: 'host-tab-1', leaf: LEAF_A, active: false },
|
||||
{ parentTab: 'host-tab-2', leaf: LEAF_B, active: true }
|
||||
]),
|
||||
ENV,
|
||||
NOW + 10
|
||||
) as Partial<WebSessionTabsSyncState>
|
||||
expect(patch.activeTabIdByWorktree?.[WT]).toBe(toWebTerminalSurfaceTabId('host-tab-2'))
|
||||
expect(groupActive(patch)).toBe(toWebTerminalSurfaceTabId('host-tab-2'))
|
||||
})
|
||||
|
||||
it('an unsolicited status echo (no intent) does NOT steal focus from current', () => {
|
||||
const prior = stateWithLocalTerminals([
|
||||
localTerminal('host-tab-1', 0, false),
|
||||
localTerminal('host-tab-2', 1, true)
|
||||
])
|
||||
// No focus intent recorded — this is an agent "thinking" echo marking tab-1 active.
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
prior,
|
||||
makeSnapshot([
|
||||
{ parentTab: 'host-tab-1', leaf: LEAF_A, active: true },
|
||||
{ parentTab: 'host-tab-2', leaf: LEAF_B, active: false }
|
||||
]),
|
||||
ENV,
|
||||
NOW + 10
|
||||
) as Partial<WebSessionTabsSyncState>
|
||||
// Status echoes cannot steal focus; client-owned placement leaves groups unchanged.
|
||||
const effectiveGroups = patch.groupsByWorktree?.[WT] ?? prior.groupsByWorktree[WT]
|
||||
expect(effectiveGroups?.[0]?.activeTabId).toBe(toWebTerminalSurfaceTabId('host-tab-2'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('parity §2: snapshot freshness (no split-brain from stale updates)', () => {
|
||||
it('ignores a stale lower-version snapshot after a newer version applied', () => {
|
||||
const prior = stateWithLocalTerminals([localTerminal('host-tab-1', 0, true)])
|
||||
const newer = makeSnapshot([{ parentTab: 'host-tab-1', leaf: LEAF_A, active: true }], {
|
||||
snapshotVersion: 3,
|
||||
publicationEpoch: 'epoch-x'
|
||||
})
|
||||
const older = makeSnapshot([{ parentTab: 'host-tab-1', leaf: LEAF_A, active: true }], {
|
||||
snapshotVersion: 2,
|
||||
publicationEpoch: 'epoch-x'
|
||||
})
|
||||
const first = applyFreshWebSessionTabsSnapshot(prior, newer, ENV, NOW + 10)
|
||||
const afterNewer = { ...prior, ...(first as Partial<WebSessionTabsSyncState>) }
|
||||
const second = applyFreshWebSessionTabsSnapshot(afterNewer, older, ENV, NOW + 20)
|
||||
expect(second).toBe(afterNewer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parity §11: remote browser create focuses the new browser tab', () => {
|
||||
it('honors focus intent for a newly created browser session tab', () => {
|
||||
const pageId = 'browser-page-1'
|
||||
const prior = stateWithLocalTerminals([localTerminal('host-tab-1', 0, true)])
|
||||
recordWebSessionFocusIntent(
|
||||
{ environmentId: ENV },
|
||||
WT,
|
||||
pageId,
|
||||
undefined,
|
||||
toWebTerminalSurfaceTabId('host-tab-1')
|
||||
)
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
prior,
|
||||
makeSnapshot([{ parentTab: 'host-tab-1', leaf: LEAF_A, active: false }], {
|
||||
activeTabId: pageId,
|
||||
activeTabType: 'browser',
|
||||
tabGroups: [
|
||||
{
|
||||
id: 'host-group-1',
|
||||
activeTabId: pageId,
|
||||
tabOrder: ['host-tab-1', pageId]
|
||||
}
|
||||
],
|
||||
tabs: [
|
||||
terminalSnapshotTab({ parentTab: 'host-tab-1', leaf: LEAF_A, active: false }),
|
||||
{
|
||||
type: 'browser',
|
||||
id: pageId,
|
||||
title: 'example.com',
|
||||
browserWorkspaceId: pageId,
|
||||
browserPageId: pageId,
|
||||
url: 'https://example.com',
|
||||
loading: false,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
}),
|
||||
ENV,
|
||||
NOW + 10
|
||||
) as Partial<WebSessionTabsSyncState>
|
||||
expect(patch.activeTabTypeByWorktree?.[WT]).toBe('browser')
|
||||
expect(patch.activeBrowserTabIdByWorktree?.[WT]).toBe(pageId)
|
||||
})
|
||||
|
||||
it('a browser status echo without intent does not steal focus from a terminal', () => {
|
||||
const pageId = 'browser-page-1'
|
||||
const prior = makeState({
|
||||
activeTabId: toWebTerminalSurfaceTabId('host-tab-1'),
|
||||
activeTabIdByWorktree: { [WT]: toWebTerminalSurfaceTabId('host-tab-1') },
|
||||
activeTabType: 'terminal',
|
||||
activeTabTypeByWorktree: { [WT]: 'terminal' },
|
||||
tabsByWorktree: { [WT]: [localTerminal('host-tab-1', 0, true).storeTab] },
|
||||
unifiedTabsByWorktree: { [WT]: [localTerminal('host-tab-1', 0, true).unified] },
|
||||
tabBarOrderByWorktree: { [WT]: [toWebTerminalSurfaceTabId('host-tab-1')] },
|
||||
groupsByWorktree: {
|
||||
[WT]: [
|
||||
{
|
||||
id: 'host-group-1',
|
||||
worktreeId: WT,
|
||||
activeTabId: toWebTerminalSurfaceTabId('host-tab-1'),
|
||||
tabOrder: [toWebTerminalSurfaceTabId('host-tab-1')],
|
||||
recentTabIds: [toWebTerminalSurfaceTabId('host-tab-1')]
|
||||
}
|
||||
]
|
||||
},
|
||||
browserTabsByWorktree: {
|
||||
[WT]: [
|
||||
{
|
||||
id: pageId,
|
||||
worktreeId: WT,
|
||||
activePageId: pageId,
|
||||
pageIds: [pageId],
|
||||
url: 'https://example.com',
|
||||
title: 'example.com',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: NOW
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
// No intent — server snapshot marks the (pre-existing) browser active.
|
||||
const patch = applyWebSessionTabsSnapshot(
|
||||
prior,
|
||||
makeSnapshot([{ parentTab: 'host-tab-1', leaf: LEAF_A, active: false }], {
|
||||
activeTabId: pageId,
|
||||
activeTabType: 'browser',
|
||||
tabGroups: [{ id: 'host-group-1', activeTabId: pageId, tabOrder: ['host-tab-1', pageId] }],
|
||||
tabs: [
|
||||
terminalSnapshotTab({ parentTab: 'host-tab-1', leaf: LEAF_A, active: false }),
|
||||
{
|
||||
type: 'browser',
|
||||
id: pageId,
|
||||
title: 'example.com',
|
||||
browserWorkspaceId: pageId,
|
||||
browserPageId: pageId,
|
||||
url: 'https://example.com',
|
||||
loading: false,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
}),
|
||||
ENV,
|
||||
NOW + 10
|
||||
) as Partial<WebSessionTabsSyncState>
|
||||
// Unchanged reconcile fields may be absent; visible type must remain terminal, never browser.
|
||||
const nextType = patch.activeTabTypeByWorktree?.[WT] ?? 'terminal'
|
||||
expect(nextType).toBe('terminal')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user