@@ -70,11 +70,11 @@ vi.mock("@/lib/agent-mode-diagnostics", () => ({
7070import { PolicyViolation } from "@/lib/collaboration-policy" ;
7171import { POST } from "./route" ;
7272
73- function makeRequest ( body : Record < string , unknown > ) {
73+ function makeRequest ( body : Record < string , unknown > , headers : Record < string , string > = { } ) {
7474 return new NextRequest ( new URL ( "/api/chat" , "http://localhost:3000" ) , {
7575 method : "POST" ,
7676 body : JSON . stringify ( body ) ,
77- headers : { "Content-Type" : "application/json" } ,
77+ headers : { "Content-Type" : "application/json" , ... headers } ,
7878 } ) ;
7979}
8080
@@ -163,6 +163,82 @@ describe("POST /api/chat", () => {
163163 expect ( mockGetGatewayClient ) . not . toHaveBeenCalled ( ) ;
164164 } ) ;
165165
166+ it ( "keeps explicit personal workspace scope when the company cookie is stale" , async ( ) => {
167+ mockResolveAccessibleWorkspace . mockResolvedValueOnce ( {
168+ id : "workspace-personal" ,
169+ companyId : null ,
170+ } ) ;
171+
172+ const response = await POST ( makeRequest ( {
173+ messages : [ { role : "user" , content : "hello" } ] ,
174+ agent : "main" ,
175+ companyId : null ,
176+ workspaceId : "workspace-personal" ,
177+ } , {
178+ Cookie : "active_company=company-stale; active_workspace=workspace-personal" ,
179+ } ) ) ;
180+
181+ expect ( response . status ) . toBe ( 200 ) ;
182+ expect ( mockResolveAccessibleWorkspace ) . toHaveBeenCalledWith ( expect . objectContaining ( {
183+ explicitCompanyId : null ,
184+ explicitWorkspaceId : "workspace-personal" ,
185+ requireExplicitForBearer : true ,
186+ } ) ) ;
187+ expect ( mockAssertPrimaryRuntimeInvocationAllowedForContext ) . toHaveBeenCalledWith ( expect . objectContaining ( {
188+ companyId : null ,
189+ workspaceId : "workspace-personal" ,
190+ } ) ) ;
191+
192+ await readFirstChunk ( response ) ;
193+ } ) ;
194+
195+ it ( "keeps explicit company scope when the workspace cookie is stale" , async ( ) => {
196+ mockResolveAccessibleWorkspace . mockResolvedValueOnce ( {
197+ id : "workspace-company" ,
198+ companyId : "company-1" ,
199+ } ) ;
200+
201+ const response = await POST ( makeRequest ( {
202+ messages : [ { role : "user" , content : "hello" } ] ,
203+ agent : "main" ,
204+ companyId : "company-1" ,
205+ } , {
206+ Cookie : "active_company=company-1; active_workspace=workspace-personal-stale" ,
207+ } ) ) ;
208+
209+ expect ( response . status ) . toBe ( 200 ) ;
210+ expect ( mockResolveAccessibleWorkspace ) . toHaveBeenCalledWith ( expect . objectContaining ( {
211+ explicitCompanyId : "company-1" ,
212+ explicitWorkspaceId : null ,
213+ requireExplicitForBearer : true ,
214+ } ) ) ;
215+ expect ( mockAssertPrimaryRuntimeInvocationAllowedForContext ) . toHaveBeenCalledWith ( expect . objectContaining ( {
216+ companyId : "company-1" ,
217+ workspaceId : "workspace-company" ,
218+ } ) ) ;
219+
220+ await readFirstChunk ( response ) ;
221+ } ) ;
222+
223+ it ( "rejects conflicting explicit company and workspace scopes" , async ( ) => {
224+ mockResolveAccessibleWorkspace . mockResolvedValueOnce ( {
225+ id : "workspace-personal" ,
226+ companyId : null ,
227+ } ) ;
228+
229+ const response = await POST ( makeRequest ( {
230+ messages : [ { role : "user" , content : "hello" } ] ,
231+ agent : "main" ,
232+ companyId : "company-1" ,
233+ workspaceId : "workspace-personal" ,
234+ } ) ) ;
235+
236+ expect ( response . status ) . toBe ( 403 ) ;
237+ await expect ( response . json ( ) ) . resolves . toEqual ( { error : "Forbidden" } ) ;
238+ expect ( mockAssertPrimaryRuntimeInvocationAllowedForContext ) . not . toHaveBeenCalled ( ) ;
239+ expect ( mockGetGatewayClient ) . not . toHaveBeenCalled ( ) ;
240+ } ) ;
241+
166242 it ( "rejects shared-context chat when the selected OpenClaw runtime is personal" , async ( ) => {
167243 mockAssertPrimaryRuntimeInvocationAllowedForContext . mockRejectedValueOnce ( new PolicyViolation ( {
168244 allowed : false ,
0 commit comments