@@ -47,6 +47,9 @@ vi.mock("~/services/personalAccessToken.server", () => ({
4747 assertSourcePatActive : async ( ) => true ,
4848 updateLastAccessedAtIfStale : vi . fn ( ) ,
4949 resolveAndRecheckUserActorClaims : vi . fn ( ) ,
50+ // A real PAT authenticates a user but carries no actor claims, so it never reaches the locator.
51+ isPersonalAccessToken : ( token : string ) => token . startsWith ( "tr_pat_" ) ,
52+ authenticateApiRequestWithPersonalAccessToken : async ( ) => ( { userId : "user_with_a_pat" } ) ,
5053} ) ) ;
5154vi . mock ( "~/services/authTelemetry.server" , ( ) => ( {
5255 authenticateBearerWithTelemetry : vi . fn ( ) ,
@@ -81,7 +84,11 @@ function suffix() {
8184 return Math . random ( ) . toString ( 36 ) . slice ( 2 , 10 ) ;
8285}
8386
84- /** An org with two projects, each with prod plus a dev environment per member. */
87+ /**
88+ * An org with two projects. Each carries the whole environment family the locator has to name:
89+ * prod, staging, a dev root per member, a preview root, and branch children of both branchable
90+ * types — plus one archived preview branch.
91+ */
8592async function seedOrg ( prisma : PrismaClient ) {
8693 const slug = `locate_${ suffix ( ) } ` ;
8794 const member = await prisma . user . create ( {
@@ -111,8 +118,13 @@ async function seedOrg(prisma: PrismaClient) {
111118
112119 const environmentFor = (
113120 envSlug : string ,
114- type : "PRODUCTION" | "DEVELOPMENT" ,
115- orgMemberId ?: string
121+ type : "PRODUCTION" | "STAGING" | "PREVIEW" | "DEVELOPMENT" ,
122+ extra ?: {
123+ orgMemberId ?: string ;
124+ parentEnvironmentId ?: string ;
125+ branchName ?: string ;
126+ archivedAt ?: Date ;
127+ }
116128 ) =>
117129 prisma . runtimeEnvironment . create ( {
118130 data : {
@@ -123,16 +135,36 @@ async function seedOrg(prisma: PrismaClient) {
123135 apiKey : `tr_${ envSlug } _${ projectSlug } _${ suffix ( ) } ` ,
124136 pkApiKey : `pk_${ envSlug } _${ projectSlug } _${ suffix ( ) } ` ,
125137 shortcode : `${ envSlug } ${ suffix ( ) } ` ,
126- ...( orgMemberId ? { orgMemberId } : { } ) ,
138+ ...extra ,
127139 } ,
128140 select : { id : true , slug : true } ,
129141 } ) ;
130142
143+ const ownDev = await environmentFor ( "dev" , "DEVELOPMENT" , { orgMemberId : memberOf . id } ) ;
144+ const previewRoot = await environmentFor ( "preview" , "PREVIEW" ) ;
145+
131146 return {
132147 project,
133148 prod : await environmentFor ( "prod" , "PRODUCTION" ) ,
134- ownDev : await environmentFor ( "dev" , "DEVELOPMENT" , memberOf . id ) ,
135- otherDev : await environmentFor ( "dev" , "DEVELOPMENT" , otherMemberOf . id ) ,
149+ // The dashboard slug for staging is "stg"; the API name is "staging".
150+ staging : await environmentFor ( "stg" , "STAGING" ) ,
151+ ownDev,
152+ otherDev : await environmentFor ( "dev" , "DEVELOPMENT" , { orgMemberId : otherMemberOf . id } ) ,
153+ previewRoot,
154+ previewBranch : await environmentFor ( "preview-feat-a" , "PREVIEW" , {
155+ parentEnvironmentId : previewRoot . id ,
156+ branchName : "feat/a" ,
157+ } ) ,
158+ archivedPreviewBranch : await environmentFor ( "preview-feat-old" , "PREVIEW" , {
159+ parentEnvironmentId : previewRoot . id ,
160+ branchName : "feat/old" ,
161+ archivedAt : new Date ( ) ,
162+ } ) ,
163+ devBranch : await environmentFor ( "dev-feat-a" , "DEVELOPMENT" , {
164+ orgMemberId : memberOf . id ,
165+ parentEnvironmentId : ownDev . id ,
166+ branchName : "feat/a" ,
167+ } ) ,
136168 } ;
137169 }
138170
@@ -238,15 +270,18 @@ async function callLoader(opts: {
238270 id : string ;
239271 userId ?: string ;
240272 organizationId ?: string ;
273+ bearer ?: string ;
241274} ) {
242- const token = opts . userId
243- ? await signUserActorToken ( SESSION_SECRET , {
244- userId : opts . userId ,
245- client : "dashboard-agent" ,
246- cap : [ "read:runs" ] ,
247- ...( opts . organizationId ? { organizationId : opts . organizationId } : { } ) ,
248- } as any )
249- : undefined ;
275+ const token = opts . bearer
276+ ? opts . bearer
277+ : opts . userId
278+ ? await signUserActorToken ( SESSION_SECRET , {
279+ userId : opts . userId ,
280+ client : "dashboard-agent" ,
281+ cap : [ "read:runs" ] ,
282+ ...( opts . organizationId ? { organizationId : opts . organizationId } : { } ) ,
283+ } as any )
284+ : undefined ;
250285
251286 const response = await loader ( {
252287 request : new Request ( `https://api.trigger.dev/api/v1/locate/${ opts . kind } /${ opts . id } ` , {
@@ -305,6 +340,68 @@ containerTest(
305340 const ownDev = await callLoader ( { kind : "run" , id : ownDevRun . friendlyId , ...caller } ) ;
306341 expect ( ownDev . body . scopes [ 0 ] . targetable ) . toBe ( true ) ;
307342
343+ // Staging's dashboard slug is "stg"; the address the agent has to use is "staging".
344+ const stagingRun = await createRun (
345+ prisma ,
346+ scopeOf ( orgA , orgA . current ) ,
347+ orgA . current . staging . id
348+ ) ;
349+ const staging = await callLoader ( { kind : "run" , id : stagingRun . friendlyId , ...caller } ) ;
350+ expect ( staging . body . scopes [ 0 ] ) . toMatchObject ( {
351+ environmentName : "staging" ,
352+ environmentType : "STAGING" ,
353+ targetable : true ,
354+ } ) ;
355+ expect ( staging . body . scopes [ 0 ] . branchName ) . toBeUndefined ( ) ;
356+
357+ // A preview branch child: the name is the family, the branch is the rest of the address.
358+ const previewBranchRun = await createRun (
359+ prisma ,
360+ scopeOf ( orgA , orgA . current ) ,
361+ orgA . current . previewBranch . id
362+ ) ;
363+ const previewBranch = await callLoader ( {
364+ kind : "run" ,
365+ id : previewBranchRun . friendlyId ,
366+ ...caller ,
367+ } ) ;
368+ expect ( previewBranch . body . scopes [ 0 ] ) . toEqual ( {
369+ projectRef : orgA . current . project . externalRef ,
370+ projectName : orgA . current . project . name ,
371+ environmentName : "preview" ,
372+ environmentType : "PREVIEW" ,
373+ branchName : "feat/a" ,
374+ targetable : true ,
375+ } ) ;
376+
377+ // A dev branch child of the caller's own dev root.
378+ const devBranchRun = await createRun (
379+ prisma ,
380+ scopeOf ( orgA , orgA . current ) ,
381+ orgA . current . devBranch . id
382+ ) ;
383+ const devBranch = await callLoader ( { kind : "run" , id : devBranchRun . friendlyId , ...caller } ) ;
384+ expect ( devBranch . body . scopes [ 0 ] ) . toMatchObject ( {
385+ environmentName : "dev" ,
386+ environmentType : "DEVELOPMENT" ,
387+ branchName : "feat/a" ,
388+ targetable : true ,
389+ } ) ;
390+
391+ // An archived branch still exists, so the run is located, it just can't be acted in.
392+ const archivedRun = await createRun (
393+ prisma ,
394+ scopeOf ( orgA , orgA . current ) ,
395+ orgA . current . archivedPreviewBranch . id
396+ ) ;
397+ const archived = await callLoader ( { kind : "run" , id : archivedRun . friendlyId , ...caller } ) ;
398+ expect ( archived . body . found ) . toBe ( true ) ;
399+ expect ( archived . body . scopes [ 0 ] ) . toMatchObject ( {
400+ environmentName : "preview" ,
401+ branchName : "feat/old" ,
402+ targetable : false ,
403+ } ) ;
404+
308405 // A run in another organization must not even be confirmed to exist.
309406 const foreignRun = await createRun ( prisma , scopeOf ( orgB , orgB . current ) , orgB . current . prod . id ) ;
310407 const foreign = await callLoader ( { kind : "run" , id : foreignRun . friendlyId , ...caller } ) ;
@@ -320,6 +417,12 @@ containerTest(
320417 ( await callLoader ( { kind : "run" , id : siblingRun . friendlyId , userId : orgA . member . id } ) ) . status
321418 ) . toBe ( 401 ) ;
322419
420+ // A plain PAT authenticates a user but carries no actor claims, so it locates nothing.
421+ expect (
422+ ( await callLoader ( { kind : "run" , id : siblingRun . friendlyId , bearer : "tr_pat_not_an_actor" } ) )
423+ . status
424+ ) . toBe ( 401 ) ;
425+
323426 // A claim naming an organization the caller doesn't belong to.
324427 const outsider = await callLoader ( {
325428 kind : "run" ,
0 commit comments