mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 61 low-value runtime tests and five suites (#34431)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -10855,7 +10855,7 @@
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/settings.test.ts src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts src/renderer/src/store/slices/settings.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/components/settings/browser-session-host-selection.test.ts src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts src/renderer/src/components/status-bar/SshStatusSegment.test.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-catalog-reconciliation.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/runtime/runtime-server-directory-browser.test.ts src/renderer/src/store/slices/repos-paired-runtime-add.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/store/slices/repos-paired-runtime-add.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-catalog-reconciliation.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts src/renderer/src/runtime/web-runtime-session.test.ts --maxWorkers=1",
|
||||
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --reporter=line",
|
||||
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
|
||||
|
||||
@@ -11235,7 +11235,6 @@
|
||||
"src/renderer/src/runtime/runtime-hooks-client.test.ts": 132,
|
||||
"src/renderer/src/runtime/runtime-host-account-sign-in-client.test.ts": 189,
|
||||
"src/renderer/src/runtime/runtime-host-connection-state.test.ts": 35,
|
||||
"src/renderer/src/runtime/runtime-host-contact-parity.test.ts": 40,
|
||||
"src/renderer/src/runtime/runtime-host-contact-regained.test.ts": 408,
|
||||
"src/renderer/src/runtime/runtime-jira-board-filter-compatibility.test.ts": 527,
|
||||
"src/renderer/src/runtime/runtime-jira-client.test.ts": 347,
|
||||
@@ -11245,8 +11244,6 @@
|
||||
"src/renderer/src/runtime/runtime-jira-image-cache.test.ts": 1065,
|
||||
"src/renderer/src/runtime/runtime-jira-image-client.test.ts": 140,
|
||||
"src/renderer/src/runtime/runtime-jira-payload-stream.test.ts": 165,
|
||||
"src/renderer/src/runtime/runtime-jira-saved-filter-state.test.ts": 162,
|
||||
"src/renderer/src/runtime/runtime-jira-status-filters.test.ts": 199,
|
||||
"src/renderer/src/runtime/runtime-jira-summary-generation-client.test.ts": 159,
|
||||
"src/renderer/src/runtime/runtime-layout-client.test.ts": 109,
|
||||
"src/renderer/src/runtime/runtime-legacy-inventory-budget.test.ts": 73,
|
||||
@@ -11267,10 +11264,8 @@
|
||||
"src/renderer/src/runtime/runtime-rpc-client-status-publication.test.ts": 234,
|
||||
"src/renderer/src/runtime/runtime-rpc-client.test.ts": 129,
|
||||
"src/renderer/src/runtime/runtime-rpc-result.test.ts": 44,
|
||||
"src/renderer/src/runtime/runtime-server-directory-browser.test.ts": 150,
|
||||
"src/renderer/src/runtime/runtime-skills-client.test.ts": 147,
|
||||
"src/renderer/src/runtime/runtime-skills-delete-client.test.ts": 57,
|
||||
"src/renderer/src/runtime/runtime-ssh-target-management.test.ts": 63,
|
||||
"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,
|
||||
|
||||
@@ -3,10 +3,8 @@ import type { SshConnectionState, SshProviderEpoch } from '../../../shared/ssh-t
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
applyRuntimeEnvironmentSshStateChanged,
|
||||
connectRuntimeEnvironmentSshTarget,
|
||||
hydrateRuntimeEnvironmentSshState,
|
||||
refreshRuntimeEnvironmentSshTargetMetadata,
|
||||
resyncRuntimeEnvironmentSshTargets
|
||||
refreshRuntimeEnvironmentSshTargetMetadata
|
||||
} from './runtime-environment-ssh-state'
|
||||
import { callRuntimeRpc } from './runtime-rpc-client'
|
||||
|
||||
@@ -74,52 +72,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('hydrateRuntimeEnvironmentSshState', () => {
|
||||
it('populates the environment bucket with targets, tombstones, and per-target states', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({
|
||||
targets: [
|
||||
{ id: 'ssh-1', label: 'devbox' },
|
||||
{ id: 'ssh-2', label: 'buildbox' }
|
||||
],
|
||||
labels: { 'ssh-old': 'retired box' },
|
||||
states: { 'ssh-1': connState('ssh-1', 'connected') }
|
||||
})
|
||||
|
||||
await hydrateRuntimeEnvironmentSshState(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetsHydrated).toBe(true)
|
||||
expect(bucket?.targetLabels.get('ssh-1')).toBe('devbox')
|
||||
expect(bucket?.removedTargetLabels.get('ssh-old')).toBe('retired box')
|
||||
expect(bucket?.connectionStates.get('ssh-1')?.status).toBe('connected')
|
||||
expect(bucket?.connectionStates.get('ssh-1')).toMatchObject({
|
||||
providerEpoch: 'ssh-1-provider-epoch',
|
||||
connectionGeneration: 7
|
||||
})
|
||||
// ssh-2 had no live state: absent, so reads fall back to 'disconnected'.
|
||||
expect(bucket?.connectionStates.has('ssh-2')).toBe(false)
|
||||
// Local maps stay untouched.
|
||||
expect(useAppStore.getState().sshTargetLabels.size).toBe(0)
|
||||
expect(useAppStore.getState().sshTargetsHydrated).toBe(false)
|
||||
// Every call was routed to the owning environment.
|
||||
for (const [target] of callRuntimeRpcMock.mock.calls) {
|
||||
expect(target).toEqual({ kind: 'environment', environmentId: envId })
|
||||
}
|
||||
})
|
||||
|
||||
it('skips refetching when already hydrated unless forced', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({ targets: [] })
|
||||
await hydrateRuntimeEnvironmentSshState(envId)
|
||||
const callsAfterFirst = callRuntimeRpcMock.mock.calls.length
|
||||
|
||||
await hydrateRuntimeEnvironmentSshState(envId)
|
||||
expect(callRuntimeRpcMock.mock.calls.length).toBe(callsAfterFirst)
|
||||
|
||||
await hydrateRuntimeEnvironmentSshState(envId, { force: true })
|
||||
expect(callRuntimeRpcMock.mock.calls.length).toBeGreaterThan(callsAfterFirst)
|
||||
})
|
||||
|
||||
it('does not rerun when an ordinary refresh joins a forced hydration', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
@@ -155,41 +107,6 @@ describe('hydrateRuntimeEnvironmentSshState', () => {
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('hydrates without force after the environment bucket is marked stale', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshTargetsMetadata(envId, [{ id: 'ssh-old', label: 'old box' }])
|
||||
useAppStore.getState().markEnvironmentSshStateStale(envId)
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-new', label: 'new box' }],
|
||||
states: { 'ssh-new': connState('ssh-new') }
|
||||
})
|
||||
|
||||
await hydrateRuntimeEnvironmentSshState(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetsHydrated).toBe(true)
|
||||
expect(bucket?.targetLabels.get('ssh-new')).toBe('new box')
|
||||
expect(bucket?.connectionStates.get('ssh-new')?.status).toBe('connected')
|
||||
})
|
||||
|
||||
it('refreshes only target summaries when hydrated metadata is unchanged', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-1', label: 'devbox' }],
|
||||
states: { 'ssh-1': connState('ssh-1') }
|
||||
})
|
||||
await hydrateRuntimeEnvironmentSshState(envId)
|
||||
callRuntimeRpcMock.mockClear()
|
||||
|
||||
await refreshRuntimeEnvironmentSshTargetMetadata(envId)
|
||||
|
||||
expect(callRuntimeRpcMock.mock.calls.map(([, method]) => method)).toEqual([
|
||||
'ssh.listTargetSummaries'
|
||||
])
|
||||
})
|
||||
|
||||
it('reads state for a labelled target the bucket never recorded a state for', async () => {
|
||||
const envId = nextEnvId()
|
||||
// A failed-connect resync can label a target without ever reading its state.
|
||||
@@ -326,16 +243,6 @@ describe('hydrateRuntimeEnvironmentSshState', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves the bucket un-hydrated when the host lacks the ssh RPC methods', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({ failListTargets: true })
|
||||
|
||||
await expect(hydrateRuntimeEnvironmentSshState(envId)).rejects.toThrow()
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetsHydrated ?? false).toBe(false)
|
||||
})
|
||||
|
||||
it('still hydrates the target list when the removed-labels fetch fails', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({
|
||||
@@ -400,54 +307,9 @@ describe('hydrateRuntimeEnvironmentSshState', () => {
|
||||
|
||||
expect(useAppStore.getState().sshStateByEnvironment.has(envId)).toBe(false)
|
||||
})
|
||||
|
||||
it('prunes connection state for targets removed by an authoritative refresh', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore.getState().setEnvironmentSshTargetsMetadata(envId, [
|
||||
{ id: 'ssh-live', label: 'live box' },
|
||||
{ id: 'ssh-removed', label: 'retired box' }
|
||||
])
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshConnectionState(envId, 'ssh-live', connState('ssh-live'))
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshConnectionState(envId, 'ssh-removed', connState('ssh-removed'))
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-live', label: 'live box' }],
|
||||
states: { 'ssh-live': connState('ssh-live') }
|
||||
})
|
||||
|
||||
await hydrateRuntimeEnvironmentSshState(envId, { force: true })
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetLabels.has('ssh-removed')).toBe(false)
|
||||
expect(bucket?.connectionStates.has('ssh-removed')).toBe(false)
|
||||
expect(bucket?.connectionStates.get('ssh-live')?.status).toBe('connected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyRuntimeEnvironmentSshStateChanged', () => {
|
||||
it('applies a known target state directly into the owning bucket without RPC', () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshTargetsMetadata(envId, [{ id: 'ssh-1', label: 'devbox' }])
|
||||
|
||||
applyRuntimeEnvironmentSshStateChanged(envId, 'ssh-1', connState('ssh-1', 'disconnected'))
|
||||
|
||||
expect(
|
||||
useAppStore.getState().sshStateByEnvironment.get(envId)?.connectionStates.get('ssh-1')
|
||||
).toMatchObject({
|
||||
status: 'disconnected',
|
||||
providerEpoch: 'ssh-1-provider-epoch',
|
||||
connectionGeneration: 7
|
||||
})
|
||||
expect(callRuntimeRpcMock).not.toHaveBeenCalled()
|
||||
// Local map untouched.
|
||||
expect(useAppStore.getState().sshConnectionStates.size).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects partial authority before retaining a runtime-owned state', () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
@@ -502,95 +364,4 @@ describe('applyRuntimeEnvironmentSshStateChanged', () => {
|
||||
expect(bucket?.connectionStates.has('ssh-gone')).toBe(false)
|
||||
expect(bucket?.removedTargetLabels.get('ssh-gone')).toBe('old devbox')
|
||||
})
|
||||
|
||||
it('picks up a just-added target through the forced refresh', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-new', label: 'fresh box' }],
|
||||
states: { 'ssh-new': connState('ssh-new', 'connecting') }
|
||||
})
|
||||
|
||||
applyRuntimeEnvironmentSshStateChanged(envId, 'ssh-new', connState('ssh-new', 'connecting'))
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
useAppStore.getState().sshStateByEnvironment.get(envId)?.targetLabels.get('ssh-new')
|
||||
).toBe('fresh box')
|
||||
})
|
||||
|
||||
expect(
|
||||
useAppStore.getState().sshStateByEnvironment.get(envId)?.connectionStates.get('ssh-new')
|
||||
?.status
|
||||
).toBe('connecting')
|
||||
})
|
||||
})
|
||||
|
||||
describe('connectRuntimeEnvironmentSshTarget', () => {
|
||||
it('routes ssh.connect to the owning environment and mirrors the returned state', async () => {
|
||||
const envId = nextEnvId()
|
||||
callRuntimeRpcMock.mockResolvedValue({ state: connState('ssh-1', 'connected') } as never)
|
||||
|
||||
const state = await connectRuntimeEnvironmentSshTarget(envId, 'ssh-1')
|
||||
|
||||
expect(state?.status).toBe('connected')
|
||||
expect(callRuntimeRpcMock).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: envId },
|
||||
'ssh.connect',
|
||||
{ targetId: 'ssh-1' },
|
||||
expect.anything()
|
||||
)
|
||||
expect(
|
||||
useAppStore.getState().sshStateByEnvironment.get(envId)?.connectionStates.get('ssh-1')?.status
|
||||
).toBe('connected')
|
||||
expect(
|
||||
useAppStore.getState().sshStateByEnvironment.get(envId)?.connectionStates.get('ssh-1')
|
||||
).toMatchObject({
|
||||
providerEpoch: 'ssh-1-provider-epoch',
|
||||
connectionGeneration: 7
|
||||
})
|
||||
expect(useAppStore.getState().sshConnectionStates.size).toBe(0)
|
||||
})
|
||||
|
||||
it('propagates connect failures without writing any state', async () => {
|
||||
const envId = nextEnvId()
|
||||
callRuntimeRpcMock.mockRejectedValue(new Error('SSH target not found'))
|
||||
|
||||
await expect(connectRuntimeEnvironmentSshTarget(envId, 'ssh-dead')).rejects.toThrow(
|
||||
'SSH target not found'
|
||||
)
|
||||
expect(useAppStore.getState().sshStateByEnvironment.has(envId)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resyncRuntimeEnvironmentSshTargets', () => {
|
||||
it('applies the target list even when the removed-labels refresh fails', async () => {
|
||||
const envId = nextEnvId()
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-live', label: 'devbox' }],
|
||||
failRemovedLabels: true
|
||||
})
|
||||
|
||||
await resyncRuntimeEnvironmentSshTargets(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetLabels.get('ssh-live')).toBe('devbox')
|
||||
expect(bucket?.targetsHydrated).toBe(true)
|
||||
})
|
||||
|
||||
it('reads the connection state of a host re-added under a new target id', async () => {
|
||||
const envId = nextEnvId()
|
||||
useAppStore
|
||||
.getState()
|
||||
.setEnvironmentSshTargetsMetadata(envId, [{ id: 'ssh-old', label: 'devbox' }])
|
||||
installRpcResponses({
|
||||
targets: [{ id: 'ssh-new', label: 'devbox' }],
|
||||
labels: { 'ssh-old': 'devbox' },
|
||||
states: { 'ssh-new': connState('ssh-new') }
|
||||
})
|
||||
|
||||
await resyncRuntimeEnvironmentSshTargets(envId)
|
||||
|
||||
const bucket = useAppStore.getState().sshStateByEnvironment.get(envId)
|
||||
expect(bucket?.targetLabels.get('ssh-new')).toBe('devbox')
|
||||
expect(bucket?.connectionStates.get('ssh-new')?.status).toBe('connected')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,25 +95,6 @@ describe('runtime file client', () => {
|
||||
expect(fsCancelDownloadedFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not open the save dialog when the remote chunk probe fails for transport reasons', async () => {
|
||||
runtimeEnvironmentCall.mockRejectedValueOnce(new Error('connection dropped'))
|
||||
|
||||
await expect(
|
||||
downloadRuntimeFile(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
'/remote/repo/archive.zip',
|
||||
'archive.zip'
|
||||
)
|
||||
).rejects.toThrow('connection dropped')
|
||||
|
||||
expect(fsStartDownloadedFile).not.toHaveBeenCalled()
|
||||
expect(fsSaveDownloadedFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to preview content when older remote runtimes lack chunked download', async () => {
|
||||
fsSaveDownloadedFile.mockResolvedValue({
|
||||
canceled: false,
|
||||
@@ -205,40 +186,6 @@ describe('runtime file client', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('asks users to update older remote runtimes when preview fallback cannot download the file', async () => {
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce({
|
||||
id: 'chunk-1',
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'method_not_found',
|
||||
message: 'Unknown method: files.readChunk'
|
||||
},
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'preview-1',
|
||||
ok: false,
|
||||
error: { code: 'runtime_error', message: 'file_too_large' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadRuntimeFile(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
'/remote/repo/archive.zip',
|
||||
'archive.zip'
|
||||
)
|
||||
).rejects.toThrow('Remote file download requires a newer Orca server')
|
||||
|
||||
expect(fsStartDownloadedFile).not.toHaveBeenCalled()
|
||||
expect(fsSaveDownloadedFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cancels the local temp download when a remote chunk fails', async () => {
|
||||
fsStartDownloadedFile.mockResolvedValue({
|
||||
canceled: false,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { importExternalPathsToRuntime } from './runtime-file-import-client'
|
||||
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
|
||||
import {
|
||||
fsImportExternalPaths,
|
||||
fsStageExternalPathsForRuntimeUpload,
|
||||
fsUploadExternalFileToRuntime,
|
||||
runtimeEnvironmentCall,
|
||||
runtimeEnvironmentTransportCall,
|
||||
installRuntimeFileClientEnvironment
|
||||
} from './runtime-file-client-test-harness'
|
||||
|
||||
@@ -59,251 +56,7 @@ function uploadRequests(): UploadRequest[] {
|
||||
return fsUploadExternalFileToRuntime.mock.calls.flat()
|
||||
}
|
||||
|
||||
const identityOf = (entry: Record<string, unknown>): Record<string, unknown> => ({
|
||||
byteLength: entry.byteLength,
|
||||
inode: entry.inode,
|
||||
deviceId: entry.deviceId,
|
||||
modifiedAtMs: entry.modifiedAtMs
|
||||
})
|
||||
|
||||
describe('runtime file client', () => {
|
||||
it('streams staged directory entries through main instead of sending base64 itself', async () => {
|
||||
replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 17 }])
|
||||
const logo = stagedFile('logo.png', 3, 101)
|
||||
const large = stagedFile('large.bin', 40 * 1024 * 1024, 102)
|
||||
fsStageExternalPathsForRuntimeUpload.mockResolvedValue({
|
||||
sources: [
|
||||
{
|
||||
sourcePath: '/Users/me/assets',
|
||||
status: 'staged',
|
||||
name: 'assets',
|
||||
kind: 'directory',
|
||||
entries: [{ relativePath: '', kind: 'directory' }, logo, large]
|
||||
}
|
||||
]
|
||||
})
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-destination-miss'))
|
||||
.mockResolvedValueOnce(okResponse('create-destination-dir'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-miss'))
|
||||
.mockResolvedValueOnce(okResponse('create-dir'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
.mockResolvedValueOnce(okResponse('commit-large-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-large-temp'))
|
||||
|
||||
await expect(
|
||||
importExternalPathsToRuntime(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
['/Users/me/assets'],
|
||||
'/remote/repo/uploads'
|
||||
)
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{
|
||||
sourcePath: '/Users/me/assets',
|
||||
status: 'imported',
|
||||
destPath: '/remote/repo/uploads/assets',
|
||||
kind: 'directory',
|
||||
renamed: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(fsStageExternalPathsForRuntimeUpload).toHaveBeenCalledWith({
|
||||
sourcePaths: ['/Users/me/assets'],
|
||||
nameExtensionlessImages: undefined
|
||||
})
|
||||
const transportCalls = runtimeEnvironmentTransportCall.mock.calls.map(([args]) => args)
|
||||
expect(transportCalls.map((args) => args.method)).toEqual([
|
||||
'status.get',
|
||||
'status.get',
|
||||
'files.stat',
|
||||
'files.createDir',
|
||||
'files.stat',
|
||||
'files.createDirNoClobber',
|
||||
'files.commitUpload',
|
||||
'files.delete',
|
||||
'files.commitUpload',
|
||||
'files.delete'
|
||||
])
|
||||
// Why: the whole point of the change — no file body crosses this boundary.
|
||||
expect(transportCalls.some((args) => String(args.method).startsWith('files.writeBase64'))).toBe(
|
||||
false
|
||||
)
|
||||
expect(transportCalls.every((args) => args.expectedEnvironmentPairingRevision === 17)).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
const uploads = uploadRequests()
|
||||
expect(uploads).toHaveLength(2)
|
||||
expect(uploads[0]?.relativePath).toMatch(/^uploads\/assets\/\.logo\.png\.orca-upload-/)
|
||||
expect(uploads[0]).toEqual({
|
||||
environmentId: 'env-1',
|
||||
sourceRootPath: '/Users/me/assets',
|
||||
entryRelativePath: 'logo.png',
|
||||
expected: identityOf(logo),
|
||||
worktree: 'id:wt-1',
|
||||
relativePath: uploads[0]?.relativePath,
|
||||
expectedExecutionHostId: 'local',
|
||||
expectedSshTargetId: undefined,
|
||||
expectedSshConnectionGeneration: undefined,
|
||||
expectedEnvironmentPairingRevision: 17,
|
||||
expectedEnvironmentRuntimeId: 'remote-runtime'
|
||||
})
|
||||
expect(uploads[1]).toMatchObject({
|
||||
entryRelativePath: 'large.bin',
|
||||
expected: identityOf(large)
|
||||
})
|
||||
expect(uploads[1]?.relativePath).toMatch(/^uploads\/assets\/\.large\.bin\.orca-upload-/)
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(5, {
|
||||
selector: 'env-1',
|
||||
method: 'files.commitUpload',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
tempRelativePath: uploads[0]?.relativePath,
|
||||
finalRelativePath: 'uploads/assets/logo.png',
|
||||
expectedExecutionHostId: 'local',
|
||||
expectedSshTargetId: undefined,
|
||||
expectedSshConnectionGeneration: undefined
|
||||
},
|
||||
timeoutMs: 30_000,
|
||||
expectedEnvironmentPairingRevision: 17,
|
||||
expectedEnvironmentRuntimeId: 'remote-runtime'
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(6, {
|
||||
selector: 'env-1',
|
||||
method: 'files.delete',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
relativePath: uploads[0]?.relativePath,
|
||||
recursive: false,
|
||||
expectedExecutionHostId: 'local',
|
||||
expectedSshTargetId: undefined,
|
||||
expectedSshConnectionGeneration: undefined
|
||||
},
|
||||
timeoutMs: 15_000,
|
||||
expectedEnvironmentPairingRevision: 17,
|
||||
expectedEnvironmentRuntimeId: 'remote-runtime'
|
||||
})
|
||||
expect(fsImportExternalPaths).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forwards a single staged file with the identity staging measured', async () => {
|
||||
const entry = stagedFile('', 40 * 1024 * 1024, 55)
|
||||
fsStageExternalPathsForRuntimeUpload.mockResolvedValue({
|
||||
sources: [
|
||||
{
|
||||
sourcePath: '/Users/me/large.bin',
|
||||
status: 'staged',
|
||||
name: 'large.bin',
|
||||
kind: 'file',
|
||||
entries: [entry]
|
||||
}
|
||||
]
|
||||
})
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-destination-miss'))
|
||||
.mockResolvedValueOnce(okResponse('create-destination-dir'))
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-miss'))
|
||||
.mockResolvedValueOnce(okResponse('commit-upload'))
|
||||
.mockResolvedValueOnce(okResponse('delete-temp'))
|
||||
|
||||
await expect(
|
||||
importExternalPathsToRuntime(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
['/Users/me/large.bin'],
|
||||
'/remote/repo/uploads'
|
||||
)
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{
|
||||
sourcePath: '/Users/me/large.bin',
|
||||
status: 'imported',
|
||||
destPath: '/remote/repo/uploads/large.bin',
|
||||
kind: 'file',
|
||||
renamed: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const upload = uploadRequests()[0]
|
||||
expect(upload?.relativePath).toMatch(/^uploads\/\.large\.bin\.orca-upload-/)
|
||||
expect(upload?.sourceRootPath).toBe('/Users/me/large.bin')
|
||||
expect(upload?.entryRelativePath).toBe('')
|
||||
expect(upload?.expected).toEqual(identityOf(entry))
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.writeBase64' })
|
||||
)
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.writeBase64Chunk' })
|
||||
)
|
||||
})
|
||||
|
||||
it('does not commit an upload when the owner generation changes while it streams', async () => {
|
||||
fsStageExternalPathsForRuntimeUpload.mockResolvedValue({
|
||||
sources: [
|
||||
{
|
||||
sourcePath: '/Users/me/large.bin',
|
||||
status: 'staged',
|
||||
name: 'large.bin',
|
||||
kind: 'file',
|
||||
entries: [stagedFile('', 40 * 1024 * 1024, 55)]
|
||||
}
|
||||
]
|
||||
})
|
||||
runtimeEnvironmentCall
|
||||
.mockResolvedValueOnce({
|
||||
id: 'stat-destination',
|
||||
ok: true,
|
||||
result: { size: 0, isDirectory: true, mtime: 1 },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
.mockResolvedValueOnce(notFoundResponse('stat-file-miss'))
|
||||
let ownerChanged = false
|
||||
fsUploadExternalFileToRuntime.mockImplementation(async () => {
|
||||
ownerChanged = true
|
||||
return { byteLength: 40 * 1024 * 1024 }
|
||||
})
|
||||
const assertCurrent = vi.fn(() => {
|
||||
if (ownerChanged) {
|
||||
throw new Error('runtime owner generation changed')
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
importExternalPathsToRuntime(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
['/Users/me/large.bin'],
|
||||
'/remote/repo/uploads',
|
||||
{ assertCurrent }
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
results: [{ status: 'failed', reason: 'runtime owner generation changed' }]
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall.mock.calls.map((call) => call[0].method)).toEqual([
|
||||
'files.stat',
|
||||
'files.stat'
|
||||
])
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.commitUpload' })
|
||||
)
|
||||
})
|
||||
|
||||
it('cleans up the staged temp path when the streamed upload fails', async () => {
|
||||
fsStageExternalPathsForRuntimeUpload.mockResolvedValue({
|
||||
sources: [
|
||||
@@ -415,45 +168,4 @@ describe('runtime file client', () => {
|
||||
expectedEnvironmentRuntimeId: 'remote-runtime'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps local external imports on filesystem IPC when no runtime is active', async () => {
|
||||
fsImportExternalPaths.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
sourcePath: '/Users/me/readme.md',
|
||||
status: 'imported',
|
||||
destPath: '/repo/readme.md',
|
||||
kind: 'file',
|
||||
renamed: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
await importExternalPathsToRuntime(
|
||||
{
|
||||
target: { kind: 'local' as const },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
connectionId: 'ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 5
|
||||
},
|
||||
['/Users/me/readme.md'],
|
||||
'/repo',
|
||||
{ ensureDestinationDir: true }
|
||||
)
|
||||
|
||||
expect(fsImportExternalPaths).toHaveBeenCalledWith({
|
||||
sourcePaths: ['/Users/me/readme.md'],
|
||||
destDir: '/repo',
|
||||
connectionId: 'ssh-1',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
ensureDir: true,
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 5
|
||||
})
|
||||
expect(fsStageExternalPathsForRuntimeUpload).not.toHaveBeenCalled()
|
||||
expect(fsUploadExternalFileToRuntime).not.toHaveBeenCalled()
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
copyRuntimePath,
|
||||
createRuntimePath,
|
||||
deleteRuntimePath,
|
||||
readRuntimeDirectory,
|
||||
renameRuntimePath,
|
||||
writeRuntimeFile
|
||||
} from './runtime-file-mutation-client'
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
RUNTIME_PROTOCOL_VERSION
|
||||
} from '../../../shared/protocol-version'
|
||||
import {
|
||||
fsReadFile,
|
||||
fsWriteFile,
|
||||
fsCopy,
|
||||
fsCreateFile,
|
||||
fsRename,
|
||||
@@ -33,208 +30,6 @@ import {
|
||||
installRuntimeFileClientEnvironment()
|
||||
|
||||
describe('runtime file client', () => {
|
||||
it('routes create, rename, copy, and delete mutations through the selected runtime', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-1',
|
||||
ok: true,
|
||||
result: { ok: true },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
const context = {
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
}
|
||||
|
||||
await createRuntimePath(context, '/remote/repo/src/new.ts', 'file')
|
||||
await renameRuntimePath(context, '/remote/repo/src/new.ts', '/remote/repo/src/renamed.ts')
|
||||
await copyRuntimePath(
|
||||
context,
|
||||
'/remote/repo/src/renamed.ts',
|
||||
'/remote/repo/src/renamed copy.ts'
|
||||
)
|
||||
await deleteRuntimePath(context, '/remote/repo/src/renamed.ts', false)
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(1, {
|
||||
selector: 'env-1',
|
||||
method: 'files.createFile',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
relativePath: 'src/new.ts',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(2, {
|
||||
selector: 'env-1',
|
||||
method: 'files.rename',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
oldRelativePath: 'src/new.ts',
|
||||
newRelativePath: 'src/renamed.ts',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(3, {
|
||||
selector: 'env-1',
|
||||
method: 'files.copy',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
sourceRelativePath: 'src/renamed.ts',
|
||||
destinationRelativePath: 'src/renamed copy.ts',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(4, {
|
||||
selector: 'env-1',
|
||||
method: 'files.delete',
|
||||
params: {
|
||||
worktree: 'id:wt-1',
|
||||
relativePath: 'src/renamed.ts',
|
||||
recursive: false,
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses HUB-local mutations before RPC when the HUB lacks ownership support', async () => {
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: { method: string }) => {
|
||||
if (args.method === 'status.get') {
|
||||
return Promise.resolve({
|
||||
id: 'status',
|
||||
ok: true,
|
||||
result: {
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
|
||||
capabilities: []
|
||||
},
|
||||
_meta: { runtimeId: 'old-hub-runtime' }
|
||||
})
|
||||
}
|
||||
return runtimeEnvironmentCall(args)
|
||||
})
|
||||
|
||||
await expect(
|
||||
writeRuntimeFile(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-old-hub' },
|
||||
worktreeId: 'wt-hub-local',
|
||||
worktreePath: '/hub/repo',
|
||||
expectedExecutionHostId: 'local'
|
||||
},
|
||||
'/hub/repo/readme.md',
|
||||
'changed'
|
||||
)
|
||||
).rejects.toThrow(FILE_MUTATION_OWNERSHIP_UPDATE_REQUIRED_MESSAGE)
|
||||
|
||||
expect(runtimeEnvironmentTransportCall).toHaveBeenCalledWith({
|
||||
selector: 'env-old-hub',
|
||||
method: 'status.get',
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
expect(fsWriteFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses nested SSH mutations before RPC when the HUB lacks ownership support', async () => {
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: { method: string }) => {
|
||||
if (args.method === 'status.get') {
|
||||
return Promise.resolve({
|
||||
id: 'status',
|
||||
ok: true,
|
||||
result: {
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
|
||||
capabilities: []
|
||||
},
|
||||
_meta: { runtimeId: 'old-hub-runtime' }
|
||||
})
|
||||
}
|
||||
return runtimeEnvironmentCall(args)
|
||||
})
|
||||
|
||||
await expect(
|
||||
renameRuntimePath(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-old-hub' },
|
||||
worktreeId: 'wt-nested-ssh',
|
||||
worktreePath: '/ssh/repo',
|
||||
connectionId: 'hub-ssh-1',
|
||||
expectedExecutionHostId: 'ssh:hub-ssh-1',
|
||||
expectedSshTargetId: 'hub-ssh-1',
|
||||
expectedSshConnectionGeneration: 7
|
||||
},
|
||||
'/ssh/repo/old.md',
|
||||
'/ssh/repo/new.md'
|
||||
)
|
||||
).rejects.toThrow(FILE_MUTATION_OWNERSHIP_UPDATE_REQUIRED_MESSAGE)
|
||||
|
||||
expect(runtimeEnvironmentTransportCall).toHaveBeenCalledWith({
|
||||
selector: 'env-old-hub',
|
||||
method: 'status.get',
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
expect(fsRename).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps reads compatible with HUBs that lack mutation ownership support', async () => {
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: { method: string }) => {
|
||||
if (args.method === 'status.get') {
|
||||
return Promise.resolve({
|
||||
id: 'status',
|
||||
ok: true,
|
||||
result: {
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
|
||||
capabilities: []
|
||||
},
|
||||
_meta: { runtimeId: 'old-hub-runtime' }
|
||||
})
|
||||
}
|
||||
return runtimeEnvironmentCall(args)
|
||||
})
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'read-dir',
|
||||
ok: true,
|
||||
result: [],
|
||||
_meta: { runtimeId: 'old-hub-runtime' }
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRuntimeDirectory(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-old-hub' },
|
||||
worktreeId: 'wt-nested-ssh',
|
||||
worktreePath: '/ssh/repo',
|
||||
connectionId: 'hub-ssh-1'
|
||||
},
|
||||
'/ssh/repo'
|
||||
)
|
||||
).resolves.toEqual([])
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'env-old-hub',
|
||||
method: 'files.readDir',
|
||||
params: { worktree: 'id:wt-nested-ssh', relativePath: '' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(fsReadFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses old-HUB imports before staging client-local files', async () => {
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: { method: string }) => {
|
||||
if (args.method === 'status.get') {
|
||||
@@ -436,49 +231,4 @@ describe('runtime file client', () => {
|
||||
expect(fsCreateFile).not.toHaveBeenCalled()
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps copy operations on local filesystem IPC when no runtime is active', async () => {
|
||||
await copyRuntimePath(
|
||||
{
|
||||
target: { kind: 'local' as const },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo'
|
||||
},
|
||||
'/repo/a.md',
|
||||
'/repo/a copy.md'
|
||||
)
|
||||
|
||||
expect(fsCopy).toHaveBeenCalledWith({
|
||||
sourcePath: '/repo/a.md',
|
||||
destinationPath: '/repo/a copy.md',
|
||||
connectionId: undefined,
|
||||
expectedExecutionHostId: 'local'
|
||||
})
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves the SSH connection for copy operations when no runtime is active', async () => {
|
||||
await copyRuntimePath(
|
||||
{
|
||||
target: { kind: 'local' as const },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
connectionId: 'ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 5
|
||||
},
|
||||
'/repo/a.md',
|
||||
'/repo/a copy.md'
|
||||
)
|
||||
|
||||
expect(fsCopy).toHaveBeenCalledWith({
|
||||
sourcePath: '/repo/a.md',
|
||||
destinationPath: '/repo/a copy.md',
|
||||
connectionId: 'ssh-1',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 5
|
||||
})
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { subscribeRuntimeFileChanges } from './runtime-file-watch-client'
|
||||
import {
|
||||
fsOnChanged,
|
||||
runtimeEnvironmentCall,
|
||||
runtimeEnvironmentSubscribe,
|
||||
installRuntimeFileClientEnvironment
|
||||
@@ -10,128 +9,6 @@ import {
|
||||
installRuntimeFileClientEnvironment()
|
||||
|
||||
describe('runtime file client', () => {
|
||||
it('uses the local fs changed stream when no runtime environment is active', async () => {
|
||||
const unsubscribe = vi.fn()
|
||||
const onPayload = vi.fn()
|
||||
fsOnChanged.mockReturnValue(unsubscribe)
|
||||
|
||||
await expect(
|
||||
subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'local' as const },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo'
|
||||
},
|
||||
onPayload
|
||||
)
|
||||
).resolves.toBe(unsubscribe)
|
||||
|
||||
expect(fsOnChanged).toHaveBeenCalledWith(onPayload)
|
||||
expect(runtimeEnvironmentSubscribe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('maps runtime file watch events back to fs changed payloads', async () => {
|
||||
const onPayload = vi.fn()
|
||||
const unsubscribe = vi.fn()
|
||||
let onResponse: ((response: unknown) => void) | undefined
|
||||
runtimeEnvironmentSubscribe.mockImplementation((_args, callbacks) => {
|
||||
onResponse = callbacks.onResponse
|
||||
return Promise.resolve({ unsubscribe, sendBinary: vi.fn() })
|
||||
})
|
||||
|
||||
const stop = await subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
onPayload
|
||||
)
|
||||
|
||||
expect(runtimeEnvironmentSubscribe).toHaveBeenCalledWith(
|
||||
{
|
||||
selector: 'env-1',
|
||||
method: 'files.watch',
|
||||
params: { worktree: 'id:wt-1', batchCadence: 'interval' },
|
||||
timeoutMs: 15_000
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
onResponse?.({
|
||||
id: 'rpc-1',
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'changed',
|
||||
worktree: 'id:wt-1',
|
||||
events: [{ kind: 'update', absolutePath: '/remote/repo/readme.md' }]
|
||||
},
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
expect(onPayload).toHaveBeenCalledWith({
|
||||
worktreePath: '/remote/repo',
|
||||
events: [{ kind: 'update', absolutePath: '/remote/repo/readme.md' }]
|
||||
})
|
||||
|
||||
stop()
|
||||
expect(unsubscribe).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('surfaces terminal watcher errors and allows a fresh subscription after stream end', async () => {
|
||||
const onError = vi.fn()
|
||||
const unsubscribe = vi.fn()
|
||||
let callbacks: { onResponse: (response: unknown) => void; onClose: () => void } | undefined
|
||||
runtimeEnvironmentSubscribe.mockImplementation((_args, nextCallbacks) => {
|
||||
callbacks = nextCallbacks
|
||||
return Promise.resolve({ unsubscribe, sendBinary: vi.fn() })
|
||||
})
|
||||
|
||||
await subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
vi.fn(),
|
||||
onError
|
||||
)
|
||||
callbacks?.onResponse({
|
||||
id: 'rpc-error',
|
||||
ok: true,
|
||||
result: { type: 'error', message: 'file watcher process crashed repeatedly' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
callbacks?.onResponse({
|
||||
id: 'rpc-end',
|
||||
ok: true,
|
||||
result: { type: 'end' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: 'file watcher process crashed repeatedly' })
|
||||
)
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
|
||||
await subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
vi.fn(),
|
||||
vi.fn()
|
||||
)
|
||||
expect(runtimeEnvironmentSubscribe).toHaveBeenCalledTimes(2)
|
||||
callbacks?.onResponse({
|
||||
id: 'rpc-second-end',
|
||||
ok: true,
|
||||
result: { type: 'end' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
})
|
||||
|
||||
it('evicts a terminal watch before notifying error listeners that retry', async () => {
|
||||
const callbacks: {
|
||||
onResponse: (response: unknown) => void
|
||||
@@ -311,76 +188,4 @@ describe('runtime file client', () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('delegates stopped pre-ready web shared file watch cleanup to the subscription handle', async () => {
|
||||
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
|
||||
const onPayload = vi.fn()
|
||||
const unsubscribe = vi.fn()
|
||||
let onResponse: ((response: unknown) => void) | undefined
|
||||
runtimeEnvironmentSubscribe.mockImplementation((_args, callbacks) => {
|
||||
onResponse = callbacks.onResponse
|
||||
return Promise.resolve({ unsubscribe, sendBinary: vi.fn() })
|
||||
})
|
||||
|
||||
const stop = await subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
onPayload
|
||||
)
|
||||
|
||||
stop()
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.unwatch' })
|
||||
)
|
||||
|
||||
onResponse?.({
|
||||
id: 'ready',
|
||||
ok: true,
|
||||
result: { type: 'ready', subscriptionId: 'files-watch-late' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.unwatch' })
|
||||
)
|
||||
})
|
||||
|
||||
it('delegates stopped ready web shared file watch cleanup to the subscription handle', async () => {
|
||||
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
|
||||
const onPayload = vi.fn()
|
||||
const unsubscribe = vi.fn()
|
||||
let onResponse: ((response: unknown) => void) | undefined
|
||||
runtimeEnvironmentSubscribe.mockImplementation((_args, callbacks) => {
|
||||
onResponse = callbacks.onResponse
|
||||
return Promise.resolve({ unsubscribe, sendBinary: vi.fn() })
|
||||
})
|
||||
|
||||
const stop = await subscribeRuntimeFileChanges(
|
||||
{
|
||||
target: { kind: 'environment' as const, environmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/remote/repo'
|
||||
},
|
||||
onPayload
|
||||
)
|
||||
|
||||
onResponse?.({
|
||||
id: 'ready',
|
||||
ok: true,
|
||||
result: { type: 'ready', subscriptionId: 'files-watch-ready' },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
stop()
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: 'files.unwatch' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status'
|
||||
import type { RuntimeStatus } from '../../../shared/runtime-types'
|
||||
import {
|
||||
isConnectedRuntimeHostState,
|
||||
isDisconnectedRuntimeHostState,
|
||||
runtimeHostConnectionState,
|
||||
runtimeHostConnectionStateForEntry,
|
||||
@@ -29,37 +28,6 @@ const windowClosedStatus = makeStatus({
|
||||
})
|
||||
|
||||
describe('runtime host connection state', () => {
|
||||
it('counts connected remote servers as connected hosts', () => {
|
||||
// Connected means reachable, independent of active/default host; there is no separate available state.
|
||||
expect(runtimeStatusForOverall('connected')).toBe('connected')
|
||||
expect(isConnectedRuntimeHostState('connected')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps reconnecting and disconnected remote servers out of the connected count', () => {
|
||||
expect(runtimeStatusForOverall('reconnecting')).toBe('connecting')
|
||||
expect(runtimeStatusForOverall('disconnected')).toBe('disconnected')
|
||||
expect(isConnectedRuntimeHostState('reconnecting')).toBe(false)
|
||||
expect(isConnectedRuntimeHostState('disconnected')).toBe(false)
|
||||
})
|
||||
|
||||
it('distinguishes a connected transport from an unavailable runtime', () => {
|
||||
expect(
|
||||
runtimeHostConnectionState({
|
||||
hasStatusEntry: true,
|
||||
status: null,
|
||||
transportStatus: 'connected'
|
||||
})
|
||||
).toBe('runtime-unavailable')
|
||||
expect(runtimeStatusForOverall('runtime-unavailable')).toBe('connected')
|
||||
expect(isConnectedRuntimeHostState('runtime-unavailable')).toBe(true)
|
||||
})
|
||||
|
||||
it('still counts a workspace-window-closed remote server as a connected host', () => {
|
||||
// Healthy transport remains connected; degraded runtime changes only wording (#12350).
|
||||
expect(runtimeStatusForOverall('workspace-window-closed')).toBe('connected')
|
||||
expect(isConnectedRuntimeHostState('workspace-window-closed')).toBe(true)
|
||||
})
|
||||
|
||||
it('distinguishes a reachable host whose workspace window is closed', () => {
|
||||
expect(runtimeHostConnectionState({ hasStatusEntry: true, status: windowClosedStatus })).toBe(
|
||||
'workspace-window-closed'
|
||||
@@ -192,27 +160,6 @@ describe('runtime host connection state for a recorded status entry', () => {
|
||||
})
|
||||
).toBe('reconnecting')
|
||||
})
|
||||
|
||||
it('agrees with the status bar that a closed control channel is disconnected', () => {
|
||||
expect(
|
||||
runtimeHostConnectionStateForEntry({
|
||||
status: makeStatus({ remoteControl: remoteControl('closed') })
|
||||
})
|
||||
).toBe('disconnected')
|
||||
})
|
||||
|
||||
it('names only the disconnected verdict as disconnected', () => {
|
||||
expect(isDisconnectedRuntimeHostState('disconnected')).toBe(true)
|
||||
for (const state of [
|
||||
'connected',
|
||||
'checking',
|
||||
'reconnecting',
|
||||
'runtime-unavailable',
|
||||
'workspace-window-closed'
|
||||
] as const) {
|
||||
expect(isDisconnectedRuntimeHostState(state)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function remoteControl(
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isRuntimeHostContactRevoked,
|
||||
type RuntimeHostStatusSnapshot
|
||||
} from '../../../shared/runtime-host-status'
|
||||
import {
|
||||
isRuntimeHostContactRevokedVerdict,
|
||||
lastRuntimeHostAnswer,
|
||||
liveRuntimeHostStatus,
|
||||
runtimeHostContactFromSnapshot
|
||||
} from '../../../shared/runtime-host-contact'
|
||||
import type { RuntimeStatus } from '../../../shared/runtime-types'
|
||||
|
||||
type Entry = {
|
||||
status: RuntimeStatus | null
|
||||
remoteControl?: RuntimeStatus['remoteControl'] | null
|
||||
snapshot?: RuntimeHostStatusSnapshot
|
||||
}
|
||||
|
||||
const VERIFICATIONS = ['checking', 'verified', 'unavailable', 'blocked'] as const
|
||||
const TRANSPORTS = ['unknown', 'connecting', 'ready', 'disconnected'] as const
|
||||
const RETIRED = [false, true] as const
|
||||
const REMOTE_CONTROL_STATES = [
|
||||
undefined,
|
||||
'ready',
|
||||
'awaiting_ready',
|
||||
'awaiting_authenticated',
|
||||
'reconnecting',
|
||||
'closed'
|
||||
] as const
|
||||
|
||||
function makeStatus(overrides: Partial<RuntimeStatus> = {}): RuntimeStatus {
|
||||
return {
|
||||
runtimeId: 'rt-1',
|
||||
rendererGraphEpoch: 0,
|
||||
graphStatus: 'ready',
|
||||
authoritativeWindowId: 1,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function makeRemoteControl(
|
||||
state: Exclude<(typeof REMOTE_CONTROL_STATES)[number], undefined>
|
||||
): NonNullable<RuntimeStatus['remoteControl']> {
|
||||
return {
|
||||
state,
|
||||
pendingRequestCount: 0,
|
||||
subscriptionCount: 0,
|
||||
reconnectAttempt: 0,
|
||||
lastConnectedAt: null,
|
||||
lastClose: null,
|
||||
lastError: null
|
||||
}
|
||||
}
|
||||
|
||||
function makeSnapshot(
|
||||
verification: (typeof VERIFICATIONS)[number],
|
||||
transport: (typeof TRANSPORTS)[number],
|
||||
retired: boolean,
|
||||
answered: RuntimeStatus | null
|
||||
): RuntimeHostStatusSnapshot {
|
||||
return {
|
||||
environmentId: 'env-a',
|
||||
pairingRevision: 1,
|
||||
sequence: 1,
|
||||
checkedAt: 1,
|
||||
status: answered,
|
||||
verification,
|
||||
transport,
|
||||
...(retired ? { retired: true as const } : {})
|
||||
}
|
||||
}
|
||||
|
||||
/** Every entry shape the derivation can distinguish: 4 x 4 x 2, across each status/diagnostic. */
|
||||
function* everySnapshotEntry(): Generator<{ label: string; entry: Entry }> {
|
||||
for (const verification of VERIFICATIONS) {
|
||||
for (const transport of TRANSPORTS) {
|
||||
for (const retired of RETIRED) {
|
||||
for (const answered of [null, makeStatus()] as const) {
|
||||
for (const remoteControlState of REMOTE_CONTROL_STATES) {
|
||||
// Store status exists only for verified unretired probes; enumerate reachable combinations, not an impossible cross-product.
|
||||
const entryStatus = verification === 'verified' && !retired ? answered : null
|
||||
const remoteControl = remoteControlState
|
||||
? makeRemoteControl(remoteControlState)
|
||||
: undefined
|
||||
yield {
|
||||
label: `${verification}/${transport}/retired=${retired}/answered=${answered !== null}/rc=${remoteControlState ?? 'none'}`,
|
||||
entry: {
|
||||
status: entryStatus,
|
||||
...(remoteControl ? { remoteControl } : {}),
|
||||
snapshot: makeSnapshot(verification, transport, retired, answered)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('the revoked predicate and the contact verdict stay in step', () => {
|
||||
it('keeps the revoked predicate and the contact verdict in step', () => {
|
||||
for (const { label, entry } of everySnapshotEntry()) {
|
||||
expect(
|
||||
isRuntimeHostContactRevokedVerdict(
|
||||
runtimeHostContactFromSnapshot(entry.snapshot!, entry.status)
|
||||
),
|
||||
label
|
||||
).toBe(isRuntimeHostContactRevoked(entry))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('the contact separates what the host said from what it is worth', () => {
|
||||
it('retains the host answer through every non-live verdict', () => {
|
||||
const answered = makeStatus()
|
||||
for (const [verification, transport, retired] of [
|
||||
['unavailable', 'ready', false],
|
||||
['checking', 'connecting', false],
|
||||
['unavailable', 'disconnected', false],
|
||||
['blocked', 'ready', false],
|
||||
['verified', 'ready', true]
|
||||
] as const) {
|
||||
const contact = runtimeHostContactFromSnapshot(
|
||||
makeSnapshot(verification, transport, retired, answered),
|
||||
null
|
||||
)
|
||||
expect(contact.verdict, `${verification}/${transport}`).not.toBe('live')
|
||||
// The fact the host gave us survives; only its currency is in question.
|
||||
expect(lastRuntimeHostAnswer(contact)).toBe(answered)
|
||||
expect(liveRuntimeHostStatus(contact)).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('reports a verified probe as live and nothing else', () => {
|
||||
const answered = makeStatus()
|
||||
const contact = runtimeHostContactFromSnapshot(
|
||||
makeSnapshot('verified', 'ready', false, answered),
|
||||
answered
|
||||
)
|
||||
expect(contact.verdict).toBe('live')
|
||||
expect(liveRuntimeHostStatus(contact)).toBe(answered)
|
||||
expect(lastRuntimeHostAnswer(contact)).toBe(answered)
|
||||
})
|
||||
|
||||
it('tells a host that was never reached apart from a handshake in flight', () => {
|
||||
// Previously collapsed nulls need opposite Connect affordances.
|
||||
expect(
|
||||
runtimeHostContactFromSnapshot(makeSnapshot('unavailable', 'unknown', false, null), null)
|
||||
).toEqual({ verdict: 'unverifiable', reason: 'never-asked', lastAnswer: null })
|
||||
expect(
|
||||
runtimeHostContactFromSnapshot(makeSnapshot('unavailable', 'connecting', false, null), null)
|
||||
).toEqual({ verdict: 'unverifiable', reason: 'transport-connecting', lastAnswer: null })
|
||||
})
|
||||
|
||||
it('tells a refused host apart from a retired pairing', () => {
|
||||
const answered = makeStatus()
|
||||
expect(
|
||||
runtimeHostContactFromSnapshot(makeSnapshot('blocked', 'ready', false, answered), null)
|
||||
.verdict
|
||||
).toBe('refused')
|
||||
expect(
|
||||
runtimeHostContactFromSnapshot(makeSnapshot('verified', 'ready', true, answered), null)
|
||||
.verdict
|
||||
).toBe('retired')
|
||||
})
|
||||
})
|
||||
@@ -52,26 +52,3 @@ it.each([
|
||||
expect(call.mock.calls.filter(([args]) => args.method === 'jira.listIssues')).toHaveLength(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('forwards both filters to a host that supports the combined board query', async () => {
|
||||
call.mockImplementation(async (args: { method: string }) =>
|
||||
args.method === 'status.get'
|
||||
? createCompatibleRuntimeStatusResponse()
|
||||
: { id: 'board-filter', ok: true, result: [] }
|
||||
)
|
||||
const filters = { statuses: ['Blocked'], currentSprint: true }
|
||||
await jiraListIssues(
|
||||
{ kind: 'environment', environmentId: 'board-current' },
|
||||
'all',
|
||||
50,
|
||||
'site-a',
|
||||
filters
|
||||
)
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selector: 'board-current',
|
||||
method: 'jira.listIssues',
|
||||
params: { filter: 'all', limit: 50, siteId: 'site-a', filters }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,32 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { parseJiraConnectionStatus } from './runtime-jira-connection-status'
|
||||
|
||||
describe('parseJiraConnectionStatus', () => {
|
||||
it('accepts an upgraded host without changing the Cloud/Server values older readers know', () => {
|
||||
const site = {
|
||||
id: 'site-scoped',
|
||||
siteUrl: 'https://example.atlassian.net',
|
||||
email: '',
|
||||
displayName: 'Ada',
|
||||
accountId: 'account-alpha',
|
||||
authType: 'cloud',
|
||||
apiBaseUrl: 'https://api.atlassian.com/ex/jira/cloud-abc'
|
||||
}
|
||||
const status = parseJiraConnectionStatus({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
sites: [site],
|
||||
supportsScopedTokens: true
|
||||
})
|
||||
expect(status.sites).toEqual([site])
|
||||
expect(status.supportsScopedTokens).toBe(true)
|
||||
expect(
|
||||
parseJiraConnectionStatus({ connected: true, viewer: null }).supportsScopedTokens
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
parseJiraConnectionStatus({ connected: true, supportsScopedTokens: 'yes' })
|
||||
.supportsScopedTokens
|
||||
).toBeUndefined()
|
||||
})
|
||||
it.each([
|
||||
['undefined', undefined],
|
||||
['null', null],
|
||||
@@ -37,73 +11,6 @@ describe('parseJiraConnectionStatus', () => {
|
||||
expect(parseJiraConnectionStatus(value)).toEqual({ connected: false, viewer: null })
|
||||
})
|
||||
|
||||
it('keeps a well-formed status', () => {
|
||||
const status = {
|
||||
connected: true,
|
||||
viewer: { accountId: 'a', email: 'a@example.com', displayName: 'A' },
|
||||
sites: [],
|
||||
activeSiteId: 'site-1',
|
||||
credentialProtection: 'plaintext'
|
||||
}
|
||||
expect(parseJiraConnectionStatus(status)).toEqual(status)
|
||||
})
|
||||
|
||||
it('round-trips a populated multi-site status unchanged', () => {
|
||||
const status = {
|
||||
connected: true,
|
||||
viewer: {
|
||||
accountId: 'account-cloud',
|
||||
displayName: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
avatarUrl: 'https://avatar.example/ada.png'
|
||||
},
|
||||
sites: [
|
||||
{
|
||||
id: 'site-server',
|
||||
siteUrl: 'https://jira.internal.example',
|
||||
email: '',
|
||||
displayName: 'Ada (PAT)',
|
||||
accountId: '',
|
||||
authType: 'server'
|
||||
},
|
||||
{
|
||||
id: 'site-cloud',
|
||||
siteUrl: 'https://example.atlassian.net',
|
||||
email: 'ada@example.com',
|
||||
displayName: 'Ada Lovelace',
|
||||
accountId: 'account-cloud',
|
||||
authType: 'cloud'
|
||||
},
|
||||
{
|
||||
id: 'site-legacy',
|
||||
siteUrl: 'https://legacy.atlassian.net',
|
||||
email: 'ada@legacy.example',
|
||||
displayName: 'Ada (legacy)',
|
||||
accountId: 'account-legacy'
|
||||
}
|
||||
],
|
||||
activeSiteId: 'site-cloud',
|
||||
selectedSiteId: 'all',
|
||||
credentialError: 'Could not decrypt the stored Jira token.',
|
||||
credentialProtection: 'plaintext'
|
||||
}
|
||||
expect(parseJiraConnectionStatus(status)).toEqual(status)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a non-object viewer', { connected: true, viewer: 'me' }],
|
||||
['a non-array sites value', { connected: true, viewer: null, sites: {} }]
|
||||
])('keeps a connected status with %s', (_label, value) => {
|
||||
expect(parseJiraConnectionStatus(value)).toEqual({ connected: true, viewer: null })
|
||||
})
|
||||
|
||||
it('fills a missing viewer with null', () => {
|
||||
expect(parseJiraConnectionStatus({ connected: false })).toEqual({
|
||||
connected: false,
|
||||
viewer: null
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a connected status while dropping malformed nested values', () => {
|
||||
expect(
|
||||
parseJiraConnectionStatus({
|
||||
@@ -139,48 +46,4 @@ describe('parseJiraConnectionStatus', () => {
|
||||
selectedSiteId: 'site-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a site with an unknown auth type connected', () => {
|
||||
expect(
|
||||
parseJiraConnectionStatus({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
sites: [
|
||||
{
|
||||
id: 'site-1',
|
||||
siteUrl: 'https://example.atlassian.net',
|
||||
email: 'a@example.com',
|
||||
displayName: 'Example',
|
||||
accountId: 'account-1',
|
||||
authType: 'datacenter'
|
||||
}
|
||||
]
|
||||
})
|
||||
).toEqual({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
sites: [
|
||||
{
|
||||
id: 'site-1',
|
||||
siteUrl: 'https://example.atlassian.net',
|
||||
email: 'a@example.com',
|
||||
displayName: 'Example',
|
||||
accountId: 'account-1'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores the Keychain fields an older host still sends', () => {
|
||||
const status = parseJiraConnectionStatus({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
credentialError: 'Could not decrypt saved Jira credential.',
|
||||
credentialErrorReason: 'keychain_locked',
|
||||
canMoveCredentialKeyToFile: true
|
||||
})
|
||||
expect(status.credentialError).toBe('Could not decrypt saved Jira credential.')
|
||||
expect(status).not.toHaveProperty('credentialErrorReason')
|
||||
expect(status).not.toHaveProperty('canMoveCredentialKeyToFile')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { readJiraSavedFilterState, mutateJiraSavedFilters } from './runtime-jira-saved-filter-state'
|
||||
import { JIRA_SAVED_FILTER_RUNTIME_CAPABILITY } from '../../../shared/jira/jira-saved-filter-capability'
|
||||
const { assertCapability, rpc } = vi.hoisted(() => ({ assertCapability: vi.fn(), rpc: vi.fn() }))
|
||||
vi.mock('./runtime-rpc-client', () => ({
|
||||
assertRuntimeEnvironmentCapability: assertCapability,
|
||||
callRuntimeRpc: rpc
|
||||
}))
|
||||
const state = {
|
||||
filters: [{ id: 'mine', name: 'My work', jql: 'project = APP' }],
|
||||
activeFilterId: 'mine'
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
it('uses local UI state for local execution', async () => {
|
||||
const read = vi.fn(async () => state)
|
||||
const mutate = vi.fn(async () => state)
|
||||
vi.stubGlobal('window', {
|
||||
api: { ui: { getJiraSavedFilters: read, mutateJiraSavedFilters: mutate } }
|
||||
})
|
||||
expect(await readJiraSavedFilterState({ kind: 'local' })).toEqual(state)
|
||||
await mutateJiraSavedFilters({ kind: 'local' }, { kind: 'select', id: 'mine' })
|
||||
expect(mutate).toHaveBeenCalledWith({ kind: 'select', id: 'mine' })
|
||||
expect(rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
it('routes paired/SSH persistence to the selected Jira host rather than this device', async () => {
|
||||
rpc.mockResolvedValue(state)
|
||||
const target = { kind: 'environment', environmentId: 'ssh-host-b' } as const
|
||||
expect(await readJiraSavedFilterState(target)).toEqual(state)
|
||||
await mutateJiraSavedFilters(target, { kind: 'select', id: 'mine' })
|
||||
expect(assertCapability).toHaveBeenCalledWith(
|
||||
'ssh-host-b',
|
||||
JIRA_SAVED_FILTER_RUNTIME_CAPABILITY,
|
||||
expect.any(String),
|
||||
15_000
|
||||
)
|
||||
expect(rpc).toHaveBeenCalledWith(
|
||||
target,
|
||||
'ui.mutateJiraSavedFilters',
|
||||
{ kind: 'select', id: 'mine' },
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
expect(rpc.mock.calls.some(([, method]) => method === 'ui.set')).toBe(false)
|
||||
})
|
||||
it('never sends new fields to an older host or falls back to local persistence', async () => {
|
||||
assertCapability.mockRejectedValue(new Error('Update the Jira host'))
|
||||
await expect(
|
||||
mutateJiraSavedFilters(
|
||||
{ kind: 'environment', environmentId: 'old-host' },
|
||||
{ kind: 'select', id: 'mine' }
|
||||
)
|
||||
).rejects.toThrow('Update')
|
||||
expect(rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
it('propagates loss of contact instead of pretending the write was saved', async () => {
|
||||
rpc.mockRejectedValue(new Error('Connection lost'))
|
||||
await expect(
|
||||
mutateJiraSavedFilters(
|
||||
{ kind: 'environment', environmentId: 'host-a' },
|
||||
{ kind: 'select', id: null }
|
||||
)
|
||||
).rejects.toThrow('Connection lost')
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { jiraListIssues, jiraListIssuesForTarget } from './runtime-jira-client'
|
||||
import {
|
||||
readJiraQuickFilterState,
|
||||
writeJiraQuickFilterState
|
||||
} from './runtime-jira-quick-filter-state'
|
||||
import { JIRA_STATUS_FILTER_RUNTIME_CAPABILITY } from '../../../shared/jira/jira-quick-filters'
|
||||
const { assertCapability, rpc } = vi.hoisted(() => ({ assertCapability: vi.fn(), rpc: vi.fn() }))
|
||||
vi.mock('./runtime-rpc-client', () => ({
|
||||
assertRuntimeEnvironmentCapability: assertCapability,
|
||||
callRuntimeRpc: rpc
|
||||
}))
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
const target = { kind: 'environment', environmentId: 'jira-owner' } as const
|
||||
it('keeps the old request shape when no statuses are selected', async () => {
|
||||
rpc.mockResolvedValue([])
|
||||
await jiraListIssues(target, 'all', 50, 'site-a')
|
||||
expect(assertCapability).not.toHaveBeenCalled()
|
||||
expect(rpc).toHaveBeenCalledWith(
|
||||
target,
|
||||
'jira.listIssues',
|
||||
{ filter: 'all', limit: 50, siteId: 'site-a' },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
})
|
||||
it('gates status list filters on the called execution host', async () => {
|
||||
rpc.mockResolvedValue([])
|
||||
await jiraListIssuesForTarget(target, 'all', 50, 'site-a', { statuses: ['In Progress'] })
|
||||
expect(assertCapability).toHaveBeenCalledWith(
|
||||
'jira-owner',
|
||||
JIRA_STATUS_FILTER_RUNTIME_CAPABILITY,
|
||||
expect.any(String),
|
||||
15_000
|
||||
)
|
||||
expect(rpc).toHaveBeenCalledWith(
|
||||
target,
|
||||
'jira.listIssues',
|
||||
{ filter: 'all', limit: 50, siteId: 'site-a', filters: { statuses: ['In Progress'] } },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
})
|
||||
it('refuses an older host without silently dropping selection or writing strict resume fields', async () => {
|
||||
assertCapability.mockRejectedValue(new Error('Update the Jira host'))
|
||||
await expect(
|
||||
jiraListIssues(target, 'all', 50, 'site-a', { statuses: ['Blocked'] })
|
||||
).rejects.toThrow('Update')
|
||||
await expect(writeJiraQuickFilterState(target, { statuses: ['Blocked'] })).rejects.toThrow(
|
||||
'Update'
|
||||
)
|
||||
expect(rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
it('routes persisted state through host UI methods and acknowledges the write', async () => {
|
||||
rpc.mockResolvedValue({ statuses: ['Blocked'] })
|
||||
expect(await readJiraQuickFilterState(target)).toEqual({ statuses: ['Blocked'] })
|
||||
expect(await writeJiraQuickFilterState(target, { statuses: ['Blocked'] })).toEqual({
|
||||
statuses: ['Blocked']
|
||||
})
|
||||
expect(rpc).toHaveBeenLastCalledWith(
|
||||
target,
|
||||
'ui.setJiraQuickFilters',
|
||||
{ statuses: ['Blocked'] },
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
})
|
||||
it('uses local IPC and reports a lost write instead of pretending to save', async () => {
|
||||
const read = vi.fn(async () => ({ statuses: ['Open'] }))
|
||||
const write = vi.fn(async () => {
|
||||
throw new Error('Lost host')
|
||||
})
|
||||
vi.stubGlobal('window', {
|
||||
api: { ui: { getJiraQuickFilters: read, setJiraQuickFilters: write } }
|
||||
})
|
||||
expect(await readJiraQuickFilterState({ kind: 'local' })).toEqual({ statuses: ['Open'] })
|
||||
await expect(writeJiraQuickFilterState({ kind: 'local' }, { statuses: [] })).rejects.toThrow(
|
||||
'Lost host'
|
||||
)
|
||||
expect(rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { browseRuntimeServerDirectory } from './runtime-server-directory-browser'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from './runtime-rpc-client'
|
||||
import {
|
||||
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
|
||||
RUNTIME_PROTOCOL_VERSION
|
||||
} from '../../../shared/protocol-version'
|
||||
import { stubRuntimeEnvironmentBridge } from './runtime-environment-bridge-fixture'
|
||||
|
||||
const runtimeEnvironmentCall = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
runtimeEnvironmentCall.mockReset()
|
||||
runtimeEnvironmentCall.mockImplementation((args: { method: string }) => {
|
||||
if (args.method === 'status.get') {
|
||||
return Promise.resolve({
|
||||
id: 'status',
|
||||
ok: true,
|
||||
result: {
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION
|
||||
},
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: 'browse',
|
||||
ok: true,
|
||||
result: {
|
||||
resolvedPath: '/home/me',
|
||||
pathFlavor: 'posix',
|
||||
entries: [
|
||||
{ name: '100 - file.txt', isDirectory: false, isSymlink: false },
|
||||
{ name: '9 - file.txt', isDirectory: false, isSymlink: false },
|
||||
{ name: '10 - repo', isDirectory: true, isSymlink: false },
|
||||
{ name: '99 - file.txt', isDirectory: false, isSymlink: false }
|
||||
]
|
||||
},
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
})
|
||||
stubRuntimeEnvironmentBridge({ call: runtimeEnvironmentCall })
|
||||
})
|
||||
|
||||
describe('runtime server directory browser', () => {
|
||||
it('routes browse requests through the selected runtime environment', async () => {
|
||||
await expect(browseRuntimeServerDirectory('env-1', '~')).resolves.toEqual({
|
||||
resolvedPath: '/home/me',
|
||||
pathFlavor: 'posix',
|
||||
entries: [
|
||||
{ name: '10 - repo', isDirectory: true, isSymlink: false },
|
||||
{ name: '9 - file.txt', isDirectory: false, isSymlink: false },
|
||||
{ name: '99 - file.txt', isDirectory: false, isSymlink: false },
|
||||
{ name: '100 - file.txt', isDirectory: false, isSymlink: false }
|
||||
]
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenLastCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'files.browseServerDir',
|
||||
params: { path: '~' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SSH_TARGET_MANAGEMENT_RUNTIME_CAPABILITY } from '../../../shared/ssh-target-management'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
callRuntimeRpc: vi.fn(),
|
||||
runtimeEnvironmentSupportsCapability: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./runtime-rpc-client', () => ({
|
||||
callRuntimeRpc: mocks.callRuntimeRpc,
|
||||
runtimeEnvironmentSupportsCapability: mocks.runtimeEnvironmentSupportsCapability
|
||||
}))
|
||||
|
||||
const {
|
||||
addRuntimeSshRepo,
|
||||
addRuntimeSshTarget,
|
||||
browseRuntimeSshDirectory,
|
||||
listRuntimeEditableSshTargets,
|
||||
removeRuntimeSshTarget,
|
||||
RuntimeSshTargetManagementUnsupportedError,
|
||||
updateRuntimeSshTarget
|
||||
} = await import('./runtime-ssh-target-management')
|
||||
|
||||
const input = { label: 'Build', host: 'build.internal', port: 22, username: 'ci' }
|
||||
|
||||
const CALLS: [string, () => Promise<unknown>][] = [
|
||||
['list', () => listRuntimeEditableSshTargets('env-1')],
|
||||
['add', () => addRuntimeSshTarget('env-1', input)],
|
||||
['update', () => updateRuntimeSshTarget('env-1', 'ssh-1', input)],
|
||||
['remove', () => removeRuntimeSshTarget('env-1', 'ssh-1')],
|
||||
['browse', () => browseRuntimeSshDirectory('env-1', 'ssh-1', '~')],
|
||||
['add project', () => addRuntimeSshRepo('env-1', { connectionId: 'ssh-1', remotePath: '~/app' })]
|
||||
]
|
||||
|
||||
describe('managing a paired server SSH targets across versions', () => {
|
||||
const sshApi = { addTarget: vi.fn(), listTargets: vi.fn(), browseDir: vi.fn() }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('window', { api: { ssh: sshApi, repos: { addRemote: vi.fn() } } })
|
||||
})
|
||||
|
||||
it.each(CALLS)(
|
||||
'an old server without the capability refuses %s and nothing falls back to this client',
|
||||
async (_name, call) => {
|
||||
mocks.runtimeEnvironmentSupportsCapability.mockResolvedValue(false)
|
||||
|
||||
await expect(call()).rejects.toBeInstanceOf(RuntimeSshTargetManagementUnsupportedError)
|
||||
|
||||
expect(mocks.runtimeEnvironmentSupportsCapability).toHaveBeenCalledWith(
|
||||
'env-1',
|
||||
SSH_TARGET_MANAGEMENT_RUNTIME_CAPABILITY
|
||||
)
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(sshApi.addTarget).not.toHaveBeenCalled()
|
||||
expect(sshApi.listTargets).not.toHaveBeenCalled()
|
||||
expect(sshApi.browseDir).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(CALLS)('a current server is called for %s', async (_name, call) => {
|
||||
mocks.runtimeEnvironmentSupportsCapability.mockResolvedValue(true)
|
||||
mocks.callRuntimeRpc.mockResolvedValue({ targets: [], target: {}, repo: {} })
|
||||
|
||||
await call()
|
||||
|
||||
expect(mocks.callRuntimeRpc.mock.calls[0]?.[0]).toEqual({
|
||||
kind: 'environment',
|
||||
environmentId: 'env-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('sends a whole target so the server can clear omitted fields', async () => {
|
||||
mocks.runtimeEnvironmentSupportsCapability.mockResolvedValue(true)
|
||||
mocks.callRuntimeRpc.mockResolvedValue({ target: { id: 'ssh-1', ...input } })
|
||||
|
||||
await updateRuntimeSshTarget('env-1', 'ssh-1', input)
|
||||
|
||||
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-1' },
|
||||
'ssh.updateTarget',
|
||||
{ targetId: 'ssh-1', target: input },
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user