@@ -4,7 +4,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
44import { eq } from 'drizzle-orm' ;
55
66import { db } from '../db/index.js' ;
7- import { contentItems , contentTypes , domains } from '../db/schema.js' ;
7+ import { agentRuns , contentItems , contentTypes , domains } from '../db/schema.js' ;
88import { resolvers } from './resolvers.js' ;
99import { schema } from './schema.js' ;
1010
@@ -14,6 +14,7 @@ describe('GraphQL Tenant Isolation', () => {
1414 let domain2TypeId : number ;
1515 let domain1ItemId : number ;
1616 let domain2ItemId : number ;
17+ let domain1RunId : number ;
1718
1819 beforeAll ( async ( ) => {
1920 const [ domain1 ] = await db . select ( ) . from ( domains ) . where ( eq ( domains . id , 1 ) ) ;
@@ -76,6 +77,15 @@ describe('GraphQL Tenant Isolation', () => {
7677 } ) . returning ( ) ;
7778 domain2ItemId = item2 . id ;
7879
80+ const [ run1 ] = await db . insert ( agentRuns ) . values ( {
81+ domainId : 1 ,
82+ goal : `domain-one-tenant-graphql-run-${ Date . now ( ) } ` ,
83+ runType : 'review_backlog_manager' ,
84+ status : 'waiting_approval' ,
85+ requestedBy : 'tenant-1'
86+ } ) . returning ( ) ;
87+ domain1RunId = run1 . id ;
88+
7989 app = Fastify ( { logger : false } ) ;
8090 app . register ( mercurius , {
8191 schema,
@@ -117,6 +127,9 @@ describe('GraphQL Tenant Isolation', () => {
117127 if ( domain2TypeId ) {
118128 await db . delete ( contentTypes ) . where ( eq ( contentTypes . id , domain2TypeId ) ) ;
119129 }
130+ if ( domain1RunId ) {
131+ await db . delete ( agentRuns ) . where ( eq ( agentRuns . id , domain1RunId ) ) ;
132+ }
120133 } ) ;
121134
122135 it ( 'rejects assigning a cross-tenant content type on updateContentItem' , async ( ) => {
@@ -219,4 +232,55 @@ describe('GraphQL Tenant Isolation', () => {
219232 expect ( payload . data ?. updateContentItemsBatch ?. results [ 0 ] ?. ok ) . toBe ( false ) ;
220233 expect ( payload . data ?. updateContentItemsBatch ?. results [ 0 ] ?. code ) . toBe ( 'CONTENT_ITEM_NOT_FOUND' ) ;
221234 } ) ;
235+
236+ it ( 'hides cross-tenant agent runs and rejects cross-tenant control actions' , async ( ) => {
237+ const queryResponse = await app . inject ( {
238+ method : 'POST' ,
239+ url : '/graphql' ,
240+ headers : {
241+ 'x-domain-id' : '2'
242+ } ,
243+ payload : {
244+ query : `
245+ query AgentRun($id: ID!) {
246+ agentRun(id: $id) { id status }
247+ }
248+ ` ,
249+ variables : {
250+ id : String ( domain1RunId )
251+ }
252+ }
253+ } ) ;
254+
255+ expect ( queryResponse . statusCode ) . toBe ( 200 ) ;
256+ const queryPayload = queryResponse . json ( ) as {
257+ data ?: { agentRun ?: { id : string } | null } ;
258+ } ;
259+ expect ( queryPayload . data ?. agentRun ) . toBeNull ( ) ;
260+
261+ const controlResponse = await app . inject ( {
262+ method : 'POST' ,
263+ url : '/graphql' ,
264+ headers : {
265+ 'x-domain-id' : '2'
266+ } ,
267+ payload : {
268+ query : `
269+ mutation ControlRun($id: ID!, $action: String!) {
270+ controlAgentRun(id: $id, action: $action) { id status }
271+ }
272+ ` ,
273+ variables : {
274+ id : String ( domain1RunId ) ,
275+ action : 'cancel'
276+ }
277+ }
278+ } ) ;
279+
280+ expect ( controlResponse . statusCode ) . toBe ( 200 ) ;
281+ const controlPayload = controlResponse . json ( ) as {
282+ errors ?: Array < { extensions ?: { code ?: string } } > ;
283+ } ;
284+ expect ( controlPayload . errors ?. [ 0 ] ?. extensions ?. code ) . toBe ( 'AGENT_RUN_NOT_FOUND' ) ;
285+ } ) ;
222286} ) ;
0 commit comments