mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
src/cli/index.ts was the only eager value-import of RuntimeClient, and five other eager modules imported just RuntimeClientError / RuntimeRpcFailureError from the runtime-client barrel -- dragging in client -> pairing -> zod -> ws -> e2ee on every invocation. Those error classes live in runtime/types.ts, which has zero children, so the five imports now point there and the client loads through the existing (already lazy by design) ctx.client getter. Eager modules 199 -> 46, with node_modules dropping 94 -> 0. `orca --help` 2.04x (59.6 -> 29.2 ms); the same for help, no-args, and both error paths, which return before constructing a client. Commands that DO construct one still gain 1.10-1.12x from not eagerly parsing the transport the local path never uses. Correction to an earlier note: websocket-transport alone is ~24 modules / ~8 ms, not the 107 / 28 ms once recorded -- that figure wrongly charged it for zod, which enters through shared/pairing on a different edge. Marginal cost, never isolated cost. Co-authored-by: Orca <help@stably.ai>
745 lines
19 KiB
TypeScript
745 lines
19 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const callMock = vi.fn()
|
|
|
|
vi.mock('../runtime-client', async () => {
|
|
class RuntimeClient {
|
|
readonly isRemote: boolean
|
|
call = callMock
|
|
getCliStatus = vi.fn()
|
|
openOrca = vi.fn()
|
|
|
|
constructor(
|
|
_userDataPath?: string,
|
|
_requestTimeoutMs?: number,
|
|
remotePairingCode = process.env.ORCA_PAIRING_CODE ?? null,
|
|
environmentSelector = process.env.ORCA_ENVIRONMENT ?? null
|
|
) {
|
|
this.isRemote = Boolean(remotePairingCode || environmentSelector)
|
|
}
|
|
}
|
|
|
|
// Why: re-export the REAL error classes; format.ts narrows with `instanceof`
|
|
// against ./runtime/types, so a look-alike would collapse every CLI error
|
|
// code into the generic `runtime_error` shape.
|
|
const { RuntimeClientError, RuntimeRpcFailureError } = await import('../runtime/types.js')
|
|
|
|
return {
|
|
RuntimeClient,
|
|
RuntimeClientError,
|
|
RuntimeRpcFailureError
|
|
}
|
|
})
|
|
|
|
import { main } from '../index'
|
|
import { okFixture, queueFixtures } from '../test-fixtures'
|
|
|
|
describe('orca linear CLI handlers', () => {
|
|
const originalEnv = { ...process.env }
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks()
|
|
callMock.mockReset()
|
|
process.env = { ...originalEnv }
|
|
// Why: these tests can run inside an Orca-managed terminal, which exports
|
|
// real worktree/terminal/pairing env hints; clear them so handler context
|
|
// assertions stay deterministic.
|
|
delete process.env.ORCA_WORKTREE_ID
|
|
delete process.env.ORCA_TERMINAL_HANDLE
|
|
delete process.env.ORCA_PAIRING_CODE
|
|
delete process.env.ORCA_ENVIRONMENT
|
|
process.exitCode = undefined
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
})
|
|
|
|
it('maps --full issue reads to read-only issueContext RPC', async () => {
|
|
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
|
|
|
await main(['linear', 'issue', 'ENG-123', '--full', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueContext',
|
|
{
|
|
input: 'ENG-123',
|
|
current: false,
|
|
workspaceId: undefined,
|
|
include: {
|
|
comments: true,
|
|
children: true,
|
|
attachments: true,
|
|
relations: true,
|
|
activity: true
|
|
},
|
|
depth: 2,
|
|
context: {
|
|
remote: false,
|
|
cwd: '/tmp/repo'
|
|
}
|
|
},
|
|
{ timeoutMs: 120_000 }
|
|
)
|
|
})
|
|
|
|
it('maps MCP-compatible list-issues filters and cursor pagination', async () => {
|
|
queueFixtures(callMock, okFixture('req_list', issueResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'list-issues',
|
|
'--team',
|
|
'ENG',
|
|
'--assignee',
|
|
'me',
|
|
'--priority',
|
|
'2',
|
|
'--cursor',
|
|
'next-page',
|
|
'--order-by',
|
|
'createdAt',
|
|
'--include-archived',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.mcpListIssues',
|
|
expect.objectContaining({
|
|
team: 'ENG',
|
|
assignee: 'me',
|
|
priority: 2,
|
|
cursor: 'next-page',
|
|
orderBy: 'createdAt',
|
|
includeArchived: true
|
|
})
|
|
)
|
|
})
|
|
|
|
it('keeps global boolean flags before Linear commands from consuming command tokens', async () => {
|
|
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
|
|
|
await main(['--json', 'linear', 'issue', 'ENG-123', '--full'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueContext',
|
|
expect.objectContaining({
|
|
input: 'ENG-123',
|
|
include: expect.objectContaining({
|
|
comments: true,
|
|
children: true,
|
|
attachments: true,
|
|
relations: true,
|
|
activity: true
|
|
})
|
|
}),
|
|
{ timeoutMs: 120_000 }
|
|
)
|
|
})
|
|
|
|
it('passes verified current-context hints without resolving cwd for remote runtimes', async () => {
|
|
process.env.ORCA_TERMINAL_HANDLE = 'term_123'
|
|
process.env.ORCA_WORKTREE_ID = 'repo::/srv/app'
|
|
process.env.ORCA_PAIRING_CODE = 'orca://pair?payload=bad'
|
|
queueFixtures(callMock, okFixture('req_linear', issueResult()))
|
|
|
|
await main(['linear', 'issue', '--current', '--comments', '--json'], '/client/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueContext',
|
|
expect.objectContaining({
|
|
input: undefined,
|
|
current: true,
|
|
include: expect.objectContaining({ comments: true }),
|
|
context: {
|
|
remote: true,
|
|
worktreeId: 'repo::/srv/app',
|
|
terminalHandle: 'term_123'
|
|
}
|
|
}),
|
|
{ timeoutMs: undefined }
|
|
)
|
|
})
|
|
|
|
it('rejects --depth unless children are requested', async () => {
|
|
await main(['linear', 'issue', 'ENG-123', '--depth', '3'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(vi.mocked(console.error).mock.calls[0][0]).toContain(
|
|
'--depth requires --children or --full'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
})
|
|
|
|
it('maps search to agent search RPC with capped limit', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_search', {
|
|
issues: [],
|
|
meta: { query: 'auth', workspaceId: 'all', limit: 50, returned: 0, limitReached: false }
|
|
})
|
|
)
|
|
|
|
await main(['linear', 'search', 'auth', '--workspace', 'all', '--limit', '500'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('linear.agentSearchIssues', {
|
|
query: 'auth',
|
|
limit: 50,
|
|
workspaceId: 'all'
|
|
})
|
|
})
|
|
|
|
it('maps project list to agent project RPC with capped limit', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_projects', {
|
|
projects: [],
|
|
meta: {
|
|
query: 'launch',
|
|
workspaceId: 'all',
|
|
limit: 50,
|
|
returned: 0,
|
|
hasMore: false,
|
|
partial: false,
|
|
workspaceErrors: []
|
|
}
|
|
})
|
|
)
|
|
|
|
await main(
|
|
['linear', 'project', 'list', '--query', 'launch', '--workspace', 'all', '--limit', '500'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith('linear.agentProjectList', {
|
|
query: 'launch',
|
|
limit: 50,
|
|
workspaceId: 'all'
|
|
})
|
|
})
|
|
|
|
it('keeps boolean flags between Linear and search from consuming the subcommand', async () => {
|
|
queueFixtures(
|
|
callMock,
|
|
okFixture('req_search', {
|
|
issues: [],
|
|
meta: { query: 'auth', workspaceId: undefined, limit: 1, returned: 0, limitReached: false }
|
|
})
|
|
)
|
|
|
|
await main(['linear', '--json', 'search', 'auth', '--limit', '1'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith('linear.agentSearchIssues', {
|
|
query: 'auth',
|
|
limit: 1,
|
|
workspaceId: undefined
|
|
})
|
|
})
|
|
|
|
it('maps status writes to the agent write RPC with an explicit target', async () => {
|
|
queueFixtures(callMock, okFixture('req_status', statusSetResult()))
|
|
|
|
await main(['linear', 'status', 'set', 'ENG-123', '--to', 'In Review', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueSetState',
|
|
{
|
|
input: 'ENG-123',
|
|
current: false,
|
|
workspaceId: undefined,
|
|
to: 'In Review',
|
|
context: {
|
|
remote: false,
|
|
cwd: '/tmp/repo'
|
|
}
|
|
},
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('maps priority writes with Linear API priority numbering', async () => {
|
|
queueFixtures(callMock, okFixture('req_priority', taskUpdateResult('priority')))
|
|
|
|
await main(['linear', 'priority', 'set', 'ENG-123', '--to', 'urgent', '--json'], '/tmp/repo')
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueUpdateTask',
|
|
expect.objectContaining({
|
|
input: 'ENG-123',
|
|
operation: 'priority',
|
|
priority: 1
|
|
}),
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('maps relation writes to the current-issue perspective', async () => {
|
|
queueFixtures(callMock, okFixture('req_relation', relationWriteResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'relation',
|
|
'add',
|
|
'ENG-123',
|
|
'--related',
|
|
'ENG-456',
|
|
'--type',
|
|
'blocked-by',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueRelationWrite',
|
|
{
|
|
input: 'ENG-123',
|
|
current: false,
|
|
workspaceId: undefined,
|
|
relatedInput: 'ENG-456',
|
|
relationship: 'blockedBy',
|
|
operation: 'add',
|
|
context: { remote: false, cwd: '/tmp/repo' }
|
|
},
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('dispatches relation rm through the remove handler', async () => {
|
|
queueFixtures(callMock, okFixture('req_relation_rm', relationWriteResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'relation',
|
|
'rm',
|
|
'ENG-123',
|
|
'--related',
|
|
'ENG-456',
|
|
'--type',
|
|
'related',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueRelationWrite',
|
|
expect.objectContaining({
|
|
input: 'ENG-123',
|
|
relatedInput: 'ENG-456',
|
|
relationship: 'relatedTo',
|
|
operation: 'remove'
|
|
}),
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('rejects impossible due dates before dispatch', async () => {
|
|
await main(['linear', 'due-date', 'set', 'ENG-123', '--to', '2026-02-30'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(vi.mocked(console.error).mock.calls[0][0]).toContain('real calendar date')
|
|
})
|
|
|
|
it('preserves repeated labels for label updates', async () => {
|
|
queueFixtures(callMock, okFixture('req_label', taskUpdateResult('labels')))
|
|
|
|
await main(
|
|
['linear', 'label', 'add', 'ENG-123', '--label', 'Bug', '--label', 'Regression', '--json'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueUpdateTask',
|
|
expect.objectContaining({
|
|
input: 'ENG-123',
|
|
operation: 'labels',
|
|
labelMode: 'add',
|
|
labels: ['Bug', 'Regression']
|
|
}),
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('requires exact write targets for issue writes', async () => {
|
|
await main(['linear', 'comment', 'add', '--body', 'done'], '/tmp/repo')
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(vi.mocked(console.error).mock.calls[0][0]).toContain(
|
|
'Pass a Linear issue id or --current'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
})
|
|
|
|
it('rejects --workspace all for writes before dispatch', async () => {
|
|
await main(
|
|
[
|
|
'linear',
|
|
'attach',
|
|
'ENG-123',
|
|
'--url',
|
|
'https://example.com/review/123',
|
|
'--workspace',
|
|
'all'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(vi.mocked(console.error).mock.calls[0][0]).toContain(
|
|
'--workspace all is not valid for Linear writes'
|
|
)
|
|
expect(process.exitCode).toBe(1)
|
|
})
|
|
|
|
it('reads comment bodies from stdin and passes retry write ids through', async () => {
|
|
const stdin = mockStdin(false, ['line one\n', 'line two'])
|
|
queueFixtures(callMock, okFixture('req_comment', commentAddResult()))
|
|
|
|
try {
|
|
await main(
|
|
[
|
|
'linear',
|
|
'comment',
|
|
'add',
|
|
'--current',
|
|
'--body-file',
|
|
'-',
|
|
'--reply-to',
|
|
'comment-parent',
|
|
'--write-id',
|
|
'123e4567-e89b-12d3-a456-426614174000',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
} finally {
|
|
stdin.restore()
|
|
}
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueAddComment',
|
|
{
|
|
input: undefined,
|
|
current: true,
|
|
workspaceId: undefined,
|
|
body: 'line one\nline two',
|
|
replyTo: 'comment-parent',
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
context: {
|
|
remote: false,
|
|
cwd: '/tmp/repo'
|
|
}
|
|
},
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('rejects malformed write ids before dispatch', async () => {
|
|
await main(
|
|
[
|
|
'linear',
|
|
'attach',
|
|
'ENG-123',
|
|
'--url',
|
|
'https://example.com/review/123',
|
|
'--write-id',
|
|
'not-a-uuid',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
const payload = JSON.parse(String(vi.mocked(console.log).mock.calls[0][0])) as {
|
|
error: { code: string }
|
|
}
|
|
expect(payload.error.code).toBe('linear_invalid_write_id')
|
|
})
|
|
|
|
it('maps create with parent-current and optional body flags', async () => {
|
|
queueFixtures(callMock, okFixture('req_create', createResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'create',
|
|
'--title',
|
|
'Follow-up bug',
|
|
'--body',
|
|
'Concrete repro',
|
|
'--parent-current',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueCreate',
|
|
{
|
|
title: 'Follow-up bug',
|
|
body: 'Concrete repro',
|
|
teamInput: undefined,
|
|
state: undefined,
|
|
assignee: undefined,
|
|
priority: undefined,
|
|
estimate: undefined,
|
|
dueDate: undefined,
|
|
labels: [],
|
|
parentInput: undefined,
|
|
parentCurrent: true,
|
|
workspaceId: undefined,
|
|
writeId: undefined,
|
|
context: {
|
|
remote: false,
|
|
cwd: '/tmp/repo'
|
|
}
|
|
},
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('maps enriched create task fields', async () => {
|
|
queueFixtures(callMock, okFixture('req_create', createResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'create',
|
|
'--title',
|
|
'Triage bug',
|
|
'--team',
|
|
'ENG',
|
|
'--project',
|
|
'project-1',
|
|
'--state',
|
|
'Todo',
|
|
'--assignee',
|
|
'me',
|
|
'--priority',
|
|
'high',
|
|
'--estimate',
|
|
'2',
|
|
'--due-date',
|
|
'2026-06-30',
|
|
'--label',
|
|
'Bug',
|
|
'--label',
|
|
'Regression',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.issueCreate',
|
|
expect.objectContaining({
|
|
title: 'Triage bug',
|
|
teamInput: 'ENG',
|
|
projectInput: 'project-1',
|
|
state: 'Todo',
|
|
assignee: 'me',
|
|
priority: 2,
|
|
estimate: 2,
|
|
dueDate: '2026-06-30',
|
|
labels: ['Bug', 'Regression']
|
|
}),
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('maps MCP-style save-issue updates and explicit clears', async () => {
|
|
queueFixtures(callMock, okFixture('req_save', createResult()))
|
|
|
|
await main(
|
|
[
|
|
'linear',
|
|
'save-issue',
|
|
'ENG-123',
|
|
'--title',
|
|
'Updated title',
|
|
'--assignee',
|
|
'null',
|
|
'--estimate',
|
|
'null',
|
|
'--due-date',
|
|
'null',
|
|
'--project',
|
|
'null',
|
|
'--label',
|
|
'Bug',
|
|
'--json'
|
|
],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).toHaveBeenCalledWith(
|
|
'linear.saveIssue',
|
|
expect.objectContaining({
|
|
input: 'ENG-123',
|
|
title: 'Updated title',
|
|
assignee: null,
|
|
estimate: null,
|
|
dueDate: null,
|
|
project: null,
|
|
labels: ['Bug']
|
|
}),
|
|
{ timeoutMs: 75_000 }
|
|
)
|
|
})
|
|
|
|
it('rejects duplicate body inputs before dispatch', async () => {
|
|
await main(
|
|
['linear', 'create', '--title', 'Bug', '--body', 'one', '--body-file', 'body.md'],
|
|
'/tmp/repo'
|
|
)
|
|
|
|
expect(callMock).not.toHaveBeenCalled()
|
|
expect(vi.mocked(console.error).mock.calls[0][0]).toContain('Use either --body or --body-file')
|
|
})
|
|
})
|
|
|
|
function issueResult(): unknown {
|
|
return {
|
|
issue: {
|
|
id: 'issue-id',
|
|
identifier: 'ENG-123',
|
|
title: 'Fix auth',
|
|
url: 'https://linear.app/acme/issue/ENG-123',
|
|
state: { name: 'Todo' },
|
|
team: { name: 'Engineering' },
|
|
labels: []
|
|
},
|
|
meta: {
|
|
requested: {
|
|
current: false,
|
|
include: {
|
|
comments: false,
|
|
children: false,
|
|
attachments: false,
|
|
relations: false,
|
|
activity: false
|
|
},
|
|
depth: 2
|
|
},
|
|
resolved: {
|
|
id: 'issue-id',
|
|
identifier: 'ENG-123',
|
|
workspaceId: 'workspace-1',
|
|
workspaceName: 'Acme'
|
|
},
|
|
partial: false,
|
|
includeErrors: [],
|
|
sections: {}
|
|
}
|
|
}
|
|
}
|
|
|
|
function statusSetResult(): unknown {
|
|
return {
|
|
issue: { id: 'issue-id', 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 }
|
|
}
|
|
}
|
|
|
|
function commentAddResult(): unknown {
|
|
return {
|
|
comment: { id: 'comment-id', url: null, parentId: 'comment-parent' },
|
|
issue: { id: 'issue-id', identifier: 'ENG-123', url: 'https://linear.app/acme/issue/ENG-123' },
|
|
meta: {
|
|
workspaceId: 'workspace-1',
|
|
bodyChars: 17,
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
deduplicated: false
|
|
}
|
|
}
|
|
}
|
|
|
|
function createResult(): unknown {
|
|
return {
|
|
issue: {
|
|
id: 'issue-child',
|
|
identifier: 'ENG-456',
|
|
title: 'Follow-up bug',
|
|
url: 'https://linear.app/acme/issue/ENG-456',
|
|
team: { id: 'team-eng', key: 'ENG', name: 'Engineering' },
|
|
state: { id: 'state-triage', name: 'Triage' },
|
|
parent: { id: 'issue-id', identifier: 'ENG-123' }
|
|
},
|
|
meta: {
|
|
workspaceId: 'workspace-1',
|
|
writeId: '123e4567-e89b-12d3-a456-426614174000',
|
|
deduplicated: false
|
|
}
|
|
}
|
|
}
|
|
|
|
function taskUpdateResult(operation: string): unknown {
|
|
return {
|
|
issue: { id: 'issue-id', identifier: 'ENG-123', url: 'https://linear.app/acme/issue/ENG-123' },
|
|
operation,
|
|
previous: {},
|
|
current: {},
|
|
meta: { workspaceId: 'workspace-1', alreadySet: false }
|
|
}
|
|
}
|
|
|
|
function relationWriteResult(): unknown {
|
|
return {
|
|
issue: {
|
|
id: 'issue-id',
|
|
identifier: 'ENG-123',
|
|
title: 'Current',
|
|
url: 'https://linear.app/acme/issue/ENG-123'
|
|
},
|
|
relatedIssue: {
|
|
id: 'related-id',
|
|
identifier: 'ENG-456',
|
|
title: 'Related',
|
|
url: 'https://linear.app/acme/issue/ENG-456'
|
|
},
|
|
relation: {
|
|
id: 'relation-id',
|
|
type: 'blocks',
|
|
direction: 'inbound',
|
|
relationship: 'blockedBy'
|
|
},
|
|
operation: 'add',
|
|
meta: { workspaceId: 'workspace-1', alreadySet: false }
|
|
}
|
|
}
|
|
|
|
function mockStdin(isTTY: boolean, chunks: string[]): { restore: () => void } {
|
|
const stdin = process.stdin
|
|
const previousIsTTY = stdin.isTTY
|
|
const previousAsyncIterator = stdin[Symbol.asyncIterator]
|
|
Object.defineProperty(stdin, 'isTTY', {
|
|
configurable: true,
|
|
value: isTTY
|
|
})
|
|
;(stdin as unknown as Record<symbol, unknown>)[Symbol.asyncIterator] = async function* () {
|
|
for (const chunk of chunks) {
|
|
yield chunk
|
|
}
|
|
return undefined
|
|
}
|
|
return {
|
|
restore: () => {
|
|
Object.defineProperty(stdin, 'isTTY', {
|
|
configurable: true,
|
|
value: previousIsTTY
|
|
})
|
|
if (previousAsyncIterator) {
|
|
;(stdin as unknown as Record<symbol, unknown>)[Symbol.asyncIterator] = previousAsyncIterator
|
|
} else {
|
|
Reflect.deleteProperty(stdin, Symbol.asyncIterator)
|
|
}
|
|
}
|
|
}
|
|
}
|