diff --git a/src/backends/adapter.ts b/src/backends/adapter.ts index 4adea58a..0d9feec4 100644 --- a/src/backends/adapter.ts +++ b/src/backends/adapter.ts @@ -14,6 +14,7 @@ import { createAgentLogger } from '../agents/utils/logging.js'; import { CUSTOM_MODELS } from '../config/customModels.js'; import { loadPartials } from '../db/repositories/partialsRepository.js'; import { withGitHubToken } from '../github/client.js'; +import { captureException } from '../sentry.js'; import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../types/index.js'; import { loadCascadeEnv, unloadCascadeEnv } from '../utils/cascadeEnv.js'; import { createFileLogger } from '../utils/fileLogger.js'; @@ -279,6 +280,10 @@ export async function executeWithBackend( backend: backend.name, error: String(err), }); + captureException(err, { + tags: { source: 'backend_execution', backend: backend.name, agent: identifier }, + extra: { runId, durationMs: Date.now() - startTime }, + }); let logBuffer: Buffer | undefined; try { diff --git a/src/cli/dashboard/_shared/base.ts b/src/cli/dashboard/_shared/base.ts index d2f3f1b8..c0553b77 100644 --- a/src/cli/dashboard/_shared/base.ts +++ b/src/cli/dashboard/_shared/base.ts @@ -4,6 +4,26 @@ import { type DashboardClient, createDashboardClient } from './client.js'; import { type CliConfig, loadConfig } from './config.js'; import { printDetail, printTable } from './format.js'; +export function extractBaseFlags(argv: string[]): { server?: string; org?: string } | undefined { + let server: string | undefined; + let org: string | undefined; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--') break; + if (arg === '--server' && i + 1 < argv.length) { + server = argv[++i]; + } else if (arg.startsWith('--server=')) { + server = arg.slice('--server='.length); + } else if (arg === '--org' && i + 1 < argv.length) { + org = argv[++i]; + } else if (arg.startsWith('--org=')) { + org = arg.slice('--org='.length); + } + } + if (!server && !org) return undefined; + return { server, org }; +} + export abstract class DashboardCommand extends Command { static override baseFlags = { json: Flags.boolean({ description: 'Output as JSON', default: false }), @@ -41,9 +61,8 @@ export abstract class DashboardCommand extends Command { return this._client; } - private parseBaseFlags(): { server?: string; json?: boolean; org?: string } | undefined { - // Base flags are parsed in run() — this is a fallback for the getter - return undefined; + private parseBaseFlags(): { server?: string; org?: string } | undefined { + return extractBaseFlags(this.argv); } protected outputJson(data: unknown): void { diff --git a/tests/unit/backends/adapter.test.ts b/tests/unit/backends/adapter.test.ts index a8dfa645..3fabd5bf 100644 --- a/tests/unit/backends/adapter.test.ts +++ b/tests/unit/backends/adapter.test.ts @@ -65,6 +65,11 @@ vi.mock('../../../src/backends/agent-profiles.js', () => ({ getAgentProfile: vi.fn(), })); +const mockCaptureException = vi.fn(); +vi.mock('../../../src/sentry.js', () => ({ + captureException: (...args: unknown[]) => mockCaptureException(...args), +})); + vi.mock('../../../src/agents/prompts/index.js', () => ({})); vi.mock('../../../src/agents/shared/promptContext.js', () => ({ @@ -277,6 +282,28 @@ describe('executeWithBackend', () => { expect(result.error).toContain('Backend crashed'); }); + it('reports backend errors to Sentry via captureException', async () => { + setupMocks(); + const backend = makeMockBackend(); + const error = new Error('HttpError: Not Found'); + vi.mocked(backend.execute).mockRejectedValue(error); + const input = makeInput(); + + await executeWithBackend(backend, 'review', input); + + expect(mockCaptureException).toHaveBeenCalledWith(error, { + tags: { + source: 'backend_execution', + backend: 'test-backend', + agent: expect.stringContaining('review'), + }, + extra: { + runId: 'run-uuid-123', + durationMs: expect.any(Number), + }, + }); + }); + it('includes log buffer in result', async () => { const loggerInstance = setupMocks(); const backend = makeMockBackend(); diff --git a/tests/unit/cli/dashboard/base.test.ts b/tests/unit/cli/dashboard/base.test.ts index 6291db81..0cd0fff1 100644 --- a/tests/unit/cli/dashboard/base.test.ts +++ b/tests/unit/cli/dashboard/base.test.ts @@ -22,7 +22,7 @@ vi.mock('chalk', () => ({ }, })); -import { DashboardCommand } from '../../../../src/cli/dashboard/_shared/base.js'; +import { DashboardCommand, extractBaseFlags } from '../../../../src/cli/dashboard/_shared/base.js'; // Concrete subclass for testing class TestCommand extends DashboardCommand { @@ -46,6 +46,53 @@ class TestErrorCommand extends DashboardCommand { } } +describe('extractBaseFlags', () => { + it('returns undefined when no overrides present', () => { + expect(extractBaseFlags([])).toBeUndefined(); + expect(extractBaseFlags(['--json', 'list'])).toBeUndefined(); + }); + + it('extracts --org value', () => { + expect(extractBaseFlags(['--org', 'test-org'])).toEqual({ org: 'test-org' }); + }); + + it('extracts --server value', () => { + expect(extractBaseFlags(['--server', 'http://localhost:4000'])).toEqual({ + server: 'http://localhost:4000', + }); + }); + + it('extracts both flags together', () => { + expect(extractBaseFlags(['--org', 'my-org', '--server', 'http://x'])).toEqual({ + org: 'my-org', + server: 'http://x', + }); + }); + + it('handles --org=value equals syntax', () => { + expect(extractBaseFlags(['--org=my-org'])).toEqual({ org: 'my-org' }); + }); + + it('handles --server=value equals syntax', () => { + expect(extractBaseFlags(['--server=http://x'])).toEqual({ server: 'http://x' }); + }); + + it('ignores flag at end without value', () => { + expect(extractBaseFlags(['--org'])).toBeUndefined(); + expect(extractBaseFlags(['--server'])).toBeUndefined(); + }); + + it('stops parsing at --', () => { + expect(extractBaseFlags(['--', '--org', 'test-org'])).toBeUndefined(); + }); + + it('extracts base flags mixed with other flags', () => { + expect(extractBaseFlags(['--json', '--org', 'my-org', '--limit', '20'])).toEqual({ + org: 'my-org', + }); + }); +}); + describe('DashboardCommand', () => { beforeEach(() => { vi.clearAllMocks(); @@ -84,6 +131,50 @@ describe('DashboardCommand', () => { }); }); + describe('--org flag integration', () => { + it('passes orgId override to createDashboardClient', async () => { + const config = { serverUrl: 'http://localhost:3000', sessionToken: 'tok' }; + mockLoadConfig.mockReturnValue(config); + mockCreateDashboardClient.mockReturnValue({}); + + const cmd = new TestCommand(['--org', 'my-org'], {} as never); + await cmd.run(); + + expect(mockCreateDashboardClient).toHaveBeenCalledWith( + expect.objectContaining({ orgId: 'my-org' }), + ); + }); + + it('passes server override to createDashboardClient', async () => { + const config = { serverUrl: 'http://localhost:3000', sessionToken: 'tok' }; + mockLoadConfig.mockReturnValue(config); + mockCreateDashboardClient.mockReturnValue({}); + + const cmd = new TestCommand(['--server', 'http://other:4000'], {} as never); + await cmd.run(); + + expect(mockCreateDashboardClient).toHaveBeenCalledWith( + expect.objectContaining({ serverUrl: 'http://other:4000' }), + ); + }); + + it('passes both --org and --server overrides', async () => { + const config = { serverUrl: 'http://localhost:3000', sessionToken: 'tok' }; + mockLoadConfig.mockReturnValue(config); + mockCreateDashboardClient.mockReturnValue({}); + + const cmd = new TestCommand( + ['--org', 'my-org', '--server', 'http://other:4000'], + {} as never, + ); + await cmd.run(); + + expect(mockCreateDashboardClient).toHaveBeenCalledWith( + expect.objectContaining({ serverUrl: 'http://other:4000', orgId: 'my-org' }), + ); + }); + }); + describe('handleError', () => { it('shows login message for UNAUTHORIZED tRPC errors', async () => { mockLoadConfig.mockReturnValue({ serverUrl: 'x', sessionToken: 'y' }); diff --git a/tests/unit/cli/dashboard/client.test.ts b/tests/unit/cli/dashboard/client.test.ts index 0fdc8f47..ff558e8c 100644 --- a/tests/unit/cli/dashboard/client.test.ts +++ b/tests/unit/cli/dashboard/client.test.ts @@ -49,6 +49,21 @@ describe('createDashboardClient', () => { }); }); + it('includes x-org-context header when orgId is set', () => { + const config = { serverUrl: 'http://localhost:3000', sessionToken: 'tok', orgId: 'my-org' }; + + createDashboardClient(config); + + const linkOpts = vi.mocked(httpBatchLink).mock.calls[0][0] as { + headers: () => Record; + }; + const headers = linkOpts.headers(); + expect(headers).toEqual({ + Cookie: 'cascade_session=tok', + 'x-org-context': 'my-org', + }); + }); + it('returns the created client', () => { const config = { serverUrl: 'http://localhost:3000', sessionToken: 'tok' };