mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
878 lines
27 KiB
TypeScript
878 lines
27 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
|
import { isLinearProjectListResult } from './ssh-remote-linear-result-guards'
|
|
import { runRemoteOrcaCli } from './ssh-remote-orca-cli'
|
|
|
|
function createRuntime() {
|
|
const runtime = {
|
|
getRuntimeId: () => 'runtime-test',
|
|
getStatus: () => ({
|
|
runtimeId: 'runtime-test',
|
|
rendererGraphEpoch: 1,
|
|
graphStatus: 'ready',
|
|
authoritativeWindowId: 1,
|
|
liveTabCount: 1,
|
|
liveLeafCount: 1
|
|
}),
|
|
linearIssueContext: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
title: 'Fix thing',
|
|
url: 'https://linear.app/acme/issue/ENG-123',
|
|
labels: [{ id: 'label-1', name: 'Bug' }],
|
|
priority: 2,
|
|
estimate: 5,
|
|
dueDate: '2026-06-30'
|
|
},
|
|
meta: {
|
|
requested: {
|
|
current: true,
|
|
depth: 2
|
|
},
|
|
resolved: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
workspaceId: 'workspace-1',
|
|
workspaceName: 'Acme'
|
|
},
|
|
partial: false,
|
|
includeErrors: [],
|
|
sections: {}
|
|
}
|
|
})),
|
|
linearSearchForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issues: [],
|
|
meta: {
|
|
query: 'auth bug',
|
|
limit: 5,
|
|
returned: 0,
|
|
limitReached: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})),
|
|
linearTeamListForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
teams: [
|
|
{
|
|
id: 'team-1',
|
|
key: 'ENG',
|
|
name: 'Engineering',
|
|
workspace: { id: 'workspace-1', name: 'Acme' }
|
|
}
|
|
],
|
|
meta: { workspaceId: 'workspace-1', returned: 1, partial: false, workspaceErrors: [] }
|
|
})),
|
|
linearTeamLabelsForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
team: { id: 'team-1', key: 'ENG', name: 'Engineering' },
|
|
labels: [{ id: 'label-1', name: 'Bug', color: '#ff0000' }],
|
|
meta: { workspaceId: 'workspace-1', returned: 1 }
|
|
})),
|
|
linearProjectListForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
projects: [
|
|
{
|
|
id: 'project-1',
|
|
name: 'Launch',
|
|
workspaceId: 'workspace-1',
|
|
workspaceName: 'Acme',
|
|
teams: [{ id: 'team-1', name: 'Engineering', key: 'ENG' }]
|
|
}
|
|
],
|
|
meta: {
|
|
query: 'launch',
|
|
workspaceId: 'workspace-1',
|
|
limit: 5,
|
|
returned: 1,
|
|
hasMore: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})),
|
|
linearIssueListForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issues: [],
|
|
meta: {
|
|
filter: 'open',
|
|
workspaceId: 'workspace-1',
|
|
limit: 5,
|
|
returned: 0,
|
|
hasMore: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})),
|
|
linearIssueSetState: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
url: 'https://linear.app/acme/issue/ENG-123'
|
|
},
|
|
state: { id: 'state-review', name: 'In Review', type: 'started' },
|
|
previousState: { id: 'state-started', name: 'In Progress' },
|
|
meta: { workspaceId: 'workspace-1', alreadyInState: false }
|
|
})),
|
|
linearIssueUpdateTask: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
url: 'https://linear.app/acme/issue/ENG-123'
|
|
},
|
|
operation: 'priority',
|
|
previous: {
|
|
assignee: null,
|
|
priority: 0,
|
|
estimate: null,
|
|
dueDate: null,
|
|
labels: []
|
|
},
|
|
current: {
|
|
assignee: null,
|
|
priority: 2,
|
|
estimate: null,
|
|
dueDate: null,
|
|
labels: []
|
|
},
|
|
meta: { workspaceId: 'workspace-1', alreadySet: false }
|
|
})),
|
|
linearIssueAddComment: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
comment: { id: 'comment-1', url: null, parentId: null },
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
url: 'https://linear.app/acme/issue/ENG-123'
|
|
},
|
|
meta: {
|
|
workspaceId: 'workspace-1',
|
|
bodyChars: 4,
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
deduplicated: false
|
|
}
|
|
})),
|
|
linearIssueAttachLink: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
attachment: { id: 'attachment-1', title: 'PR/MR link', url: 'https://example.com/review/1' },
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
url: 'https://linear.app/acme/issue/ENG-123'
|
|
},
|
|
meta: {
|
|
workspaceId: 'workspace-1',
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
deduplicated: false
|
|
}
|
|
})),
|
|
linearIssueCreate: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issue: {
|
|
id: 'issue-2',
|
|
identifier: 'ENG-456',
|
|
title: 'Follow-up',
|
|
url: 'https://linear.app/acme/issue/ENG-456',
|
|
team: { id: 'team-1', key: 'ENG', name: 'Engineering' },
|
|
state: { id: 'state-triage', name: 'Triage' },
|
|
parent: { id: 'issue-1', identifier: 'ENG-123' },
|
|
project: { id: 'project-1', name: 'Launch' }
|
|
},
|
|
meta: {
|
|
workspaceId: 'workspace-1',
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
deduplicated: false
|
|
}
|
|
}))
|
|
} as unknown as OrcaRuntimeService
|
|
return runtime
|
|
}
|
|
|
|
describe('runRemoteOrcaCli Linear commands', () => {
|
|
it('dispatches Linear issue reads through the remote runtime with SSH context hints', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', '--current', '--full', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { current: boolean; context: Record<string, unknown> } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
current: true,
|
|
include: expect.objectContaining({ activity: true }),
|
|
context: {
|
|
remote: true,
|
|
terminalHandle: 'term_ssh',
|
|
worktreeId: 'repo::remote'
|
|
}
|
|
})
|
|
expect(payload.result.request.context).not.toHaveProperty('cwd')
|
|
})
|
|
|
|
it('accepts leading boolean flags before SSH Linear commands', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['--json', 'linear', 'issue', 'ENG-123', '--full'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { input: string; include: Record<string, boolean> } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
input: 'ENG-123',
|
|
include: expect.objectContaining({ activity: true })
|
|
})
|
|
})
|
|
|
|
it('dispatches Linear search positional queries through the remote runtime', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'search', 'auth bug', '--limit', '5', '--workspace', 'all', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { query: string; limit: number; workspaceId: string } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toEqual({
|
|
query: 'auth bug',
|
|
limit: 5,
|
|
workspaceId: 'all'
|
|
})
|
|
})
|
|
|
|
it('dispatches Linear discovery and list reads through the remote runtime', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const teamList = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'team', 'list', '--workspace', 'all', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
const labels = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'team', 'labels', '--team', 'ENG', '--workspace', 'workspace-1', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
const list = await runRemoteOrcaCli(runtime, {
|
|
argv: [
|
|
'linear',
|
|
'list',
|
|
'--filter',
|
|
'open',
|
|
'--team',
|
|
'ENG',
|
|
'--limit',
|
|
'5',
|
|
'--workspace',
|
|
'workspace-1',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
const projects = await runRemoteOrcaCli(runtime, {
|
|
argv: [
|
|
'linear',
|
|
'project',
|
|
'list',
|
|
'--query',
|
|
'launch',
|
|
'--limit',
|
|
'5',
|
|
'--workspace',
|
|
'workspace-1',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(teamList.exitCode).toBe(0)
|
|
expect(labels.exitCode).toBe(0)
|
|
expect(list.exitCode).toBe(0)
|
|
expect(projects.exitCode).toBe(0)
|
|
expect(
|
|
(runtime as unknown as { linearTeamListForAgents: ReturnType<typeof vi.fn> })
|
|
.linearTeamListForAgents
|
|
).toHaveBeenCalledWith({ workspaceId: 'all' })
|
|
expect(
|
|
(runtime as unknown as { linearTeamLabelsForAgents: ReturnType<typeof vi.fn> })
|
|
.linearTeamLabelsForAgents
|
|
).toHaveBeenCalledWith({ teamInput: 'ENG', workspaceId: 'workspace-1' })
|
|
expect(
|
|
(runtime as unknown as { linearIssueListForAgents: ReturnType<typeof vi.fn> })
|
|
.linearIssueListForAgents
|
|
).toHaveBeenCalledWith({
|
|
filter: 'open',
|
|
teamInput: 'ENG',
|
|
limit: 5,
|
|
workspaceId: 'workspace-1'
|
|
})
|
|
expect(
|
|
(runtime as unknown as { linearProjectListForAgents: ReturnType<typeof vi.fn> })
|
|
.linearProjectListForAgents
|
|
).toHaveBeenCalledWith({
|
|
query: 'launch',
|
|
limit: 5,
|
|
workspaceId: 'workspace-1'
|
|
})
|
|
})
|
|
|
|
it('formats SSH Linear project list in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'project', 'list', '--query', 'launch', '--workspace', 'workspace-1'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toContain('Launch')
|
|
expect(result.stdout).toContain('project-1')
|
|
expect(result.stdout).toContain('ENG')
|
|
expect(result.stdout).toContain('Acme')
|
|
expect(result.stderr).toBe('')
|
|
})
|
|
|
|
it('rejects malformed SSH Linear project list results before formatting', () => {
|
|
expect(
|
|
isLinearProjectListResult({
|
|
projects: [{ id: 'project-1' }],
|
|
meta: {
|
|
limit: 5,
|
|
returned: 1,
|
|
hasMore: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
isLinearProjectListResult({
|
|
projects: [{ id: 'project-1', name: 'Launch', teams: [{ id: 'team-1' }] }],
|
|
meta: {
|
|
limit: 5,
|
|
returned: 1,
|
|
hasMore: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('dispatches Linear status writes through the remote runtime with SSH context hints', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'status', 'set', 'ENG-123', '--to', 'In Review', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { input: string; to: string; context: Record<string, unknown> } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
input: 'ENG-123',
|
|
to: 'In Review',
|
|
context: {
|
|
remote: true,
|
|
terminalHandle: 'term_ssh',
|
|
worktreeId: 'repo::remote'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('dispatches Linear task-field writes through the SSH remote runtime', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'priority', 'set', 'ENG-123', '--to', 'high', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { input: string; operation: string; priority: number } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
input: 'ENG-123',
|
|
operation: 'priority',
|
|
priority: 2
|
|
})
|
|
})
|
|
|
|
it('dispatches Linear creates with project input through the SSH remote runtime', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: [
|
|
'linear',
|
|
'create',
|
|
'--title',
|
|
'Follow-up',
|
|
'--team',
|
|
'ENG',
|
|
'--project',
|
|
'project-1',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { title: string; teamInput: string; projectInput: string } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
title: 'Follow-up',
|
|
teamInput: 'ENG',
|
|
projectInput: 'project-1'
|
|
})
|
|
})
|
|
|
|
it('formats SSH Linear creates with project input in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'create', '--title', 'Follow-up', '--team', 'ENG', '--project', 'project-1'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toBe('Created ENG-456 under ENG-123 in Launch: Follow-up.\n')
|
|
expect(result.stderr).toBe('')
|
|
})
|
|
|
|
it('parses --me as a boolean for SSH Linear assignee writes', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'assignee', 'set', '--me', 'ENG-123', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_WORKTREE_ID: 'repo::remote'
|
|
}
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { input: string; operation: string; assigneeMe: boolean } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
input: 'ENG-123',
|
|
operation: 'assignee',
|
|
assigneeMe: true
|
|
})
|
|
})
|
|
|
|
it('preserves repeated labels for SSH Linear label writes', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: [
|
|
'linear',
|
|
'label',
|
|
'set',
|
|
'ENG-123',
|
|
'--label',
|
|
'label-1',
|
|
'--label',
|
|
'label-2',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { operation: string; labelMode: string; labels: string[] } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request).toMatchObject({
|
|
operation: 'labels',
|
|
labelMode: 'set',
|
|
labels: ['label-1', 'label-2']
|
|
})
|
|
})
|
|
|
|
it('formats SSH Linear writes in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'comment', 'add', 'ENG-123', '--body', 'Done'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toBe('Added comment comment-1 to ENG-123.\n')
|
|
expect(result.stderr).toBe('')
|
|
})
|
|
|
|
it('dispatches body-file stdin writes in the SSH shim', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'comment', 'add', '--current', '--body-file', '-', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' },
|
|
stdin: 'line one\nline two\n'
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { request: { body: string } }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.request.body).toBe('line one\nline two\n')
|
|
})
|
|
|
|
it('rejects body-file stdin writes when SSH stdin is unavailable', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'comment', 'add', '--current', '--body-file', '-', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string; message: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error).toMatchObject({
|
|
code: 'invalid_argument',
|
|
message: 'SSH Linear writes require stdin when using --body-file -.'
|
|
})
|
|
})
|
|
|
|
it('rejects remote body-file paths in the SSH shim before dispatch', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'comment', 'add', '--current', '--body-file', 'body.md', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string; message: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error).toMatchObject({
|
|
code: 'invalid_argument',
|
|
message: 'SSH Linear writes only support --body-file - for stdin.'
|
|
})
|
|
})
|
|
|
|
it('formats SSH Linear issue reads in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', '--current'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toContain('ENG-123 Fix thing')
|
|
expect(result.stdout).toContain('URL: https://linear.app/acme/issue/ENG-123')
|
|
expect(result.stdout).toContain('Priority: high')
|
|
expect(result.stdout).toContain('Estimate: 5')
|
|
expect(result.stdout).toContain('Labels: Bug')
|
|
expect(result.stdout).toContain('Due: 2026-06-30')
|
|
expect(result.stdout).not.toContain('"issue"')
|
|
})
|
|
|
|
it('prints SSH Linear search partial warnings to stderr in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
const linearSearchForAgents = (
|
|
runtime as unknown as { linearSearchForAgents: ReturnType<typeof vi.fn> }
|
|
).linearSearchForAgents
|
|
linearSearchForAgents.mockResolvedValueOnce({
|
|
issues: [],
|
|
meta: {
|
|
query: 'auth',
|
|
limit: 20,
|
|
returned: 0,
|
|
limitReached: false,
|
|
partial: true,
|
|
workspaceErrors: [
|
|
{
|
|
workspace: { id: 'workspace-stale', name: 'Stale' },
|
|
code: 'linear_network_error',
|
|
message: 'fetch failed'
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'search', 'auth'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toBe('No Linear issues found.\n')
|
|
expect(result.stderr).toContain('warning: Stale unavailable for Linear search: fetch failed')
|
|
})
|
|
|
|
it('formats older SSH Linear search results without workspaceErrors in non-json mode', async () => {
|
|
const runtime = createRuntime()
|
|
const linearSearchForAgents = (
|
|
runtime as unknown as { linearSearchForAgents: ReturnType<typeof vi.fn> }
|
|
).linearSearchForAgents
|
|
linearSearchForAgents.mockResolvedValueOnce({
|
|
issues: [],
|
|
meta: {
|
|
query: 'auth',
|
|
limit: 20,
|
|
returned: 0,
|
|
limitReached: false,
|
|
partial: false
|
|
}
|
|
})
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'search', 'auth'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toBe('No Linear issues found.\n')
|
|
expect(result.stderr).toBe('')
|
|
})
|
|
|
|
it('prints SSH Linear non-json failures to stderr instead of stdout', async () => {
|
|
const runtime = createRuntime()
|
|
const linearIssueContext = (
|
|
runtime as unknown as { linearIssueContext: ReturnType<typeof vi.fn> }
|
|
).linearIssueContext
|
|
linearIssueContext.mockRejectedValueOnce(new Error('Linear is not connected.'))
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', '--current'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(result.stdout).toBe('')
|
|
expect(result.stderr).toContain('Linear is not connected.')
|
|
})
|
|
|
|
it('prints SSH Linear non-json next steps from structured errors', async () => {
|
|
const runtime = createRuntime()
|
|
const linearIssueAddComment = (
|
|
runtime as unknown as { linearIssueAddComment: ReturnType<typeof vi.fn> }
|
|
).linearIssueAddComment
|
|
linearIssueAddComment.mockRejectedValueOnce(
|
|
Object.assign(new Error('Linear may have applied the write.'), {
|
|
code: 'linear_write_unconfirmed',
|
|
data: { nextSteps: ['Retry once with the pinned command: `orca linear comment add`.'] }
|
|
})
|
|
)
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'comment', 'add', 'ENG-123', '--body', 'Done'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(result.stdout).toBe('')
|
|
expect(result.stderr).toContain('Linear may have applied the write.')
|
|
expect(result.stderr).toContain('Next step: Retry once with the pinned command')
|
|
})
|
|
|
|
it('shows SSH Linear command help without dispatching to the runtime', async () => {
|
|
const runtime = createRuntime()
|
|
const linearIssueContext = (
|
|
runtime as unknown as { linearIssueContext: ReturnType<typeof vi.fn> }
|
|
).linearIssueContext
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', '--help'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toContain('orca linear issue')
|
|
expect(result.stdout).toContain('Usage: orca linear issue')
|
|
expect(linearIssueContext).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows SSH Linear group help without dispatching to the runtime', async () => {
|
|
const runtime = createRuntime()
|
|
const linearIssueContext = (
|
|
runtime as unknown as { linearIssueContext: ReturnType<typeof vi.fn> }
|
|
).linearIssueContext
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', '--help'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(result.stdout).toContain('orca linear')
|
|
expect(result.stdout).toContain('Usage: orca linear <command> [options]')
|
|
expect(result.stdout).toContain('search')
|
|
expect(result.stdout).toContain('team list')
|
|
expect(result.stdout).toContain('label set')
|
|
expect(result.stdout).toContain('comment add')
|
|
expect(linearIssueContext).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows SSH Linear help through the local help command form', async () => {
|
|
const runtime = createRuntime()
|
|
const linearIssueContext = (
|
|
runtime as unknown as { linearIssueContext: ReturnType<typeof vi.fn> }
|
|
).linearIssueContext
|
|
|
|
const group = await runRemoteOrcaCli(runtime, {
|
|
argv: ['help', 'linear'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
const issue = await runRemoteOrcaCli(runtime, {
|
|
argv: ['help', 'linear', 'issue'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(group.exitCode).toBe(0)
|
|
expect(group.stdout).toContain('Usage: orca linear <command> [options]')
|
|
expect(issue.exitCode).toBe(0)
|
|
expect(issue.stdout).toContain('Usage: orca linear issue')
|
|
expect(linearIssueContext).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects ambiguous Linear issue positional and flag ids in the remote shim', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', 'ENG-123', '--id', 'ENG-456', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string; message: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error).toMatchObject({
|
|
code: 'invalid_argument',
|
|
message: 'Pass --id either positionally or as a flag, not both.'
|
|
})
|
|
})
|
|
|
|
it('rejects invalid Linear numeric flags in the remote shim', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'search', 'auth', '--limit', 'bad', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string; message: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error).toMatchObject({
|
|
code: 'invalid_argument',
|
|
message: 'Invalid numeric value for --limit'
|
|
})
|
|
})
|
|
|
|
it('preserves Linear-specific JSON error codes for pre-dispatch remote shim validation', async () => {
|
|
const runtime = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(runtime, {
|
|
argv: ['linear', 'issue', 'ENG-123', '--workspace', 'all', '--json'],
|
|
cwd: '/home/alice/remote-repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
})
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string; message: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error).toMatchObject({
|
|
code: 'linear_invalid_workspace',
|
|
message: '--workspace all is not valid for issue'
|
|
})
|
|
})
|
|
})
|