@@ -381,7 +381,7 @@ export interface TestDeps {
381381type CommonOptions = FactoryCommonOptions ;
382382
383383interface ListOptions extends CommonOptions {
384- projectId : string ;
384+ projectId ? : string ;
385385 type ?: 'frontend' | 'backend' ;
386386 createdFrom ?: 'portal' | 'mcp' | 'cli' ;
387387 /**
@@ -444,7 +444,8 @@ export async function runList(opts: ListOptions, deps: TestDeps = {}): Promise<P
444444 // (exit 3) when the caller also lacks a configured key. Order matters
445445 // for the CLI error spec §2 — bad input is a caller bug, not an auth
446446 // gate.
447- requireProjectId ( opts . projectId ) ;
447+ const projectId = resolveProjectId ( opts . projectId , deps ) ;
448+ requireProjectId ( projectId ) ;
448449
449450 const paginationFlags : PaginationFlags = validatePaginationFlags ( {
450451 pageSize : opts . pageSize ,
@@ -467,7 +468,7 @@ export async function runList(opts: ListOptions, deps: TestDeps = {}): Promise<P
467468 validateStatusFilter ( opts . status ) ;
468469
469470 const baseQuery : Record < string , string | number | boolean | undefined > = {
470- projectId : opts . projectId ,
471+ projectId,
471472 type : opts . type ,
472473 createdFrom : opts . createdFrom ,
473474 status : opts . status ,
@@ -523,7 +524,7 @@ export type CliCreatePriority = (typeof CLI_CREATE_PRIORITIES)[number];
523524const MAX_INLINE_CODE_BYTES = 350 * 1024 ;
524525
525526interface CreateOptions extends CommonOptions {
526- projectId : string ;
527+ projectId ? : string ;
527528 type : 'frontend' | 'backend' ;
528529 name : string ;
529530 description ?: string ;
@@ -671,7 +672,8 @@ export async function runCreate(
671672 assertChainedRunKeyFits ( opts . run , opts . idempotencyKey ) ;
672673 // Validate inputs before touching credentials or fs — matches the
673674 // M2 read commands' "input gates first, then auth, then I/O" ordering.
674- requireProjectId ( opts . projectId ) ;
675+ const projectId = resolveProjectId ( opts . projectId , deps ) ;
676+ requireProjectId ( projectId ) ;
675677 requireNonEmpty ( 'name' , opts . name ) ;
676678 // P1-3: client-side length checks matching server limits (name ≤200,
677679 // description ≤2000) so the user gets instant, actionable errors instead
@@ -747,7 +749,7 @@ export async function runCreate(
747749 }
748750
749751 const body : Record < string , unknown > = {
750- projectId : opts . projectId ,
752+ projectId,
751753 type : opts . type ,
752754 name : opts . name ,
753755 description : opts . description ,
@@ -791,7 +793,7 @@ export async function runCreate(
791793 // B3: best-effort duplicate-name advisory. Skip under --dry-run.
792794 if ( ! opts . dryRun ) {
793795 const stderrFn = deps . stderr ?? ( ( line : string ) => process . stderr . write ( `${ line } \n` ) ) ;
794- await emitDupNameAdvisoryIfNeeded ( client , opts . projectId , opts . name , stderrFn ) ;
796+ await emitDupNameAdvisoryIfNeeded ( client , projectId , opts . name , stderrFn ) ;
795797 }
796798
797799 const response = await client . post < CliCreateTestResponse > ( '/tests' , {
@@ -811,7 +813,7 @@ export async function runCreate(
811813 // R1: suppress under --dry-run (fake canned test id).
812814 const chainDashboardUrl = opts . dryRun
813815 ? undefined
814- : resolvePortalUrl ( resolveApiUrl ( opts , deps ) , opts . projectId , response . testId ) ;
816+ : resolvePortalUrl ( resolveApiUrl ( opts , deps ) , projectId , response . testId ) ;
815817 const createContextWithUrl =
816818 chainDashboardUrl !== undefined ? { ...response , dashboardUrl : chainDashboardUrl } : response ;
817819 await runTestRun (
@@ -841,7 +843,7 @@ export async function runCreate(
841843 // (e.g. "test_dryrun_create_2026") and a live-looking URL would mislead.
842844 const dashboardUrl = opts . dryRun
843845 ? undefined
844- : resolvePortalUrl ( resolveApiUrl ( opts , deps ) , opts . projectId , response . testId ) ;
846+ : resolvePortalUrl ( resolveApiUrl ( opts , deps ) , projectId , response . testId ) ;
845847 if ( opts . output === 'json' ) {
846848 out . print ( dashboardUrl !== undefined ? { ...response , dashboardUrl } : response , data =>
847849 renderCreateText ( data as CliCreateTestResponse ) ,
@@ -5060,7 +5062,7 @@ export async function runTestWait(
50605062
50615063interface RunTestRunAllOptions extends CommonOptions {
50625064 /** projectId to run all BE tests in. */
5063- projectId : string ;
5065+ projectId ? : string ;
50645066 /** --filter <substr>: only run tests whose name contains this substring (case-insensitive). */
50655067 nameFilter ?: string ;
50665068 /** --wait: block until terminal or --timeout. */
@@ -5097,7 +5099,8 @@ export async function runTestRunAll(
50975099 deps : TestDeps = { } ,
50985100) : Promise < BatchRunFreshResponse | undefined > {
50995101 assertIdempotencyKey ( opts . idempotencyKey ) ;
5100- requireProjectId ( opts . projectId ) ;
5102+ const projectId = resolveProjectId ( opts . projectId , deps ) ;
5103+ requireProjectId ( projectId ) ;
51015104 if (
51025105 ! Number . isInteger ( opts . maxConcurrency ) ||
51035106 opts . maxConcurrency < 1 ||
@@ -5121,7 +5124,7 @@ export async function runTestRunAll(
51215124 method : 'POST' ,
51225125 path : '/api/cli/v1/tests/batch/run' ,
51235126 body : {
5124- projectId : opts . projectId ,
5127+ projectId,
51255128 testIds : opts . nameFilter ? [ '<filtered by --filter>' ] : undefined ,
51265129 source : 'cli' as const ,
51275130 } ,
@@ -5144,9 +5147,9 @@ export async function runTestRunAll(
51445147 const projectDashboardUrl =
51455148 batchPortalBase === undefined
51465149 ? undefined
5147- : `${ batchPortalBase } /dashboard/tests/${ encodeURIComponent ( opts . projectId ) } ` ;
5150+ : `${ batchPortalBase } /dashboard/tests/${ encodeURIComponent ( projectId ) } ` ;
51485151 const withBatchDashboardUrl = < T extends { testId : string } > ( item : T ) : T => {
5149- const dashboardUrl = resolvePortalUrl ( batchApiUrl , opts . projectId , item . testId ) ;
5152+ const dashboardUrl = resolvePortalUrl ( batchApiUrl , projectId , item . testId ) ;
51505153 return dashboardUrl !== undefined ? { ...item , dashboardUrl } : item ;
51515154 } ;
51525155
@@ -5162,7 +5165,7 @@ export async function runTestRunAll(
51625165 const allPage = await paginate < CliTest > (
51635166 async ( { pageSize, cursor } ) =>
51645167 client . get < Page < CliTest > > ( '/tests' , {
5165- query : { projectId : opts . projectId , pageSize, cursor } ,
5168+ query : { projectId, pageSize, cursor } ,
51665169 } ) ,
51675170 { } ,
51685171 ) ;
@@ -5178,7 +5181,7 @@ export async function runTestRunAll(
51785181 testIds = filtered . map ( t => t . id ) ;
51795182 if ( testIds . length === 0 ) {
51805183 stderrFn (
5181- `No tests found in project ${ opts . projectId } matching --filter "${ opts . nameFilter } " — nothing to run.` ,
5184+ `No tests found in project ${ projectId } matching --filter "${ opts . nameFilter } " — nothing to run.` ,
51825185 ) ;
51835186 out . print ( {
51845187 accepted : [ ] ,
@@ -5190,14 +5193,14 @@ export async function runTestRunAll(
51905193 return undefined ;
51915194 }
51925195 stderrFn (
5193- `Resolved ${ testIds . length } test${ testIds . length !== 1 ? 's' : '' } in project ${ opts . projectId } for batch run.` ,
5196+ `Resolved ${ testIds . length } test${ testIds . length !== 1 ? 's' : '' } in project ${ projectId } for batch run.` ,
51945197 ) ;
51955198 }
51965199 // When no --filter, omit testIds → server runs ALL BE tests in the project.
51975200
51985201 const batchResp = await client . triggerBatchRunFresh (
51995202 {
5200- projectId : opts . projectId ,
5203+ projectId,
52015204 ...( testIds !== undefined ? { testIds } : { } ) ,
52025205 source : 'cli' ,
52035206 } ,
@@ -5341,7 +5344,7 @@ export async function runTestRunAll(
53415344 try {
53425345 retryResp = await client . triggerBatchRunFresh (
53435346 {
5344- projectId : opts . projectId ,
5347+ projectId,
53455348 testIds : retryIds ,
53465349 source : 'cli' ,
53475350 } ,
@@ -7465,10 +7468,11 @@ export function createTestCommand(deps: TestDeps = {}): Command {
74657468
74667469 if ( isAll ) {
74677470 // --all path: wave-ordered fresh batch run.
7468- if ( ! cmdOpts . project ) {
7471+ const projectId = resolveProjectId ( cmdOpts . project , deps ) ;
7472+ if ( ! projectId ) {
74697473 throw localValidationError (
74707474 'project' ,
7471- '--all requires a project id — pass --project <id>' ,
7475+ '--all requires a project id - pass --project <id> or set TESTSPRITE_PROJECT_ID ' ,
74727476 ) ;
74737477 }
74747478 // --target-url has no effect on the --all batch path: it is BE-only
@@ -7484,7 +7488,7 @@ export function createTestCommand(deps: TestDeps = {}): Command {
74847488 await runTestRunAll (
74857489 {
74867490 ...resolveCommonOptions ( command ) ,
7487- projectId : cmdOpts . project ,
7491+ projectId,
74887492 nameFilter : cmdOpts . filter ,
74897493 wait : cmdOpts . wait === true ,
74907494 timeoutSeconds : parseTimeoutFlag ( cmdOpts . timeout , 'timeout' ) ,
@@ -7772,7 +7776,13 @@ interface StepsFlagOpts {
77727776 runId ?: string ;
77737777}
77747778
7775- function requireProjectId ( projectId : string ) : void {
7779+ function resolveProjectId ( projectId : string | undefined , deps : TestDeps ) : string | undefined {
7780+ if ( projectId !== undefined ) return projectId ;
7781+ const envValue = ( deps . env ?? process . env ) . TESTSPRITE_PROJECT_ID ;
7782+ const trimmed = envValue ?. trim ( ) ;
7783+ return trimmed && trimmed . length > 0 ? trimmed : undefined ;
7784+ }
7785+ function requireProjectId ( projectId : string | undefined ) : asserts projectId is string {
77767786 if ( typeof projectId !== 'string' || projectId . length === 0 ) {
77777787 throw localValidationError ( 'project' , 'is required' ) ;
77787788 }
0 commit comments