@@ -1431,5 +1431,238 @@ await copilotEventManager.SaveSingleCopilotEventToSqlStaging(new CopilotAuditLog
14311431 Assert . IsFalse ( standardChat . Agent . IsCustomAgent . HasValue && standardChat . Agent . IsCustomAgent . Value , "IsCustomAgent should be false/null for standard agent" ) ;
14321432 }
14331433 }
1434+
1435+ /// <summary>
1436+ /// Tests that only custom agents are charged Copilot Credits.
1437+ /// Standard M365 Copilot should have 0 credits.
1438+ /// </summary>
1439+ [ TestMethod ]
1440+ public void CopilotCreditEstimation_CustomAgentOnly_ChargesCopilotCredits ( )
1441+ {
1442+ // Arrange - create audit event with messages and tenant graph resources
1443+ var json = @"{
1444+ ""Messages"": [
1445+ { ""Id"": ""msg1"", ""isPrompt"": true },
1446+ { ""Id"": ""msg2"", ""isPrompt"": false },
1447+ { ""Id"": ""msg3"", ""isPrompt"": false }
1448+ ],
1449+ ""AccessedResources"": [
1450+ { ""Type"": ""File"", ""SiteUrl"": ""https://contoso.sharepoint.com/sites/team"" }
1451+ ],
1452+ ""ModelTransparencyDetails"": []
1453+ }" ;
1454+
1455+ // Act - analyze for custom agent
1456+ var customAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : true ) ;
1457+
1458+ // Assert - custom agent should be charged
1459+ Assert . AreEqual ( 2 , customAgentCost . GenerativeAnswers , "Custom agent should have 2 generative answers" ) ;
1460+ Assert . AreEqual ( 2 , customAgentCost . TenantGraphGroundedAnswers , "Custom agent should have 2 tenant graph grounded answers" ) ;
1461+ Assert . AreEqual ( 24 , customAgentCost . TotalCredits , "Custom agent should be charged 24 credits (2 * (2 + 10))" ) ;
1462+
1463+ // Act - analyze for non-custom (standard M365) agent
1464+ var standardAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : false ) ;
1465+
1466+ // Assert - standard agent should NOT be charged
1467+ Assert . AreEqual ( 0 , standardAgentCost . GenerativeAnswers , "Standard agent should have 0 generative answers counted" ) ;
1468+ Assert . AreEqual ( 0 , standardAgentCost . TenantGraphGroundedAnswers , "Standard agent should have 0 tenant graph answers counted" ) ;
1469+ Assert . AreEqual ( 0 , standardAgentCost . TotalCredits , "Standard agent should have 0 credits" ) ;
1470+
1471+ // Verify analytics data is still captured for standard agents
1472+ Assert . IsTrue ( standardAgentCost . ResourceTypeBreakdown . Count > 0 , "Resource breakdown should still be captured for analytics" ) ;
1473+ }
1474+
1475+ /// <summary>
1476+ /// Tests that deep reasoning (premium model) is only charged for custom agents
1477+ /// </summary>
1478+ [ TestMethod ]
1479+ public void CopilotCreditEstimation_CustomAgentOnly_ChargesDeepReasoning ( )
1480+ {
1481+ // Arrange - create audit event with deep reasoning model
1482+ var json = @"{
1483+ ""Messages"": [
1484+ { ""Id"": ""msg1"", ""isPrompt"": true },
1485+ { ""Id"": ""msg2"", ""isPrompt"": false }
1486+ ],
1487+ ""AccessedResources"": [
1488+ { ""Type"": ""File"", ""SiteUrl"": ""https://contoso.sharepoint.com/sites/team"" }
1489+ ],
1490+ ""ModelTransparencyDetails"": [
1491+ { ""ModelName"": ""DEEP_LEO"" }
1492+ ]
1493+ }" ;
1494+
1495+ // Act - analyze for custom agent
1496+ var customAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : true ) ;
1497+
1498+ // Assert - custom agent should be charged for deep reasoning
1499+ Assert . AreEqual ( 1 , customAgentCost . DeepReasoningActions , "Custom agent should have 1 deep reasoning action" ) ;
1500+ Assert . AreEqual ( 17 , customAgentCost . TotalCredits , "Custom agent should be charged 17 credits (2 + 10 + 5)" ) ;
1501+ Assert . IsTrue ( customAgentCost . ModelsUsed . Contains ( "DEEP_LEO" ) , "DEEP_LEO model should be tracked for custom agent" ) ;
1502+
1503+ // Act - analyze for non-custom (standard M365) agent
1504+ var standardAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : false ) ;
1505+
1506+ // Assert - standard agent should NOT be charged for deep reasoning
1507+ Assert . AreEqual ( 0 , standardAgentCost . DeepReasoningActions , "Standard agent should have 0 deep reasoning actions counted" ) ;
1508+ Assert . AreEqual ( 0 , standardAgentCost . TotalCredits , "Standard agent should have 0 credits" ) ;
1509+
1510+ // Verify model is still tracked for analytics even though not charged
1511+ Assert . IsTrue ( standardAgentCost . ModelsUsed . Contains ( "DEEP_LEO" ) , "DEEP_LEO model should still be tracked for analytics" ) ;
1512+ }
1513+
1514+ /// <summary>
1515+ /// Tests that web-only searches (no tenant resources) still result in 0 credits for standard agents
1516+ /// </summary>
1517+ [ TestMethod ]
1518+ public void CopilotCreditEstimation_StandardAgent_WebSearchesHaveZeroCredits ( )
1519+ {
1520+ // Arrange - create audit event with web-only resources (no tenant graph)
1521+ var json = @"{
1522+ ""Messages"": [
1523+ { ""Id"": ""msg1"", ""isPrompt"": true },
1524+ { ""Id"": ""msg2"", ""isPrompt"": false },
1525+ { ""Id"": ""msg3"", ""isPrompt"": false }
1526+ ],
1527+ ""AccessedResources"": [
1528+ { ""Type"": ""WebPage"", ""SiteUrl"": ""https://www.example.com"" }
1529+ ],
1530+ ""ModelTransparencyDetails"": []
1531+ }" ;
1532+
1533+ // Act - analyze for custom agent (should only charge for generative, not tenant graph)
1534+ var customAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : true ) ;
1535+
1536+ // Assert - custom agent charged for generative only (no tenant graph)
1537+ Assert . AreEqual ( 2 , customAgentCost . GenerativeAnswers , "Custom agent should have 2 generative answers" ) ;
1538+ Assert . AreEqual ( 0 , customAgentCost . TenantGraphGroundedAnswers , "No tenant graph resources accessed" ) ;
1539+ Assert . AreEqual ( 4 , customAgentCost . TotalCredits , "Custom agent should be charged 4 credits (2 * 2)" ) ;
1540+
1541+ // Act - analyze for standard agent
1542+ var standardAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : false ) ;
1543+
1544+ // Assert - standard agent has 0 credits
1545+ Assert . AreEqual ( 0 , standardAgentCost . TotalCredits , "Standard agent should have 0 credits even with web searches" ) ;
1546+ }
1547+
1548+ /// <summary>
1549+ /// Tests comprehensive scenario with all billing components for custom vs standard agents
1550+ /// </summary>
1551+ [ TestMethod ]
1552+ public void CopilotCreditEstimation_ComprehensiveScenario_DifferentiatesAgentTypes ( )
1553+ {
1554+ // Arrange - complex scenario with multiple messages, tenant resources, and deep reasoning
1555+ var json = @"{
1556+ ""Messages"": [
1557+ { ""Id"": ""msg1"", ""isPrompt"": true },
1558+ { ""Id"": ""msg2"", ""isPrompt"": false },
1559+ { ""Id"": ""msg3"", ""isPrompt"": false },
1560+ { ""Id"": ""msg4"", ""isPrompt"": true },
1561+ { ""Id"": ""msg5"", ""isPrompt"": false }
1562+ ],
1563+ ""AccessedResources"": [
1564+ { ""Type"": ""docx"", ""Name"": ""Document1.docx"", ""SiteUrl"": ""https://contoso.sharepoint.com/sites/team"" },
1565+ { ""Type"": ""xlsx"", ""Name"": ""Spreadsheet1.xlsx"", ""SiteUrl"": ""https://contoso-my.sharepoint.com/personal/user"" },
1566+ { ""Type"": ""Email"", ""Name"": ""Meeting Notes"" }
1567+ ],
1568+ ""ModelTransparencyDetails"": [
1569+ { ""ModelName"": ""DEEP_LEO"" }
1570+ ]
1571+ }" ;
1572+
1573+ // Act - analyze for custom agent
1574+ var customAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : true ) ;
1575+
1576+ // Assert - custom agent full billing
1577+ Assert . AreEqual ( 3 , customAgentCost . GenerativeAnswers , "3 response messages" ) ;
1578+ Assert . AreEqual ( 3 , customAgentCost . TenantGraphGroundedAnswers , "All 3 responses use tenant graph" ) ;
1579+ Assert . AreEqual ( 1 , customAgentCost . DeepReasoningActions , "1 deep reasoning action" ) ;
1580+ Assert . AreEqual ( 41 , customAgentCost . TotalCredits , "Total: 3*(2+10) + 5 = 36 + 5 = 41 credits" ) ;
1581+ Assert . AreEqual ( 3 , customAgentCost . ResourceTypeBreakdown . Count , "3 resource types accessed" ) ;
1582+ Assert . IsTrue ( customAgentCost . CreditBreakdown . ContainsKey ( "Generative Answers" ) , "Should have generative breakdown" ) ;
1583+ Assert . IsTrue ( customAgentCost . CreditBreakdown . ContainsKey ( "Tenant Graph Grounding" ) , "Should have tenant graph breakdown" ) ;
1584+ Assert . IsTrue ( customAgentCost . CreditBreakdown . ContainsKey ( "Agent Actions (Deep Reasoning)" ) , "Should have deep reasoning breakdown" ) ;
1585+
1586+ // Act - analyze for standard agent
1587+ var standardAgentCost = CopilotCreditEstimation . Analyze ( json , isCustomAgent : false ) ;
1588+
1589+ // Assert - standard agent no billing but analytics preserved
1590+ Assert . AreEqual ( 0 , standardAgentCost . GenerativeAnswers , "No answers counted for standard agent" ) ;
1591+ Assert . AreEqual ( 0 , standardAgentCost . TenantGraphGroundedAnswers , "No grounding counted for standard agent" ) ;
1592+ Assert . AreEqual ( 0 , standardAgentCost . DeepReasoningActions , "No actions counted for standard agent" ) ;
1593+ Assert . AreEqual ( 0 , standardAgentCost . TotalCredits , "Standard M365 Copilot has 0 credits" ) ;
1594+ Assert . AreEqual ( 0 , standardAgentCost . CreditBreakdown . Count , "No credit breakdown for standard agent" ) ;
1595+
1596+ // Analytics data still captured
1597+ Assert . AreEqual ( 3 , standardAgentCost . ResourceTypeBreakdown . Count , "Resource analytics still captured" ) ;
1598+ Assert . IsTrue ( standardAgentCost . ModelsUsed . Contains ( "DEEP_LEO" ) , "Model analytics still captured" ) ;
1599+ }
1600+
1601+ /// <summary>
1602+ /// Tests empty/null scenarios for both agent types
1603+ /// </summary>
1604+ [ TestMethod ]
1605+ public void CopilotCreditEstimation_EmptyEvents_BothAgentTypesReturnZero ( )
1606+ {
1607+ // Arrange - empty event
1608+ var emptyJson = @"{
1609+ ""Messages"": [],
1610+ ""AccessedResources"": [],
1611+ ""ModelTransparencyDetails"": []
1612+ }" ;
1613+
1614+ // Act
1615+ var customAgentCost = CopilotCreditEstimation . Analyze ( emptyJson , isCustomAgent : true ) ;
1616+ var standardAgentCost = CopilotCreditEstimation . Analyze ( emptyJson , isCustomAgent : false ) ;
1617+
1618+ // Assert - both should return 0
1619+ Assert . AreEqual ( 0 , customAgentCost . TotalCredits , "Empty custom agent event should have 0 credits" ) ;
1620+ Assert . AreEqual ( 0 , standardAgentCost . TotalCredits , "Empty standard agent event should have 0 credits" ) ;
1621+
1622+ // Test null string
1623+ var nullCost = CopilotCreditEstimation . Analyze ( ( string ) null , isCustomAgent : true ) ;
1624+ Assert . AreEqual ( 0 , nullCost . TotalCredits , "Null event should have 0 credits" ) ;
1625+
1626+ // Test empty string
1627+ var emptyCost = CopilotCreditEstimation . Analyze ( string . Empty , isCustomAgent : true ) ;
1628+ Assert . AreEqual ( 0 , emptyCost . TotalCredits , "Empty string should have 0 credits" ) ;
1629+ }
1630+
1631+ /// <summary>
1632+ /// Tests that when there's no agent information (AgentName is null/empty), Cost is set to NoCost (0 credits).
1633+ /// This verifies the logic in CopilotAuditLogContent.FromJson() that assigns NoCost when AgentName is empty.
1634+ /// </summary>
1635+ [ TestMethod ]
1636+ public void CopilotAuditLogContent_NoAgentName_HasNoCost ( )
1637+ {
1638+ // Arrange - JSON with Copilot event data but NO AgentName or AgentId
1639+ // This would normally result in charges, but without an agent identifier, no cost should be assigned
1640+ var jsonWithoutAgent = @"{
1641+ ""CopilotEventData"": {
1642+ ""AppHost"": ""Teams"",
1643+ ""AccessedResources"": [
1644+ {
1645+ ""Id"": ""file123"",
1646+ ""Type"": ""File"",
1647+ ""SiteUrl"": ""https://contoso.sharepoint.com/sites/test""
1648+ }
1649+ ]
1650+ },
1651+ ""Messages"": [
1652+ { ""IsPrompt"": true },
1653+ { ""IsPrompt"": false }
1654+ ]
1655+ }" ;
1656+
1657+ // Act - Deserialize using FromJson (which applies the cost logic)
1658+ var auditLog = CopilotAuditLogContent . FromJson ( jsonWithoutAgent ) ;
1659+
1660+ // Assert - When AgentName is null/empty, Cost should be NoCost (0 credits)
1661+ Assert . IsNotNull ( auditLog . Cost , "Cost should not be null" ) ;
1662+ Assert . AreEqual ( 0 , auditLog . Cost . TotalCredits , "When AgentName is empty, TotalCredits should be 0" ) ;
1663+ Assert . IsTrue ( string . IsNullOrEmpty ( auditLog . AgentName ) , "AgentName should be null or empty" ) ;
1664+ Assert . IsTrue ( string . IsNullOrEmpty ( auditLog . AgentId ) , "AgentId should be null or empty" ) ;
1665+ Assert . IsNull ( auditLog . IsCustomAgent , "IsCustomAgent should be null when agent info is not present" ) ;
1666+ }
14341667 }
14351668}
0 commit comments