@@ -61,6 +61,41 @@ const {
6161 releaseChatSendClaim : vi . fn ( ) ,
6262} ) )
6363
64+ /**
65+ * The root span, captured so a test can assert what a refused turn exported.
66+ * `withCopilotSpan` is a pass-through here — the nesting it provides is not
67+ * under test and a real tracer would need an exporter to observe.
68+ */
69+ const { setInputMessages, setUserMessagePreview, startCopilotOtelRoot } = vi . hoisted ( ( ) => ( {
70+ setInputMessages : vi . fn ( ) ,
71+ setUserMessagePreview : vi . fn ( ) ,
72+ startCopilotOtelRoot : vi . fn ( ) ,
73+ } ) )
74+
75+ vi . mock ( '@/lib/copilot/request/otel' , async ( ) => {
76+ const { ROOT_CONTEXT , trace } = await import ( '@opentelemetry/api' )
77+ const span = ( ) => trace . getTracer ( 'post-test' ) . startSpan ( 'post-test' )
78+ startCopilotOtelRoot . mockImplementation ( ( ) => ( {
79+ span : span ( ) ,
80+ context : ROOT_CONTEXT ,
81+ requestId : 'req-1' ,
82+ finish : vi . fn ( ) ,
83+ setUserMessagePreview,
84+ setInputMessages,
85+ setOutputMessages : vi . fn ( ) ,
86+ setRequestShape : vi . fn ( ) ,
87+ } ) )
88+ return {
89+ startCopilotOtelRoot,
90+ withCopilotSpan : (
91+ _name : string ,
92+ _attrs : Record < string , unknown > | undefined ,
93+ fn : ( child : ReturnType < typeof span > ) => unknown ,
94+ _context ?: unknown
95+ ) => fn ( span ( ) ) ,
96+ }
97+ } )
98+
6499const resolvePermissionGroupConfig = permissionGroupScopeMockFns . mockResolvePermissionGroupConfig
65100
66101const getSession = authMockFns . mockGetSession
@@ -1068,6 +1103,38 @@ describe('handleUnifiedChatPost copilot.use capability gate', () => {
10681103 expect ( createSSEStream ) . toHaveBeenCalledTimes ( 1 )
10691104 } )
10701105
1106+ /**
1107+ * Prompt content is exported only once the turn is going to run. GenAI
1108+ * message capture is gated on whether capture is enabled at all, not on
1109+ * whether this caller may send, so capturing at span start exported the
1110+ * message of every turn the gate then refused.
1111+ */
1112+ it ( 'exports no part of the prompt when the send is refused' , async ( ) => {
1113+ resolvePermissionGroupConfig . mockResolvedValue ( {
1114+ ...DEFAULT_PERMISSION_GROUP_CONFIG ,
1115+ hideCopilot : true ,
1116+ } )
1117+
1118+ const response = await handleUnifiedChatPost ( chatRequest ( { createNewChat : true } ) )
1119+
1120+ expect ( response . status ) . toBe ( 403 )
1121+ expect ( setInputMessages ) . not . toHaveBeenCalled ( )
1122+ expect ( setUserMessagePreview ) . not . toHaveBeenCalled ( )
1123+ expect ( startCopilotOtelRoot ) . toHaveBeenCalledWith (
1124+ expect . not . objectContaining ( { userMessagePreview : expect . anything ( ) } )
1125+ )
1126+ } )
1127+
1128+ it ( 'captures the prompt once the send is allowed to run' , async ( ) => {
1129+ resolvePermissionGroupConfig . mockResolvedValue ( DEFAULT_PERMISSION_GROUP_CONFIG )
1130+
1131+ const response = await handleUnifiedChatPost ( chatRequest ( { createNewChat : true } ) )
1132+
1133+ expect ( response . status ) . toBe ( 200 )
1134+ expect ( setUserMessagePreview ) . toHaveBeenCalledWith ( 'Hello' )
1135+ expect ( setInputMessages ) . toHaveBeenCalledWith ( { userMessage : 'Hello' } )
1136+ } )
1137+
10711138 /** A branch that lands in no workspace at all is governed by no group. */
10721139 it ( 'does not consult a permission group when the branch resolves no workspace' , async ( ) => {
10731140 resolveWorkflowIdForUser . mockResolvedValue ( {
0 commit comments