@@ -309,6 +309,68 @@ public async Task CopilotEventManagerAgentNameUpdateSaveTest()
309309 }
310310 }
311311
312+ /// <summary>
313+ /// Tests that when TargetAgentName changes for the same AgentId, the DB is updated with the new name
314+ /// </summary>
315+ [ TestMethod ]
316+ public async Task CopilotEventManagerTargetAgentNameUpdateSaveTest ( )
317+ {
318+ using ( var _db = new AnalyticsEntitiesContext ( _config . ConnectionStrings . SQL , true , false ) )
319+ {
320+ await ClearEvents ( _db ) ;
321+
322+ var adaptor = new FakeCopilotMetadataLoader ( ) ;
323+
324+ // First save with initial TargetAgentName resolved through FromJson
325+ var agentId = "TargetAgentTest_" + DateTime . Now . Ticks ;
326+ var initialTargetName = "InitialCustomEngine_" + DateTime . Now . Ticks ;
327+ var firstChatEvents = await ExecuteCopilotEventManagerSaveFlow ( adaptor , _db , Tuple . Create ( agentId , initialTargetName ) ) ;
328+
329+ // Verify first events saved with initial agent name
330+ foreach ( var evt in firstChatEvents )
331+ {
332+ var id = evt . Id ;
333+ var reloaded = await _db . CopilotChats . Include ( x => x . Agent ) . FirstOrDefaultAsync ( x => x . AuditEvent . Id == id ) ;
334+ Assert . IsNotNull ( reloaded , $ "CopilotChat not found for initial event { id } ") ;
335+ Assert . IsNotNull ( reloaded . Agent , $ "Agent navigation null for initial event { id } ") ;
336+ Assert . AreEqual ( agentId , reloaded . Agent . AgentID , $ "AgentID mismatch for initial event { id } ") ;
337+ Assert . AreEqual ( initialTargetName , reloaded . Agent . Name , $ "Agent Name mismatch for initial event { id } ") ;
338+ }
339+
340+ // Second save with updated TargetAgentName (same AgentId) - simulates custom engine agent rename
341+ var updatedTargetName = "UpdatedCustomEngine_" + DateTime . Now . Ticks ;
342+ var secondChatEvents = await ExecuteCopilotEventManagerSaveFlow ( adaptor , _db , Tuple . Create ( agentId , updatedTargetName ) ) ;
343+
344+ // Verify ALL second events saved and agent name updated
345+ foreach ( var evt in secondChatEvents )
346+ {
347+ var id = evt . Id ;
348+ var reloaded = await _db . CopilotChats . Include ( x => x . Agent ) . FirstOrDefaultAsync ( x => x . AuditEvent . Id == id ) ;
349+ if ( reloaded ? . Agent != null )
350+ {
351+ await _db . Entry ( reloaded . Agent ) . ReloadAsync ( ) ;
352+ }
353+ Assert . IsNotNull ( reloaded , $ "CopilotChat not found for second event { id } ") ;
354+ Assert . IsNotNull ( reloaded . Agent , $ "Agent navigation null for second event { id } ") ;
355+ Assert . AreEqual ( agentId , reloaded . Agent . AgentID , $ "AgentID mismatch for second event { id } ") ;
356+ Assert . AreEqual ( updatedTargetName , reloaded . Agent . Name , $ "Updated Agent Name mismatch for second event { id } ") ;
357+ }
358+
359+ // Assert previously created events now reflect updated agent name
360+ var previouslyCreatedIds = firstChatEvents . Select ( e => e . Id ) . ToList ( ) ;
361+ var previouslyCreatedChats = await _db . CopilotChats . Include ( x => x . Agent )
362+ . Where ( x => previouslyCreatedIds . Contains ( x . AuditEvent . Id ) ) . ToListAsync ( ) ;
363+ foreach ( var chat in previouslyCreatedChats )
364+ {
365+ if ( chat . Agent != null )
366+ {
367+ await _db . Entry ( chat . Agent ) . ReloadAsync ( ) ;
368+ Assert . AreEqual ( updatedTargetName , chat . Agent . Name , "Existing event did not reflect updated agent name from TargetAgentName" ) ;
369+ }
370+ }
371+ }
372+ }
373+
312374 /// <summary>
313375 /// Tests that AccessedResources are correctly saved to lookup tables and junction table
314376 /// </summary>
@@ -1137,7 +1199,7 @@ public void CopilotAuditLogContent_FromJson_ExtractsAgentFromAppIdentity()
11371199 Assert . AreEqual ( appIdentity , result . AgentId , "AgentId should be set to AppIdentity value" ) ;
11381200 Assert . AreEqual ( appIdentity , result . AppIdentity , "AppIdentity should be preserved" ) ;
11391201 Assert . AreEqual ( organizationId , result . OrganizationId , "OrganizationId should be preserved" ) ;
1140- Assert . IsTrue ( result . IsCustomAgent . HasValue && result . IsCustomAgent . Value , "IsCustomAgent should be true when extracted from AppIdentity " ) ;
1202+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson " ) ;
11411203 }
11421204
11431205 /// <summary>
@@ -1171,7 +1233,221 @@ public void CopilotAuditLogContent_FromJson_PreservesExistingAgentValues()
11711233 Assert . IsNotNull ( result , "Result should not be null" ) ;
11721234 Assert . AreEqual ( existingAgentName , result . AgentName , "Existing AgentName should be preserved" ) ;
11731235 Assert . AreEqual ( existingAgentId , result . AgentId , "Existing AgentId should be preserved" ) ;
1174- Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should remain null when not extracted from AppIdentity" ) ;
1236+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1237+ }
1238+
1239+ /// <summary>
1240+ /// Tests that declarative agents (AgentId starting with "CopilotStudio.Declarative.") are not marked as custom
1241+ /// </summary>
1242+ [ TestMethod ]
1243+ public void CopilotAuditLogContent_FromJson_DeclarativeAgentIsNotCustom ( )
1244+ {
1245+ // Arrange
1246+ var agentName = "DeclarativeAgent" ;
1247+ var agentId = "CopilotStudio.Declarative.T_a83f31f8-c2a2-3418-17dd-c7a5c8b01a45.7c8cc6e4-257e-455b-b557-c6ffa78eda90" ;
1248+
1249+ var json = $@ "{{
1250+ ""AgentName"": ""{ agentName } "",
1251+ ""AgentId"": ""{ agentId } "",
1252+ ""CopilotEventData"": {{
1253+ ""AppHost"": ""Teams"",
1254+ ""AccessedResources"": [],
1255+ ""Contexts"": []
1256+ }}
1257+ }}" ;
1258+
1259+ // Act
1260+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1261+
1262+ // Assert
1263+ Assert . IsNotNull ( result , "Result should not be null" ) ;
1264+ Assert . AreEqual ( agentName , result . AgentName , "AgentName should be preserved" ) ;
1265+ Assert . AreEqual ( agentId , result . AgentId , "AgentId should be preserved" ) ;
1266+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1267+ }
1268+
1269+ /// <summary>
1270+ /// Tests that TargetAgentName is used as the agent name and marked as custom agent
1271+ /// </summary>
1272+ [ TestMethod ]
1273+ public void CopilotAuditLogContent_FromJson_UsesTargetAgentNameAsCustomAgent ( )
1274+ {
1275+ // Arrange
1276+ var targetAgentName = "MyCustomEngineAgent" ;
1277+ var agentId = "custom-engine-agent-id-123" ;
1278+
1279+ var json = $@ "{{
1280+ ""AgentId"": ""{ agentId } "",
1281+ ""CopilotEventData"": {{
1282+ ""AppHost"": ""Teams"",
1283+ ""AccessedResources"": [],
1284+ ""Contexts"": [],
1285+ ""TargetAgentName"": ""{ targetAgentName } ""
1286+ }}
1287+ }}" ;
1288+
1289+ // Act
1290+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1291+
1292+ // Assert
1293+ Assert . IsNotNull ( result , "Result should not be null" ) ;
1294+ Assert . AreEqual ( targetAgentName , result . AgentName , "AgentName should be set from TargetAgentName" ) ;
1295+ Assert . AreEqual ( agentId , result . AgentId , "AgentId should be preserved from JSON" ) ;
1296+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1297+ }
1298+
1299+ /// <summary>
1300+ /// Tests that TargetAgentName takes priority over AgentName when both are present
1301+ /// </summary>
1302+ [ TestMethod ]
1303+ public void CopilotAuditLogContent_FromJson_TargetAgentNameOverridesAgentName ( )
1304+ {
1305+ // Arrange
1306+ var targetAgentName = "CustomEngineAgent" ;
1307+ var declarativeAgentName = "DeclarativeAgent" ;
1308+ var agentId = "agent-id-456" ;
1309+
1310+ var json = $@ "{{
1311+ ""AgentName"": ""{ declarativeAgentName } "",
1312+ ""AgentId"": ""{ agentId } "",
1313+ ""CopilotEventData"": {{
1314+ ""AppHost"": ""Teams"",
1315+ ""AccessedResources"": [],
1316+ ""Contexts"": [],
1317+ ""TargetAgentName"": ""{ targetAgentName } ""
1318+ }}
1319+ }}" ;
1320+
1321+ // Act
1322+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1323+
1324+ // Assert
1325+ Assert . AreEqual ( targetAgentName , result . AgentName , "TargetAgentName should take priority over AgentName" ) ;
1326+ Assert . AreEqual ( agentId , result . AgentId , "AgentId should be preserved" ) ;
1327+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1328+ }
1329+
1330+ /// <summary>
1331+ /// Tests that TargetAgentName takes priority over AppIdentity fallback
1332+ /// </summary>
1333+ [ TestMethod ]
1334+ public void CopilotAuditLogContent_FromJson_TargetAgentNameSkipsAppIdentityFallback ( )
1335+ {
1336+ // Arrange
1337+ var organizationId = "873ca9a3-4805-48f2-b419-fabf868641da" ;
1338+ var appIdentity = $ "Copilot.Studio.Default-{ organizationId } -appIdentityAgent";
1339+ var targetAgentName = "CustomEngineAgentFromTargetField" ;
1340+
1341+ var json = $@ "{{
1342+ ""OrganizationId"": ""{ organizationId } "",
1343+ ""AppIdentity"": ""{ appIdentity } "",
1344+ ""CopilotEventData"": {{
1345+ ""AppHost"": ""Teams"",
1346+ ""AccessedResources"": [],
1347+ ""Contexts"": [],
1348+ ""TargetAgentName"": ""{ targetAgentName } ""
1349+ }}
1350+ }}" ;
1351+
1352+ // Act
1353+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1354+
1355+ // Assert
1356+ Assert . AreEqual ( targetAgentName , result . AgentName , "TargetAgentName should be used instead of AppIdentity extraction" ) ;
1357+ Assert . AreEqual ( appIdentity , result . AgentId , "AgentId should fall back to AppIdentity when not set in JSON" ) ;
1358+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1359+ }
1360+
1361+ /// <summary>
1362+ /// Tests that TargetAgentName uses AppIdentity as AgentId when AgentId is not set
1363+ /// </summary>
1364+ [ TestMethod ]
1365+ public void CopilotAuditLogContent_FromJson_TargetAgentNameUsesAppIdentityAsAgentIdFallback ( )
1366+ {
1367+ // Arrange
1368+ var targetAgentName = "CustomEngineAgent" ;
1369+ var appIdentity = "Copilot.Studio.Default-someorg-someagent" ;
1370+
1371+ var json = $@ "{{
1372+ ""AppIdentity"": ""{ appIdentity } "",
1373+ ""CopilotEventData"": {{
1374+ ""AppHost"": ""Teams"",
1375+ ""AccessedResources"": [],
1376+ ""Contexts"": [],
1377+ ""TargetAgentName"": ""{ targetAgentName } ""
1378+ }}
1379+ }}" ;
1380+
1381+ // Act
1382+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1383+
1384+ // Assert
1385+ Assert . AreEqual ( targetAgentName , result . AgentName , "AgentName should be set from TargetAgentName" ) ;
1386+ Assert . AreEqual ( appIdentity , result . AgentId , "AgentId should fall back to AppIdentity when not in JSON" ) ;
1387+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1388+ }
1389+
1390+ /// <summary>
1391+ /// Tests that TargetAgentName preserves existing AgentId when it is set
1392+ /// </summary>
1393+ [ TestMethod ]
1394+ public void CopilotAuditLogContent_FromJson_TargetAgentNamePreservesExistingAgentId ( )
1395+ {
1396+ // Arrange
1397+ var targetAgentName = "CustomEngineAgent" ;
1398+ var agentId = "explicit-agent-id-789" ;
1399+ var appIdentity = "Copilot.Studio.Default-someorg-someagent" ;
1400+
1401+ var json = $@ "{{
1402+ ""AgentId"": ""{ agentId } "",
1403+ ""AppIdentity"": ""{ appIdentity } "",
1404+ ""CopilotEventData"": {{
1405+ ""AppHost"": ""Teams"",
1406+ ""AccessedResources"": [],
1407+ ""Contexts"": [],
1408+ ""TargetAgentName"": ""{ targetAgentName } ""
1409+ }}
1410+ }}" ;
1411+
1412+ // Act
1413+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1414+
1415+ // Assert
1416+ Assert . AreEqual ( targetAgentName , result . AgentName , "AgentName should be set from TargetAgentName" ) ;
1417+ Assert . AreEqual ( agentId , result . AgentId , "AgentId from JSON should be preserved, not overwritten by AppIdentity" ) ;
1418+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1419+ }
1420+
1421+ /// <summary>
1422+ /// Tests that TargetAgentName triggers Copilot Credit calculation as a custom agent
1423+ /// </summary>
1424+ [ TestMethod ]
1425+ public void CopilotAuditLogContent_FromJson_TargetAgentNameCalculatesCost ( )
1426+ {
1427+ // Arrange - JSON with TargetAgentName inside CopilotEventData and event data that would incur costs
1428+ var json = @"{
1429+ ""AgentId"": ""cost-test-agent-id"",
1430+ ""CopilotEventData"": {
1431+ ""AppHost"": ""Teams"",
1432+ ""AccessedResources"": [
1433+ { ""Type"": ""File"", ""SiteUrl"": ""https://contoso.sharepoint.com/sites/team"" }
1434+ ],
1435+ ""Contexts"": [],
1436+ ""Messages"": [
1437+ { ""Id"": ""msg1"", ""isPrompt"": true },
1438+ { ""Id"": ""msg2"", ""isPrompt"": false }
1439+ ],
1440+ ""TargetAgentName"": ""CostTestAgent""
1441+ }
1442+ }" ;
1443+
1444+ // Act
1445+ var result = CopilotAuditLogContent . FromJson ( json ) ;
1446+
1447+ // Assert
1448+ Assert . IsNotNull ( result . Cost , "Cost should be calculated for TargetAgentName (custom agent)" ) ;
1449+ Assert . IsNull ( result . IsCustomAgent , "IsCustomAgent should always be null from FromJson" ) ;
1450+ Assert . AreEqual ( "CostTestAgent" , result . AgentName , "AgentName should be set from TargetAgentName" ) ;
11751451 }
11761452
11771453 /// <summary>
@@ -1326,7 +1602,7 @@ public void CopilotAuditLogContent_FromJson_HandlesVariousAgentNameFormats()
13261602 Assert . IsNotNull ( result , $ "Result should not be null for agent name: { expectedAgentName } ") ;
13271603 Assert . AreEqual ( expectedAgentName , result . AgentName , $ "AgentName should be correctly extracted for: { expectedAgentName } ") ;
13281604 Assert . AreEqual ( appIdentity , result . AgentId , $ "AgentId should be set to AppIdentity for: { expectedAgentName } ") ;
1329- Assert . IsTrue ( result . IsCustomAgent . HasValue && result . IsCustomAgent . Value , $ "IsCustomAgent should be true for: { expectedAgentName } ") ;
1605+ Assert . IsNull ( result . IsCustomAgent , $ "IsCustomAgent should always be null from FromJson for: { expectedAgentName } ") ;
13301606 }
13311607 }
13321608
0 commit comments