1010 * FF on → Go binary's os-flows extension resolves dep graphs via the plugin
1111 * orchestrator and posts to /rest/orgs/:orgId/tests.
1212 *
13- * Starter corpus. Expand per resolveDepgraphs-rollout-plan.md.
13+ * Phase 1 of the rollout: breadth across every ecosystem that resolves through
14+ * the legacy-CLI fallback. The native resolver flags (cargo, pnpm, gradle, …)
15+ * stay OFF here, so BOTH runs resolve via the same legacy CLI and only the
16+ * submission endpoint differs — that endpoint/serialization parity is what this
17+ * phase proves. Native-resolver parity (gradle, pnpm) and exit-1/error parity
18+ * arrive in later phases.
19+ *
20+ * Fixtures that need an external build tool to resolve (go, sbt, swift, …) are
21+ * tagged with `requiresCmd` and skip when that tool is absent rather than
22+ * failing "inconclusive". Pure lockfile ecosystems (yarn, ruby, composer,
23+ * poetry, …) need no tool beyond what the harness already has.
24+ *
25+ * Note: pip/pipenv and hex/mix are deliberately excluded — their snyk plugins
26+ * resolve by introspecting an *installed* environment (site-packages / fetched
27+ * mix deps), so they need an install step and can't resolve from a clean
28+ * checkout. Python is covered via Poetry, which resolves offline from its lock.
1429 */
1530
31+ import { execFileSync } from 'child_process' ;
1632import { fakeServer } from '../../../acceptance/fake-server' ;
1733import { createProjectFromWorkspace } from '../../util/createProject' ;
1834import { getServerPort } from '../../util/getServerPort' ;
1935import { assertEquivalent , runBothFlows } from './equivalenceHelpers' ;
2036
2137jest . setTimeout ( 1000 * 60 * 3 ) ;
2238
39+ /** True when `cmd` resolves on PATH — used to skip fixtures whose toolchain is absent. */
40+ function commandAvailable ( cmd : string ) : boolean {
41+ try {
42+ execFileSync ( process . platform === 'win32' ? 'where' : 'which' , [ cmd ] , {
43+ stdio : 'ignore' ,
44+ } ) ;
45+ return true ;
46+ } catch {
47+ return false ;
48+ }
49+ }
50+
2351describe ( 'snyk test — unified test API equivalence (FF off vs on)' , ( ) => {
2452 let server ;
2553 let env : Record < string , string > ;
@@ -47,9 +75,15 @@ describe('snyk test — unified test API equivalence (FF off vs on)', () => {
4775 name : string ;
4876 args : string ;
4977 expectNoSubmissions ?: boolean ;
78+ /** Expected to be rejected by both flows (e.g. out-of-sync lockfile). */
79+ expectError ?: boolean ;
80+ /** Binary that must be on PATH for the legacy CLI to resolve this fixture.
81+ * Omitted for lockfile-only ecosystems that need no external build tool. */
82+ requiresCmd ?: string ;
5083 } ;
5184
5285 const fixtures : Fixture [ ] = [
86+ // --- Starter corpus (unchanged) ---
5387 { name : 'npm-package' , args : 'test' } ,
5488 { name : 'maven-app' , args : 'test' } ,
5589 { name : 'mono-repo-project' , args : 'test --all-projects' } ,
@@ -58,24 +92,81 @@ describe('snyk test — unified test API equivalence (FF off vs on)', () => {
5892 args : 'test' ,
5993 expectNoSubmissions : true ,
6094 } ,
95+
96+ // --- Phase 1: breadth via the legacy fallback ---
97+ // JavaScript (yarn) — resolved from yarn.lock; no tool beyond node.
98+ { name : 'yarn-package' , args : 'test' } ,
99+ { name : 'yarn-workspaces' , args : 'test --all-projects' } ,
100+ // Ruby — resolved from Gemfile.lock.
101+ { name : 'ruby-app' , args : 'test' } ,
102+ // PHP (Composer) — resolved from composer.lock.
103+ { name : 'composer-app' , args : 'test' } ,
104+ // CocoaPods — resolved from Podfile.lock.
105+ { name : 'cocoapods-app' , args : 'test' } ,
106+ // Swift (SwiftPM) — the plugin shells out to `swift package show-dependencies`,
107+ // so it needs the swift toolchain (gated) and the deps on disk. The fixture
108+ // depends on a sibling package by path, keeping resolution network-free.
109+ { name : 'swift-local-dep' , args : 'test' , requiresCmd : 'swift' } ,
110+ // Go modules — needs the go toolchain.
111+ { name : 'golang-gomodules' , args : 'test' , requiresCmd : 'go' } ,
112+ // Python (Poetry) — resolved from poetry.lock; no tool beyond node.
113+ // (pip/pipenv are intentionally NOT used here: their snyk plugins resolve by
114+ // introspecting *installed* site-packages, so they require a pip/pipenv
115+ // install step and can't resolve from a clean checkout the way a lockfile can.)
116+ { name : 'poetry-app' , args : 'test' } ,
117+ // .NET (NuGet) — resolved from project.assets.json; no tool beyond node.
118+ { name : 'nuget-app-2' , args : 'test' } ,
119+ // Scala (sbt) — needs sbt.
120+ { name : 'sbt-app' , args : 'test' , requiresCmd : 'sbt' } ,
121+
122+ // --- Phase 2: option parity (same flag applied to BOTH flows) ---
123+ // Each option is passed to legacy and unified alike; the resolved graph and
124+ // project metadata must still match. Clean projects, so exit code stays 0.
125+ { name : 'npm-package' , args : 'test --dev' } ,
126+ { name : 'npm-package' , args : 'test --file=package.json' } ,
127+ {
128+ name : 'npm-package-pruneable' ,
129+ args : 'test --prune-repeated-subdependencies' ,
130+ } ,
131+
132+ // --- Phase 2: partial-failure parity under --all-projects ---
133+ // monorepo-bad-project mixes resolvable and unresolvable projects. This is
134+ // the exact scenario the os-flows change targeted ("tolerate per-project
135+ // resolution failures in the --all-projects orchestrator path"): both flows
136+ // should resolve the good projects, skip the bad one, and agree on the
137+ // submitted-project count and exit code.
138+ { name : 'monorepo-bad-project' , args : 'test --all-projects' } ,
139+
140+ // --- Phase 2: error parity (both flows must REJECT the bad input) ---
141+ // Exit-code values legitimately differ (TS CLI 3 vs os-flows 2), so these
142+ // assert the tolerance invariant "both fail" rather than equal codes.
143+ { name : 'npm-out-of-sync' , args : 'test' , expectError : true } ,
144+ { name : 'yarn-out-of-sync' , args : 'test' , expectError : true } ,
61145 ] ;
62146
63147 describe . each ( fixtures ) (
64148 'fixture: $name ($args)' ,
65- ( { name, args, expectNoSubmissions } ) => {
66- test ( 'FF off vs on produces equivalent dep graphs and exit code' , async ( ) => {
67- const project = await createProjectFromWorkspace ( name ) ;
149+ ( { name, args, expectNoSubmissions, expectError, requiresCmd } ) => {
150+ const runnable = ! requiresCmd || commandAvailable ( requiresCmd ) ;
151+ ( runnable ? test : test . skip ) (
152+ 'FF off vs on produces equivalent dep graphs and exit code' ,
153+ async ( ) => {
154+ const project = await createProjectFromWorkspace ( name ) ;
68155
69- const result = await runBothFlows ( project . path ( ) , args , server , env ) ;
70- const diff = assertEquivalent ( result , { expectNoSubmissions } ) ;
156+ const result = await runBothFlows ( project . path ( ) , args , server , env ) ;
157+ const diff = assertEquivalent ( result , {
158+ expectNoSubmissions,
159+ expectError,
160+ } ) ;
71161
72- if ( ! diff . ok ) {
73- throw new Error (
74- `Equivalence failed for ${ name } (${ args } ): ${ diff . reason } \n` +
75- `detail=${ JSON . stringify ( diff . detail , null , 2 ) } ` ,
76- ) ;
77- }
78- } ) ;
162+ if ( ! diff . ok ) {
163+ throw new Error (
164+ `Equivalence failed for ${ name } (${ args } ): ${ diff . reason } \n` +
165+ `detail=${ JSON . stringify ( diff . detail , null , 2 ) } ` ,
166+ ) ;
167+ }
168+ } ,
169+ ) ;
79170 } ,
80171 ) ;
81172} ) ;
0 commit comments