diff --git a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go index 96f0b446ed..488ea7056d 100644 --- a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go +++ b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go @@ -2,6 +2,7 @@ package behavior import ( "github.com/snyk/error-catalog-golang-public/aibom" + "github.com/snyk/error-catalog-golang-public/cli" "github.com/snyk/error-catalog-golang-public/code" "github.com/snyk/error-catalog-golang-public/snyk" "github.com/snyk/error-catalog-golang-public/snyk_errors" @@ -14,9 +15,10 @@ var MapErrorCatalogToExitCode func(err *snyk_errors.Error, defaultValue int) int // mapErrorToExitCode maps error catalog errors to exit codes. Please extend the switch statement if new error codes need to be mapped. func mapErrorToExitCode(err *snyk_errors.Error, defaultValue int) int { var errorCatalogToExitCodeMap = map[string]int{ - code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, + code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + cli.NewNoSupportedFilesFoundError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, // Add new mappings here } diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index bac899a594..1758842aee 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -31,6 +31,7 @@ import ( cli_errors "github.com/snyk/cli/cliv2/internal/errors" + "github.com/snyk/cli/cliv2/cmd/cliv2/behavior" "github.com/snyk/cli/cliv2/internal/constants" debug_utils "github.com/snyk/cli/cliv2/internal/debug" "github.com/snyk/cli/cliv2/internal/embedded" @@ -566,6 +567,7 @@ func DeriveExitCode(err error) int { if err != nil { var exitError *exec.ExitError var errorWithExitCode *cli_errors.ErrorWithExitCode + var snykErr snyk_errors.Error if errors.As(err, &exitError) { returnCode = exitError.ExitCode() @@ -575,6 +577,8 @@ func DeriveExitCode(err error) int { } } else if errors.As(err, &errorWithExitCode) { returnCode = errorWithExitCode.ExitCode + } else if errors.As(err, &snykErr) { + returnCode = behavior.MapErrorCatalogToExitCode(&snykErr, constants.SNYK_EXIT_CODE_ERROR) } else { // got an error but it's not an ExitError returnCode = constants.SNYK_EXIT_CODE_ERROR @@ -583,6 +587,7 @@ func DeriveExitCode(err error) int { return returnCode } + func (e EnvironmentWarning) Error() string { return e.message } diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index 847f66f2da..43d6412996 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/snyk/error-catalog-golang-public/code" "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" @@ -604,6 +605,9 @@ func TestDeriveExitCode(t *testing.T) { {name: "no error", err: nil, expected: constants.SNYK_EXIT_CODE_OK}, {name: "error with exit code", err: &cli_errors.ErrorWithExitCode{ExitCode: 42}, expected: 42}, {name: "other error", err: errors.New("some other error"), expected: constants.SNYK_EXIT_CODE_ERROR}, + {name: "snyk_errors.Error with mapped code", err: snyk_errors.Error{ErrorCode: code.NewUnsupportedProjectError("").ErrorCode}, expected: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS}, + {name: "snyk_errors.Error with unmapped code", err: snyk_errors.Error{ErrorCode: "SNYK-UNKNOWN-9999"}, expected: constants.SNYK_EXIT_CODE_ERROR}, + {name: "wrapped snyk_errors.Error", err: fmt.Errorf("wrap: %w", snyk_errors.Error{ErrorCode: code.NewUnsupportedProjectError("").ErrorCode}), expected: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS}, } for _, tc := range tests { diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 70e6fbbc35..effdcf9659 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -219,8 +219,7 @@ export default async function monitor(...args0: MethodArgs): Promise { const verboseEnabled = args.includes('-Dverbose') || args.includes('-Dverbose=true') || - !!options['print-graph'] || - !!options['print-output-jsonl-with-errors']; + !!options['print-graph']; if (verboseEnabled) { enableMavenDverboseExhaustiveDeps = (await hasFeatureFlag( MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, diff --git a/src/lib/ecosystems/test.ts b/src/lib/ecosystems/test.ts index 3cff71bda6..2029e9cc4a 100644 --- a/src/lib/ecosystems/test.ts +++ b/src/lib/ecosystems/test.ts @@ -13,7 +13,8 @@ import { assembleQueryString, printDepGraph, printDepGraphJsonl, - shouldPrintDepGraph, + shouldPrintGraph, + isJsonl, } from '../snyk-test/common'; import { getAuthHeader } from '../api-token'; import { resolveAndTestFacts } from './resolve-test-facts'; @@ -57,7 +58,7 @@ export async function testEcosystem( if ( isUnmanagedEcosystem(ecosystem) && - (shouldPrintDepGraph(options) || options['print-output-jsonl-with-errors']) + shouldPrintGraph(options) ) { const [target] = paths; return printUnmanagedDepGraph(results, target, process.stdout, options); @@ -108,7 +109,7 @@ export async function printUnmanagedDepGraph( const [result] = await getUnmanagedDepGraph(results); const depGraph = convertDepGraph(result); - if (options['print-output-jsonl-with-errors']) { + if (isJsonl(options)) { await printDepGraphJsonl( depGraph, target, diff --git a/src/lib/monitor/index.ts b/src/lib/monitor/index.ts index f613b61cc8..80c80a3d12 100644 --- a/src/lib/monitor/index.ts +++ b/src/lib/monitor/index.ts @@ -317,7 +317,8 @@ export async function monitorDepGraph( analytics.add('targetBranch', target.branch); } - const pruneIsRequired = options.pruneRepeatedSubdependencies; + const pruneIsRequired = + options.pruneRepeatedSubdependencies || !!options['prune']; depGraph = await pruneGraph(depGraph, packageManager, pruneIsRequired); let callGraphPayload; diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index b977f1783e..ab2afcd294 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -17,6 +17,7 @@ import { detectPackageManagerFromFile, } from '../detect'; import * as analytics from '../analytics'; +import { shouldEmbedErrors } from '../snyk-test/common'; import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; @@ -65,7 +66,7 @@ export async function getDepsFromPlugin( ); if (targetFiles.length === 0) { const error = NoSupportedManifestsFoundError([root]); - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect' }, scannedProjects: [], @@ -138,7 +139,7 @@ export async function getDepsFromPlugin( } if (!options.docker && !(options.file || options.packageManager)) { const error = NoSupportedManifestsFoundError([root]); - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect' }, scannedProjects: [], @@ -149,6 +150,7 @@ export async function getDepsFromPlugin( errMessage: error.userMessage, }, ], + } as MultiProjectResultCustom; } throw error; @@ -158,7 +160,7 @@ export async function getDepsFromPlugin( try { inspectRes = await getSinglePluginResult(root, options, '', featureFlags); } catch (error) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { const errMessage = error?.message ?? 'Something went wrong getting dependencies'; debug( diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index c3f124b117..4bb0665600 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -18,6 +18,7 @@ import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-m import { PluginMetadata } from '@snyk/cli-interface/legacy/plugin'; import { CallGraph } from '@snyk/cli-interface/legacy/common'; import { errorMessageWithRetry, FailedToRunTestError } from '../errors'; +import { shouldEmbedErrors } from '../snyk-test/common'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; import { processNpmWorkspaces } from './nodejs-plugin/npm-workspaces-parser'; import { processPnpmWorkspaces } from 'snyk-nodejs-plugin'; @@ -203,7 +204,7 @@ export async function getMultiPluginResult( } if (!allResults.length) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect', diff --git a/src/lib/snyk-test/common.ts b/src/lib/snyk-test/common.ts index 8ebbbfe7d4..020bbbd8aa 100644 --- a/src/lib/snyk-test/common.ts +++ b/src/lib/snyk-test/common.ts @@ -11,6 +11,7 @@ import { ContainerTarget, GitTarget } from '../project-metadata/types'; import { CLI, ProblemError } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from '../errors'; import { FailedProjectScanError } from '../plugins/get-multi-plugin-result'; +import * as analytics from '../analytics'; /** * Determines workspace information from the plugin name and scanned project metadata. @@ -132,8 +133,67 @@ export async function printDepGraph( }); } -export function shouldPrintDepGraph(opts: Options): boolean { - return opts['print-graph'] && !opts['print-deps']; +// PHASE 2: --jsonl will be removed once all consumers use the dep-graph router +// directly. At that point, JSONL will be the only print-graph output format. +export function isJsonl(opts: Options): boolean { + return !!opts['jsonl']; +} + +export function shouldEmbedErrors(opts: Options): boolean { + return !!opts['embed-errors']; +} + +export function shouldPrintGraph(opts: Options): boolean { + return !opts['print-deps'] && ( + !!opts['print-graph'] || + !!opts['print-effective-graph'] || + !!opts['print-effective-graph-with-errors'] || + !!opts['print-output-jsonl-with-errors'] + ); +} + +// DEPRECATION: The legacy flag mappings below exist for backward compatibility. +// Once analytics confirm no usage of the old flags, remove the legacyMappings +// table and the deprecation warnings. +export function mapLegacyGraphFlags(opts: Options): void { + // --prune implies --jsonl (pruned output is always JSONL) + if (opts['prune']) { + opts['jsonl'] = true; + opts['print-graph'] = true; + } + + // New-style --jsonl or --prune: always embed errors. + // Consumers of the new model are expected to handle embedded errors. + if (opts['jsonl']) { + opts['print-graph'] = true; + opts['embed-errors'] = true; + return; + } + + const legacyMappings: Array<{ flag: keyof Options; prune: boolean; embedErrors: boolean }> = [ + { flag: 'print-effective-graph', prune: true, embedErrors: false }, + { flag: 'print-effective-graph-with-errors', prune: true, embedErrors: true }, + { flag: 'print-output-jsonl-with-errors', prune: false, embedErrors: true }, + ]; + + for (const { flag, prune, embedErrors } of legacyMappings) { + if (opts[flag]) { + const replacement = prune ? '--print-graph --prune' : '--print-graph --jsonl'; + process.stderr.write( + `WARNING: --${flag} is deprecated. Use ${replacement} instead.\n`, + ); + analytics.add('deprecatedLegacyDepGraphFlag', flag); + opts['print-graph'] = true; + opts['jsonl'] = true; + if (prune) { + opts['prune'] = true; + } + if (embedErrors) { + opts['embed-errors'] = true; + } + return; + } + } } /** @@ -180,8 +240,8 @@ export async function printDepGraphError( return new Promise((res, rej) => { // Normalize the target file path to be relative to root, consistent with printDepGraphJsonl const normalisedTargetFile = failedProjectScanError.targetFile - ? path.relative(root, failedProjectScanError.targetFile) - : failedProjectScanError.targetFile; + ? path.relative(root, path.resolve(root, failedProjectScanError.targetFile)) + : failedProjectScanError.targetFile; const problemError = getOrCreateErrorCatalogError(failedProjectScanError); const serializedError = problemError.toJsonApi().body(); @@ -198,23 +258,6 @@ export async function printDepGraphError( }); } -/** - * Checks if either --print-effective-graph or --print-effective-graph-with-errors is set. - */ -export function shouldPrintEffectiveDepGraph(opts: Options): boolean { - return ( - !!opts['print-effective-graph'] || - shouldPrintEffectiveDepGraphWithErrors(opts) - ); -} - -/** - * shouldPrintEffectiveDepGraphWithErrors checks if the --print-effective-graph-with-errors flag is set. - * This is used to determine if the effective dep-graph with errors should be printed. - */ -export function shouldPrintEffectiveDepGraphWithErrors(opts: Options): boolean { - return !!opts['print-effective-graph-with-errors']; -} /** * getOrCreateErrorCatalogError returns a ProblemError instance for consistent error catalog usage. diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index 03c98980fb..bb4b36fcf2 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -19,7 +19,7 @@ const { DISABLE_GO_PACKAGE_URLS_IN_CLI_FEATURE_FLAG, } = require('../package-managers'); const { getOrganizationID } = require('../organization'); -const { printDepGraphError } = require('./common'); +const { printDepGraphError, mapLegacyGraphFlags, shouldEmbedErrors } = require('./common'); const debug = require('debug')('snyk-test'); async function test(root, options, callback) { @@ -42,6 +42,8 @@ async function test(root, options, callback) { } async function executeTest(root, options) { + mapLegacyGraphFlags(options); + const includeGoStandardLibraryDeps = await hasFeatureFlagOrDefault( INCLUDE_GO_STANDARD_LIBRARY_DEPS_FEATURE_FLAG, options, @@ -57,8 +59,7 @@ async function executeTest(root, options) { const verboseEnabled = args.includes('-Dverbose') || args.includes('-Dverbose=true') || - !!options['print-graph'] || - !!options['print-output-jsonl-with-errors']; + (!!options['print-graph'] && !options['prune']); if (verboseEnabled) { enableMavenDverboseExhaustiveDeps = await hasFeatureFlag( MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, @@ -117,7 +118,7 @@ async function executeTest(root, options) { featureFlags, ); } catch (error) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { await printDepGraphError( root, { diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 8c50e4b844..68ccc79a4a 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -43,9 +43,9 @@ import { printDepGraphJsonl, printDepGraphError, assembleQueryString, - shouldPrintDepGraph, - shouldPrintEffectiveDepGraph, - shouldPrintEffectiveDepGraphWithErrors, + shouldPrintGraph, + isJsonl, + shouldEmbedErrors, } from './common'; import config from '../config'; import * as analytics from '../analytics'; @@ -358,10 +358,10 @@ async function sendAndParseResults( }); } - if (ecosystem && shouldPrintDepGraph(options)) { + if (ecosystem && shouldPrintGraph(options)) { await spinner.clear(spinnerLbl)(); - if (options['print-output-jsonl-with-errors']) { - for (const { graph, targetFile, targetName } of depGraphs) { + for (const { graph, targetFile, targetName } of depGraphs) { + if (isJsonl(options)) { await printDepGraphJsonl( graph, targetFile || targetName, @@ -372,12 +372,7 @@ async function sendAndParseResults( undefined, process.stdout, ); - } - } else { - const depGraphsByTarget = new Map( - depGraphs.map(({ targetName, graph }) => [targetName, graph]), - ); - for (const [targetName, graph] of depGraphsByTarget) { + } else { await printDepGraph(graph, targetName, process.stdout); } } @@ -403,9 +398,7 @@ export async function runTest( // dependency graph artifacts for printing. if ( !options.docker && - (shouldPrintDepGraph(options) || - shouldPrintEffectiveDepGraph(options) || - options['print-output-jsonl-with-errors']) + shouldPrintGraph(options) ) { return []; } @@ -703,10 +696,7 @@ async function assembleLocalPayloads( failedResults, ); - if ( - shouldPrintEffectiveDepGraphWithErrors(options) || - options['print-output-jsonl-with-errors'] - ) { + if (shouldPrintGraph(options) && shouldEmbedErrors(options)) { for (const failed of failedResults) { await printDepGraphError(root, failed, process.stdout); } @@ -846,7 +836,7 @@ async function assembleLocalPayloads( ? (pkg as depGraphLib.DepGraph).rootPkg.name : (pkg as DepTree).name; - if (shouldPrintDepGraph(options)) { + if (shouldPrintGraph(options) && !options['prune']) { spinner.clear(spinnerLbl)(); let root: depGraphLib.DepGraph; if (scannedProject.depGraph) { @@ -859,7 +849,7 @@ async function assembleLocalPayloads( ); } - if (options['print-output-jsonl-with-errors']) { + if (isJsonl(options)) { await printDepGraphJsonl( root.toJSON(), targetFile || '', @@ -911,13 +901,14 @@ async function assembleLocalPayloads( }); } - const pruneIsRequired = options.pruneRepeatedSubdependencies; + const pruneIsRequired = + options.pruneRepeatedSubdependencies || !!options['prune']; - if (packageManager && !options['print-output-jsonl-with-errors']) { + if (packageManager && (!isJsonl(options) || options['prune'])) { depGraph = await pruneGraph(depGraph, packageManager, pruneIsRequired); } - if (shouldPrintEffectiveDepGraph(options)) { + if (shouldPrintGraph(options) && options['prune']) { spinner.clear(spinnerLbl)(); await printDepGraphJsonl( depGraph.toJSON(), diff --git a/src/lib/types.ts b/src/lib/types.ts index 948aeee47f..e5c1d922bc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -68,9 +68,13 @@ export interface Options { 'print-deps'?: boolean; 'print-tree'?: boolean; 'print-dep-paths'?: boolean; + 'print-graph'?: boolean; 'print-effective-graph'?: boolean; 'print-effective-graph-with-errors'?: boolean; 'print-output-jsonl-with-errors'?: boolean; + 'prune'?: boolean; + 'jsonl'?: boolean; + 'embed-errors'?: boolean; 'remote-repo-url'?: string; criticality?: string; scanAllUnmanaged?: boolean; diff --git a/test/jest/unit/print-graph-flag-resolution.spec.ts b/test/jest/unit/print-graph-flag-resolution.spec.ts new file mode 100644 index 0000000000..2ab2a40c44 --- /dev/null +++ b/test/jest/unit/print-graph-flag-resolution.spec.ts @@ -0,0 +1,126 @@ +import { + mapLegacyGraphFlags, + shouldPrintGraph, + isJsonl, + shouldEmbedErrors, +} from '../../../src/lib/snyk-test/common'; +import { Options, TestOptions } from '../../../src/lib/types'; + +describe('print-graph flag resolution', () => { + let stderrSpy: jest.SpyInstance; + + const baseOptions: Options & TestOptions = { + path: '', + showVulnPaths: 'some', + }; + + beforeEach(() => { + stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); + }); + + afterEach(() => { + stderrSpy.mockRestore(); + }); + + it('bare --print-graph resolves to plaintext (no jsonl)', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(false); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(opts['prune']).toBeFalsy(); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-graph --jsonl resolves to complete JSONL (no pruning)', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, jsonl: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['prune']).toBeFalsy(); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-graph --prune resolves to pruned JSONL', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, prune: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--prune alone implies --print-graph and --jsonl', () => { + const opts: Options & TestOptions = { ...baseOptions, prune: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['print-graph']).toBe(true); + }); + + it('--print-effective-graph maps to pruned JSONL but throws on errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-effective-graph is deprecated'), + ); + }); + + it('--print-effective-graph-with-errors maps to pruned JSONL with embedded errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph-with-errors': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-effective-graph-with-errors is deprecated'), + ); + }); + + it('--print-output-jsonl-with-errors maps to unpruned JSONL with embedded errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-output-jsonl-with-errors': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBeFalsy(); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-output-jsonl-with-errors is deprecated'), + ); + }); + + it('no print flags resolves to no graph printing', () => { + const opts: Options & TestOptions = { ...baseOptions }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(false); + expect(isJsonl(opts)).toBe(false); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-deps alongside --print-graph suppresses graph printing', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, 'print-deps': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(false); + }); +});