diff --git a/config/scripts/dependency-security-release.test.mjs b/config/scripts/dependency-security-release.test.mjs index 03c30d3d203..9166381776c 100644 --- a/config/scripts/dependency-security-release.test.mjs +++ b/config/scripts/dependency-security-release.test.mjs @@ -142,12 +142,12 @@ it('retains fork reports without requiring a Slack secret and redacts delivery e sendNotification({ fetch: vi.fn().mockRejectedValue(new Error('fetch failed: https://example.test/slack')) }) - ).rejects.toThrow('Slack dependency review notification could not be sent') + ).rejects.toThrow(/^Slack dependency review notification could not be sent$/) await expect( sendNotification({ fetch: vi.fn().mockResolvedValue({ ok: false, status: 503 }) }) - ).rejects.toThrow('Slack dependency review notification returned HTTP 503') + ).rejects.toThrow(/^Slack dependency review notification returned HTTP 503$/) }) it('extracts release data while excluding executable package and Cargo configuration', async () => { diff --git a/src/cli/index-reference-jira.test.ts b/src/cli/index-reference-jira.test.ts index 9fa163b5786..f730ddb75be 100644 --- a/src/cli/index-reference-jira.test.ts +++ b/src/cli/index-reference-jira.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ callMock: vi.fn(), @@ -31,10 +31,6 @@ import { buildWorktree, okFixture } from './test-fixtures' import { RuntimeClientError } from './runtime/types' import { JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY } from '../shared/jira/jira-agent-capability' import { WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY } from '../shared/workspace-attachment-capabilities' -import { - getWorkspaceReferenceIdentity, - parseWorkspaceReferenceUrl -} from '../shared/workspace-reference-identity' const site = { id: 'site-1', @@ -56,9 +52,17 @@ const issue = { updatedAt: '2026-10-13', createdAt: '2026-10-12' } -const attachment = { ...parseWorkspaceReferenceUrl(issue.url), title: issue.title } +const jiraReference = { + provider: 'jira' as const, + type: 'issue' as const, + number: 0, + identifier: 'ENG-123', + jiraIdentifier: 'ENG-123', + url: issue.url +} +const attachment = { ...jiraReference, title: issue.title } const reviewUrl = 'https://gitlab.com/acme/api/-/merge_requests/12' -const review = parseWorkspaceReferenceUrl(reviewUrl) +const review = { provider: 'gitlab' as const, type: 'mr' as const, number: 12, url: reviewUrl } const workspace = { ...buildWorktree('/tmp/api', 'api'), kind: 'worktree', name: 'api' } const addArgs = ['reference', 'add', 'eng-123', '--worktree', 'name:api'] const createArgs = [ @@ -106,8 +110,16 @@ function expectNoWrite(): void { } describe('Jira reference keys', () => { + const priorExitCode = process.exitCode + beforeEach(() => { + process.exitCode = undefined + }) + afterEach(() => { + process.exitCode = priorExitCode + }) useWorktreeAwarenessEnvironment(mocks) beforeEach(() => { + mocks.runtimeClientConstructorMock.mockClear() vi.spyOn(console, 'log').mockImplementation(() => {}) vi.spyOn(console, 'error').mockImplementation(() => {}) pairRuntimeEnvironment(mocks.listEnvironmentsMock, 'remote') @@ -123,6 +135,7 @@ describe('Jira reference keys', () => { expect(mocks.callMock).toHaveBeenCalledWith( 'worktree.set', expect.objectContaining({ + worktree: 'id:repo::/tmp/api', linkedItemsBase: [], linkedItems: [attachment], linkedItemsSelectionChanged: false @@ -132,13 +145,18 @@ describe('Jira reference keys', () => { it('uses the selected execution host for Jira lookup and workspace mutation', async () => { await main([...addArgs, '--environment', 'remote'], '/tmp/api') - expect(mocks.runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'remote') + expect(mocks.runtimeClientConstructorMock).toHaveBeenCalledExactlyOnceWith(undefined, 'remote') expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', { key: 'ENG-123', siteId: site.id }) expect(mocks.callMock).toHaveBeenCalledWith('reference.list', { worktree: 'name:api' }) - expect(mocks.callMock).toHaveBeenCalledWith('worktree.set', expect.anything()) + expect(mocks.callMock).toHaveBeenCalledWith('worktree.set', { + worktree: 'id:repo::/tmp/api', + linkedItemsBase: [], + linkedItems: [attachment], + linkedItemsSelectionChanged: false + }) }) it('refuses ambiguous sites rather than using activeSiteId', async () => { @@ -157,7 +175,12 @@ describe('Jira reference keys', () => { it.each(['all', 'missing'])('rejects invalid explicit site %s before lookup', async (id) => { await main([...addArgs, '--reference-site', id], '/tmp/api') - expect(console.error).toHaveBeenCalled() + expect(console.error).toHaveBeenCalledExactlyOnceWith( + id === 'all' + ? 'Choose one Jira site ID; --reference-site all is unsupported.' + : 'No matching connected Jira site. Check orca jira connection and Orca Jira settings.' + ) + expect(process.exitCode).toBe(1) expect(mocks.callMock).not.toHaveBeenCalledWith('jira.lookupIssueSummary', expect.anything()) expectNoWrite() }) @@ -182,7 +205,7 @@ describe('Jira reference keys', () => { expect(mocks.callMock).toHaveBeenCalledWith( 'worktree.set', expect.objectContaining({ - linkedItems: [{ ...parseWorkspaceReferenceUrl(serverIssue.url), title: issue.title }] + linkedItems: [{ ...attachment, url: serverIssue.url }] }) ) }) @@ -198,7 +221,14 @@ describe('Jira reference keys', () => { ])('rejects an absent or mismatched summary %#', async (summary) => { responses({ 'jira.lookupIssueSummary': summary }) await main(addArgs, '/tmp/api') - expect(console.error).toHaveBeenCalled() + expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', { + key: 'ENG-123', + siteId: site.id + }) + expect(console.error).toHaveBeenCalledExactlyOnceWith( + 'Jira did not return issue ENG-123 with a title on the selected site.' + ) + expect(process.exitCode).toBe(1) expectNoWrite() }) @@ -241,7 +271,13 @@ describe('Jira reference keys', () => { responses({ 'reference.list': { worktree: workspace, - references: [{ ...stored, key: getWorkspaceReferenceIdentity(stored), selected: false }] + references: [ + { + ...stored, + key: '["jira","issue","example.atlassian.net","/browse/ENG-123"]', + selected: false + } + ] } }) await main([...addArgs, '--json'], '/tmp/api') @@ -273,6 +309,7 @@ describe('Jira reference keys', () => { it('resolves create references before workspace creation', async () => { await main(createArgs, '/tmp/api') const methods = mocks.callMock.mock.calls.map(([method]) => method) + expect(methods).toContain('jira.lookupIssueSummary') expect(methods.indexOf('jira.lookupIssueSummary')).toBeLessThan( methods.indexOf('worktree.create') ) @@ -285,6 +322,18 @@ describe('Jira reference keys', () => { it('creates nothing when the Jira reference cannot resolve', async () => { responses({ 'jira.lookupIssueSummary': null }) await main(createArgs, '/tmp/api') + expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', { + key: 'ENG-123', + siteId: site.id + }) + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({ + ok: false, + error: { + code: 'jira_issue_unavailable', + message: 'Jira did not return issue ENG-123 with a title on the selected site.' + } + }) + expect(process.exitCode).toBe(1) expectNoWrite() }) @@ -312,7 +361,18 @@ describe('Jira reference keys', () => { key: 'ENG-124', siteId: site.id }) - expect(console.error).toHaveBeenCalled() + expect( + mocks.callMock.mock.calls + .filter(([method]) => method === 'jira.lookupIssueSummary') + .map(([, params]) => params) + ).toEqual([ + { key: 'ENG-123', siteId: site.id }, + { key: 'ENG-124', siteId: site.id } + ]) + expect(console.error).toHaveBeenCalledExactlyOnceWith( + 'Jira did not return issue ENG-124 with a title on the selected site.' + ) + expect(process.exitCode).toBe(1) expectNoWrite() }) @@ -328,11 +388,11 @@ describe('Jira reference keys', () => { it('keeps full Jira URLs usable without a saved Jira connection', async () => { responses({ 'status.get': { capabilities: [WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY] } }) await main(['reference', 'add', issue.url, '--worktree', 'name:api'], '/tmp/api') - expect(mocks.callMock).not.toHaveBeenCalledWith('jira.status') + expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('jira.status') expect(mocks.callMock).toHaveBeenCalledWith( 'worktree.set', expect.objectContaining({ - linkedItems: [parseWorkspaceReferenceUrl(issue.url)] + linkedItems: [jiraReference] }) ) }) @@ -341,7 +401,7 @@ describe('Jira reference keys', () => { responses({ 'status.get': { capabilities: [WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY] } }) await main(addArgs, '/tmp/api') expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Update Orca')) - expect(mocks.callMock).not.toHaveBeenCalledWith('jira.status') + expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('jira.status') expectNoWrite() }) }) diff --git a/src/cli/index-reference.test.ts b/src/cli/index-reference.test.ts index 82c77cb5efe..1f08f8e07e0 100644 --- a/src/cli/index-reference.test.ts +++ b/src/cli/index-reference.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { callMock, @@ -42,17 +42,24 @@ vi.mock('child_process', async () => { import { main } from './index' import { buildWorktree, okFixture } from './test-fixtures' -import { - getWorkspaceReferenceIdentity, - parseWorkspaceReferenceUrl -} from '../shared/workspace-reference-identity' +import { getWorkspaceReferenceIdentity } from '../shared/workspace-reference-identity' +import type { WorkspaceAttachment } from '../shared/worktree/types' import { WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY } from '../shared/workspace-attachment-capabilities' import { useWorktreeAwarenessEnvironment } from './index-test-harness' const PR = 'https://github.com/acme/api/pull/5123' const TASK = 'https://linear.app/acme/issue/STA-1234' -const parsedPR = parseWorkspaceReferenceUrl(PR) -const parsedTask = parseWorkspaceReferenceUrl(TASK) +const PR_KEY = '["github","pr","github.com","/acme/api/pull/5123"]' +const parsedPR: WorkspaceAttachment = { provider: 'github', type: 'pr', number: 5123, url: PR } +const parsedTask: WorkspaceAttachment = { + provider: 'linear', + type: 'issue', + number: 0, + identifier: 'STA-1234', + linearIdentifier: 'STA-1234', + linearOrganizationUrlKey: 'acme', + url: TASK +} function listResult(references = [parsedPR], kind: 'worktree' | 'folder' = 'worktree') { return { @@ -94,6 +101,13 @@ function mockCalls( } describe('reference CLI', () => { + const priorExitCode = process.exitCode + beforeEach(() => { + process.exitCode = undefined + }) + afterEach(() => { + process.exitCode = priorExitCode + }) useWorktreeAwarenessEnvironment({ callMock, serveOrcaAppMock, @@ -112,7 +126,14 @@ describe('reference CLI', () => { linkedItems: [parsedPR, parsedTask], linkedItemsSelectionChanged: false }) - expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toHaveLength(2) + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toEqual([ + { key: PR_KEY, operation: 'add', changed: true }, + { + key: '["linear","issue","linear.app","/acme/issue/STA-1234"]', + operation: 'add', + changed: true + } + ]) }) it('validates every URL before any request', async () => { @@ -125,17 +146,20 @@ describe('reference CLI', () => { it('requires the delta capability even if the host supports linked items', async () => { mockCalls(listResult(), listResult(), ['worktree.linked-items.v1']) await main(['reference', 'add', TASK, '--worktree', 'name:api'], '/tmp/api') - expect(callMock).toHaveBeenCalledTimes(1) + expect(callMock).toHaveBeenCalledExactlyOnceWith('status.get') expect(console.error).toHaveBeenCalledWith(expect.stringContaining('cannot merge')) }) it('duplicate adds preserve current metadata and produce a no-op', async () => { - mockCalls() + mockCalls(listResult([{ ...parsedPR, title: 'Saved title' }])) await main(['reference', 'add', PR, PR, '--worktree', 'name:api', '--json'], '/tmp/api') expect(callMock.mock.calls.some(([method]) => method === 'worktree.set')).toBe(false) + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.references[0].title).toBe( + 'Saved title' + ) expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toEqual([ { - key: getWorkspaceReferenceIdentity(parsedPR), + key: PR_KEY, operation: 'add', changed: false, reason: 'already_linked' @@ -145,17 +169,7 @@ describe('reference CLI', () => { it('removes only requested keys from folders via existing folder storage', async () => { mockCalls(listResult([parsedPR, parsedTask], 'folder'), listResult([parsedTask], 'folder')) - await main( - [ - 'reference', - 'remove', - '--key', - getWorkspaceReferenceIdentity(parsedPR), - '--worktree', - 'folder:folder1' - ], - '/tmp/api' - ) + await main(['reference', 'remove', '--key', PR_KEY, '--worktree', 'folder:folder1'], '/tmp/api') expect(callMock).toHaveBeenCalledWith('folderWorkspace.update', { folderWorkspaceId: 'folder1', updates: { @@ -167,7 +181,7 @@ describe('reference CLI', () => { }) it('removes a URL without touching the same review number in another repository', async () => { - const other = parseWorkspaceReferenceUrl('https://github.com/acme/other/pull/5123') + const other = { ...parsedPR, url: 'https://github.com/acme/other/pull/5123' } mockCalls(listResult([parsedPR, other]), listResult([other])) await main(['reference', 'remove', PR, '--worktree', 'name:api'], '/tmp/api') expect(callMock).toHaveBeenCalledWith( @@ -308,13 +322,21 @@ describe('reference CLI', () => { }) it.each([ - ['5123'], - ['STA-1234', '--worktree', 'name:api', '--repo', 'id:repo'], - ['STA-1234', '--limit', '0'] - ])('rejects invalid find arguments %j before querying', async (...args) => { + { args: ['5123'], message: 'Expected a full reference URL (https://…).' }, + { + args: ['STA-1234', '--worktree', 'name:api', '--repo', 'id:repo'], + message: 'Pass --worktree or --repo, not both.' + }, + { args: ['STA-1234', '--limit', '0'], message: 'Invalid positive integer for --limit' } + ])('rejects invalid find arguments $args before querying', async ({ args, message }) => { mockCalls() - await main(['reference', 'find', ...args], '/tmp/api') + await main(['reference', 'find', ...args, '--json'], '/tmp/api') expect(callMock).not.toHaveBeenCalled() + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({ + ok: false, + error: { code: 'invalid_argument', message } + }) + expect(process.exitCode).toBe(1) }) it('seeds repeated references in the create request before setup or agent startup', async () => { @@ -357,11 +379,17 @@ describe('reference CLI', () => { '--reference', PR, `--${flag}`, - '4' + '4', + '--json' ], '/tmp/api' ) expect(callMock).not.toHaveBeenCalled() + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({ + ok: false, + error: { code: 'invalid_argument', message: `Cannot combine --reference with --${flag}.` } + }) + expect(process.exitCode).toBe(1) } ) @@ -380,13 +408,16 @@ describe('reference CLI', () => { } return base?.(method, ...rest) }) - const priorExitCode = process.exitCode - await main(argv, '/tmp/api') - const printed = vi.mocked(console.error).mock.calls.flat().join('\n') - expect(printed).toContain('Update Orca on the execution host') - expect(printed).not.toContain('Unknown method') + await main([...argv, '--json'], '/tmp/api') + expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({ + ok: false, + error: { + code: 'incompatible_runtime', + message: + 'This Orca host does not support reference commands yet. Update Orca on the execution host.' + } + }) expect(process.exitCode).toBe(1) - process.exitCode = priorExitCode }) it('validates create flags locally before probing reference capability', async () => { diff --git a/src/cli/index-serve-command.test.ts b/src/cli/index-serve-command.test.ts index 2ce7be81b1e..4f831de3220 100644 --- a/src/cli/index-serve-command.test.ts +++ b/src/cli/index-serve-command.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { callMock, @@ -44,6 +44,13 @@ import { main } from './index' import { useWorktreeAwarenessEnvironment } from './index-test-harness' describe('orca cli worktree awareness', () => { + const priorExitCode = process.exitCode + beforeEach(() => { + process.exitCode = undefined + }) + afterEach(() => { + process.exitCode = priorExitCode + }) useWorktreeAwarenessEnvironment({ callMock, serveOrcaAppMock, @@ -54,7 +61,7 @@ describe('orca cli worktree awareness', () => { }) it('starts a foreground headless server through `serve`', async () => { - serveOrcaAppMock.mockResolvedValue(0) + serveOrcaAppMock.mockResolvedValue(17) process.env.ORCA_ENVIRONMENT = 'stale-env' await main( @@ -62,7 +69,7 @@ describe('orca cli worktree awareness', () => { '/tmp/repo' ) - expect(serveOrcaAppMock).toHaveBeenCalledWith({ + expect(serveOrcaAppMock).toHaveBeenCalledExactlyOnceWith({ json: true, port: '6768', pairingAddress: '100.64.1.20', @@ -72,6 +79,8 @@ describe('orca cli worktree awareness', () => { recipeJson: false, projectRoot: null }) + expect(process.exitCode).toBe(17) + expect(callMock).not.toHaveBeenCalled() }) it.skipIf(process.platform === 'win32')( @@ -88,11 +97,16 @@ describe('orca cli worktree awareness', () => { expect(serveOrcaAppMock.mock.lastCall?.[0]).not.toHaveProperty('supervise') serveOrcaAppMock.mockClear() + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) await main( ['serve', '--supervise', '--recipe-json', '--project-root', '/workspace/repo'], '/tmp/repo' ) expect(serveOrcaAppMock).not.toHaveBeenCalled() + expect(error).toHaveBeenCalledExactlyOnceWith( + '--recipe-json starts a detached server and returns; it cannot be combined with --supervise.' + ) + expect(process.exitCode).toBe(1) } ) @@ -114,7 +128,7 @@ describe('orca cli worktree awareness', () => { '/tmp/repo' ) - expect(serveOrcaAppMock).toHaveBeenCalledWith({ + expect(serveOrcaAppMock).toHaveBeenCalledExactlyOnceWith({ json: true, port: null, pairingAddress: '100.64.1.20', @@ -141,7 +155,7 @@ describe('orca cli worktree awareness', () => { '/tmp/repo' ) - expect(serveOrcaAppMock).toHaveBeenCalledWith({ + expect(serveOrcaAppMock).toHaveBeenCalledExactlyOnceWith({ json: false, port: null, pairingAddress: 'wss://sandbox.example.com', @@ -153,86 +167,46 @@ describe('orca cli worktree awareness', () => { }) }) - it('rejects recipe JSON output without a project root', async () => { + it.each([ + { + name: 'recipe JSON output without a project root', + args: ['serve', '--recipe-json'], + message: 'Recipe JSON output requires --project-root.' + }, + { + name: 'recipe JSON output with mobile pairing', + args: ['serve', '--recipe-json', '--project-root', '/workspace/repo', '--mobile-pairing'], + message: 'Recipe JSON output requires runtime pairing; remove --mobile-pairing.' + }, + { + name: 'contradictory pairing flags', + args: ['serve', '--mobile-pairing', '--no-pairing'], + message: 'Use either --mobile-pairing or --no-pairing, not both.' + }, + { + name: 'an invalid port', + args: ['serve', '--port', 'not-a-port'], + message: 'Invalid --port value: not-a-port' + }, + { + name: 'a valueless port', + args: ['serve', '--port'], + message: 'Missing value for --port.' + } + ])('rejects $name before launching the app', async ({ args, message }) => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode + vi.spyOn(console, 'error').mockImplementation(() => {}) - await main(['serve', '--recipe-json'], '/tmp/repo') + await main([...args, '--json'], '/tmp/repo') expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Recipe JSON output requires --project-root.' - ) + expect(logSpy).toHaveBeenCalledTimes(1) + expect(JSON.parse(logSpy.mock.calls[0][0])).toEqual({ + id: 'local', + ok: false, + error: { code: 'invalid_argument', message }, + _meta: { runtimeId: null } + }) expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('rejects recipe JSON output with mobile pairing', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main( - ['serve', '--recipe-json', '--project-root', '/workspace/repo', '--mobile-pairing'], - '/tmp/repo' - ) - - expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Recipe JSON output requires runtime pairing; remove --mobile-pairing.' - ) - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('rejects contradictory serve pairing flags', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main(['serve', '--mobile-pairing', '--no-pairing', '--json'], '/tmp/repo') - - expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Use either --mobile-pairing or --no-pairing, not both.' - ) - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('rejects invalid serve ports before launching the app', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main(['serve', '--port', 'not-a-port', '--json'], '/tmp/repo') - - expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Invalid --port value: not-a-port' - ) - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('rejects value-less serve ports before launching the app', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main(['serve', '--port', '--json'], '/tmp/repo') - - expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Missing value for --port.' - ) - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode }) }) diff --git a/src/cli/index-serve-restore.test.ts b/src/cli/index-serve-restore.test.ts index 89cbe79d81e..5a77c8eeeeb 100644 --- a/src/cli/index-serve-restore.test.ts +++ b/src/cli/index-serve-restore.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' const { callMock, @@ -53,6 +53,10 @@ describe('orca serve restore resume', () => { spawnMock }) + beforeEach(() => { + runtimeClientConstructorMock.mockClear() + }) + it('asks the local Orca to resume and never starts a server', async () => { process.env.ORCA_ENVIRONMENT = 'paired-env' callMock.mockResolvedValue({ @@ -72,10 +76,13 @@ describe('orca serve restore resume', () => { await main(['serve', 'restore', 'resume'], '/tmp/repo') expect(serveOrcaAppMock).not.toHaveBeenCalled() - expect(callMock).toHaveBeenCalledWith('runtime.automaticWork.resume') - expect(runtimeClientConstructorMock).toHaveBeenLastCalledWith(null, null) - expect(logSpy.mock.calls[0]?.[0]).toContain('Automatic restores resumed') - expect(logSpy.mock.calls[0]?.[0]).toContain('stopped unexpectedly') + expect(callMock.mock.calls).toEqual([['runtime.automaticWork.resume']]) + expect(runtimeClientConstructorMock.mock.calls).toEqual([[null, null]]) + expect(logSpy.mock.calls).toEqual([ + [ + 'Automatic restores resumed. They were paused at 1970-01-01T00:00:00.000Z because the terminal host stopped unexpectedly.' + ] + ]) }) it('says so when nothing was paused, and when the host has no gate', async () => { @@ -95,8 +102,14 @@ describe('orca serve restore resume', () => { }) await main(['serve', 'restore', 'resume'], '/tmp/repo') - expect(logSpy.mock.calls[0]?.[0]).toContain('were not paused') - expect(logSpy.mock.calls[1]?.[0]).toContain('no automatic-restore gate') + expect(callMock.mock.calls).toEqual([ + ['runtime.automaticWork.resume'], + ['runtime.automaticWork.resume'] + ]) + expect(logSpy.mock.calls).toEqual([ + ['Automatic restores were not paused; nothing to resume.'], + ['This host has no automatic-restore gate to resume (its terminal host predates it).'] + ]) }) it('explains a terminal host that predates the command', async () => { @@ -108,6 +121,11 @@ describe('orca serve restore resume', () => { _meta: { runtimeId: 'runtime-1' } }) await main(['serve', 'restore', 'resume'], '/tmp/repo') - expect(logSpy.mock.calls[0]?.[0]).toContain('predates this command') + expect(callMock.mock.calls).toEqual([['runtime.automaticWork.resume']]) + expect(logSpy.mock.calls).toEqual([ + [ + 'The terminal host running now predates this command, so it cannot clear its pause. Run this again after the terminal host restarts. Until then, opening a sleeping terminal still resumes it.' + ] + ]) }) }) diff --git a/src/cli/index-terminal-commands.test.ts b/src/cli/index-terminal-commands.test.ts index 09bc0e8d821..73464747632 100644 --- a/src/cli/index-terminal-commands.test.ts +++ b/src/cli/index-terminal-commands.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { callMock, @@ -54,14 +54,114 @@ describe('orca cli worktree awareness', () => { spawnMock }) - it('passes explicit focus through terminal.create', async () => { + const originalExitCode = process.exitCode + beforeEach(() => { + process.exitCode = undefined + runtimeClientConstructorMock.mockClear() + }) + afterEach(() => { + process.exitCode = originalExitCode + }) + + it.each([ + [ + 'passes explicit focus through terminal.create', + { + title: 'RUNNER', + command: undefined, + flags: ['--focus'], + expected: { focus: true, presentation: 'focused' } + } + ], + [ + 'keeps interactive Codex startup commands backgrounded unless focus is explicit', + { + title: 'Codex', + command: 'codex', + flags: [], + expected: { focus: false, rendererBacked: true, activate: false } + } + ], + [ + 'keeps explicit focus semantics when forcing Codex through the renderer path', + { + title: 'Codex', + command: 'codex', + flags: ['--focus'], + expected: { focus: true, presentation: 'focused', rendererBacked: true, activate: true } + } + ], + [ + 'does not force the visible terminal path for explicit Codex exec commands', + { + title: 'Codex exec', + command: 'codex exec summarize', + flags: [], + expected: { focus: false } + } + ], + [ + 'does not force the visible terminal path for Codex exec commands after global options', + { + title: 'Codex exec', + command: 'codex -m gpt-5 --sandbox workspace-write exec summarize', + flags: [], + expected: { focus: false } + } + ], + [ + 'does not force the visible terminal path for Codex review commands after long options', + { + title: 'Codex review', + command: 'codex --model=gpt-5 --sandbox=workspace-write review', + flags: [], + expected: { focus: false } + } + ], + [ + 'does not force the visible terminal path for Codex help commands', + { + title: 'Codex help', + command: 'codex --help', + flags: [], + expected: { focus: false } + } + ], + [ + 'keeps Codex prompts after global options backgrounded unless focus is explicit', + { + title: 'Codex prompt', + command: 'codex -m gpt-5 "fix the flaky test"', + flags: [], + expected: { focus: false, rendererBacked: true, activate: false } + } + ], + [ + 'keeps interactive Claude startup commands backgrounded unless focus is explicit', + { + title: 'Claude', + command: 'claude', + flags: [], + expected: { focus: false, rendererBacked: true, activate: false } + } + ], + [ + 'keeps Claude print commands on the background terminal path', + { + title: 'Claude print', + command: 'claude -p "summarize"', + flags: [], + expected: { focus: false } + } + ] + ] as const)('%s', async (_name, { title, command, flags, expected }) => { queueFixtures( callMock, okFixture('req_terminal_create', { terminal: { handle: 'term_1', worktreeId: 'repo-1::/tmp/repo/feature', - title: 'RUNNER' + title } }) ) @@ -74,20 +174,18 @@ describe('orca cli worktree awareness', () => { '--worktree', 'path:/tmp/repo/feature', '--title', - 'RUNNER', - '--focus', + title, + ...(command === undefined ? [] : ['--command', command]), + ...flags, '--json' ], '/tmp/repo' ) - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: undefined, - title: 'RUNNER', - focus: true, - presentation: 'focused' - }) + expect(callMock.mock.calls).toEqual([ + ['terminal.create', { worktree: 'path:/tmp/repo/feature', command, title, ...expected }] + ]) + expect(process.exitCode).toBeUndefined() }) it('prints terminal.read fallback screen lines in json mode', async () => { @@ -126,340 +224,6 @@ describe('orca cli worktree awareness', () => { ]) }) - it('keeps interactive Codex startup commands backgrounded unless focus is explicit', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex', - '--command', - 'codex', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex', - title: 'Codex', - focus: false, - rendererBacked: true, - activate: false - }) - }) - - it('keeps explicit focus semantics when forcing Codex through the renderer path', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex', - '--command', - 'codex', - '--focus', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex', - title: 'Codex', - focus: true, - presentation: 'focused', - rendererBacked: true, - activate: true - }) - }) - - it('does not force the visible terminal path for explicit Codex exec commands', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex exec' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex exec', - '--command', - 'codex exec summarize', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex exec summarize', - title: 'Codex exec', - focus: false - }) - }) - - it('does not force the visible terminal path for Codex exec commands after global options', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex exec' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex exec', - '--command', - 'codex -m gpt-5 --sandbox workspace-write exec summarize', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex -m gpt-5 --sandbox workspace-write exec summarize', - title: 'Codex exec', - focus: false - }) - }) - - it('does not force the visible terminal path for Codex review commands after long options', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex review' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex review', - '--command', - 'codex --model=gpt-5 --sandbox=workspace-write review', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex --model=gpt-5 --sandbox=workspace-write review', - title: 'Codex review', - focus: false - }) - }) - - it('does not force the visible terminal path for Codex help commands', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex help' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex help', - '--command', - 'codex --help', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex --help', - title: 'Codex help', - focus: false - }) - }) - - it('keeps Codex prompts after global options backgrounded unless focus is explicit', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Codex prompt' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Codex prompt', - '--command', - 'codex -m gpt-5 "fix the flaky test"', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'codex -m gpt-5 "fix the flaky test"', - title: 'Codex prompt', - focus: false, - rendererBacked: true, - activate: false - }) - }) - - it('keeps interactive Claude startup commands backgrounded unless focus is explicit', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Claude' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Claude', - '--command', - 'claude', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'claude', - title: 'Claude', - focus: false, - rendererBacked: true, - activate: false - }) - }) - - it('keeps Claude print commands on the background terminal path', async () => { - queueFixtures( - callMock, - okFixture('req_terminal_create', { - terminal: { - handle: 'term_1', - worktreeId: 'repo-1::/tmp/repo/feature', - title: 'Claude print' - } - }) - ) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - [ - 'terminal', - 'create', - '--worktree', - 'path:/tmp/repo/feature', - '--title', - 'Claude print', - '--command', - 'claude -p "summarize"', - '--json' - ], - '/tmp/repo' - ) - - expect(callMock).toHaveBeenCalledWith('terminal.create', { - worktree: 'path:/tmp/repo/feature', - command: 'claude -p "summarize"', - title: 'Claude print', - focus: false - }) - }) - it('uses the resolved enclosing worktree for terminal consumers', async () => { queueFixtures( callMock, @@ -532,7 +296,6 @@ describe('orca cli worktree awareness', () => { it('rejects implicit remote terminal create instead of resolving from client cwd', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode await main( ['terminal', 'create', '--pairing-code', 'remote-runtime', '--json'], @@ -540,12 +303,17 @@ describe('orca cli worktree awareness', () => { ) expect(callMock).not.toHaveBeenCalled() - expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( - 'Remote terminal create requires --worktree' - ) + expect(logSpy).toHaveBeenCalledTimes(1) + expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({ + ok: false, + error: { + code: 'invalid_argument', + message: + 'Remote terminal create requires --worktree because the client cwd cannot identify a server worktree.' + } + }) + expect(errSpy).not.toHaveBeenCalled() expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode }) it('sends explicit remote terminal create worktree selectors unchanged', async () => { @@ -602,7 +370,6 @@ describe('orca cli worktree awareness', () => { } }) const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const priorExitCode = process.exitCode await main(['terminal', 'wait', '--terminal', 'term_worker', '--for', 'tui-idle'], '/tmp/repo') @@ -619,8 +386,6 @@ describe('orca cli worktree awareness', () => { ) expect(logSpy.mock.calls.flat().join('\n')).toContain('blockedReason: codex-cwd-prompt') expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode }) it('does not force remote Codex terminal creates through a local renderer path', async () => { diff --git a/src/cli/index-terminal-list-host-scope.test.ts b/src/cli/index-terminal-list-host-scope.test.ts index bbaa3249c3f..dd2d4078e28 100644 --- a/src/cli/index-terminal-list-host-scope.test.ts +++ b/src/cli/index-terminal-list-host-scope.test.ts @@ -84,6 +84,10 @@ describe('orca terminal list host scope', () => { await main(['terminal', 'list', '--json'], '/tmp/repo') + expect(callMock.mock.calls).toEqual([ + ['terminal.list', { worktree: undefined, limit: undefined, includeVisualLayouts: false }] + ]) + const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0])) expect(printed.result.terminals[0].executionHostId).toBe('ssh:box-1') expect(printed.result.hostScope).toEqual({ @@ -108,10 +112,14 @@ describe('orca terminal list host scope', () => { await main(['terminal', 'list'], '/tmp/repo') + expect(callMock.mock.calls).toEqual([ + ['terminal.list', { worktree: undefined, limit: undefined, includeVisualLayouts: true }] + ]) + const printed = String(logSpy.mock.calls[0]?.[0]) - expect(printed).toContain('host=ssh:box-1') - expect(printed).toContain('scope: ssh:box-1') - expect(printed).toContain('not covered: local') + expect(printed).toBe( + 'term_remote worker connected host=ssh:box-1 /remote/wt\npreview: \n\nscope: ssh:box-1 — not covered: local (reached through this runtime; no flag selects it)' + ) }) it('does not claim a local scope when the host never reported one', async () => { @@ -123,8 +131,13 @@ describe('orca terminal list host scope', () => { await main(['terminal', 'list'], '/tmp/repo') + expect(callMock.mock.calls).toEqual([ + ['terminal.list', { worktree: undefined, limit: undefined, includeVisualLayouts: true }] + ]) + const printed = String(logSpy.mock.calls[0]?.[0]) - expect(printed).toContain('scope: unverifiable') - expect(printed).not.toContain('scope: local') + expect(printed).toBe( + 'No terminals listed.\nscope: unverifiable — this host does not report which hosts it lists' + ) }) }) diff --git a/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.test.ts b/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.test.ts index f86d6149e73..8730fa017a0 100644 --- a/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.test.ts +++ b/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.test.ts @@ -35,7 +35,7 @@ describe('dashboard visible-use telemetry', () => { let visibility: DocumentVisibilityState beforeEach(() => { - vi.useFakeTimers() + vi.useFakeTimers({ now: new Date('2026-10-22T12:00:00Z') }) track.mockReset().mockResolvedValue(true) getConsentState.mockReset().mockResolvedValue({ effective: 'enabled' }) useAppStore.setState({