From caba73b11dead6adb9cbadcb30680d5fc3c9e8dc Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Mon, 23 Feb 2026 15:26:02 +0000 Subject: [PATCH] fix(cli,sentry): fix --org/--server flags and add Sentry capture to backend errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --org and --server CLI flags on all 40+ dashboard commands were silently ignored because parseBaseFlags() was a stub returning undefined. Extract flag values directly from this.argv via a pure extractBaseFlags() helper — zero changes to any subcommand. Also add missing captureException() call in the claude-code backend adapter's catch block, matching the pattern already used in the llmist lifecycle. Backend execution errors (e.g. 404 on PR fetch) were logged locally but never reported to Sentry. Co-Authored-By: Claude Opus 4.6 --- src/backends/adapter.ts | 5 ++ src/cli/dashboard/_shared/base.ts | 25 ++++++- tests/unit/backends/adapter.test.ts | 27 +++++++ tests/unit/cli/dashboard/base.test.ts | 93 ++++++++++++++++++++++++- tests/unit/cli/dashboard/client.test.ts | 15 ++++ 5 files changed, 161 insertions(+), 4 deletions(-) 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' };