| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525 |
- package mcp
- import (
- "context"
- "io"
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/clustercache"
- "github.com/opencost/opencost/core/pkg/opencost"
- models "github.com/opencost/opencost/pkg/cloud/models"
- "github.com/opencost/opencost/pkg/cloudcost"
- "github.com/opencost/opencost/pkg/costmodel"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func TestQueryTypeConstants(t *testing.T) {
- assert.Equal(t, QueryType("allocation"), AllocationQueryType)
- assert.Equal(t, QueryType("asset"), AssetQueryType)
- assert.Equal(t, QueryType("cloudcost"), CloudCostQueryType)
- }
- func TestAllocationQueryStruct(t *testing.T) {
- query := AllocationQuery{
- Step: 1 * time.Hour,
- Accumulate: true,
- ShareIdle: true,
- Aggregate: "namespace",
- IncludeIdle: true,
- IdleByNode: true,
- IncludeProportionalAssetResourceCosts: true,
- IncludeAggregatedMetadata: true,
- ShareLB: true,
- }
- assert.Equal(t, 1*time.Hour, query.Step)
- assert.True(t, query.Accumulate)
- assert.True(t, query.ShareIdle)
- assert.Equal(t, "namespace", query.Aggregate)
- assert.True(t, query.IncludeIdle)
- assert.True(t, query.IdleByNode)
- assert.True(t, query.IncludeProportionalAssetResourceCosts)
- assert.True(t, query.IncludeAggregatedMetadata)
- assert.True(t, query.ShareLB)
- }
- func TestAssetQueryStruct(t *testing.T) {
- query := AssetQuery{}
- // AssetQuery is currently empty, just test that it can be created
- assert.NotNil(t, query)
- }
- func TestCloudCostQueryStruct(t *testing.T) {
- query := CloudCostQuery{
- Aggregate: "provider,service",
- Accumulate: "day",
- Filter: "provider=aws",
- Provider: "aws",
- Service: "ec2",
- Category: "compute",
- Region: "us-east-1",
- AccountID: "123456789",
- }
- assert.Equal(t, "provider,service", query.Aggregate)
- assert.Equal(t, "day", query.Accumulate)
- assert.Equal(t, "provider=aws", query.Filter)
- assert.Equal(t, "aws", query.Provider)
- assert.Equal(t, "ec2", query.Service)
- assert.Equal(t, "compute", query.Category)
- assert.Equal(t, "us-east-1", query.Region)
- assert.Equal(t, "123456789", query.AccountID)
- }
- func TestMCPRequestStruct(t *testing.T) {
- request := MCPRequest{
- SessionID: "test-session-123",
- Query: &OpenCostQueryRequest{
- QueryType: AllocationQueryType,
- Window: "24h",
- AllocationParams: &AllocationQuery{
- Step: 1 * time.Hour,
- Accumulate: true,
- ShareIdle: true,
- },
- },
- }
- assert.Equal(t, "test-session-123", request.SessionID)
- assert.NotNil(t, request.Query)
- assert.Equal(t, AllocationQueryType, request.Query.QueryType)
- assert.Equal(t, "24h", request.Query.Window)
- assert.NotNil(t, request.Query.AllocationParams)
- assert.Equal(t, 1*time.Hour, request.Query.AllocationParams.Step)
- assert.True(t, request.Query.AllocationParams.Accumulate)
- assert.True(t, request.Query.AllocationParams.ShareIdle)
- }
- func TestMCPResponseStruct(t *testing.T) {
- response := MCPResponse{
- Data: "test-data",
- QueryInfo: QueryMetadata{
- QueryID: "query-123",
- Timestamp: time.Now(),
- ProcessingTime: 100 * time.Millisecond,
- },
- }
- assert.Equal(t, "test-data", response.Data)
- assert.Equal(t, "query-123", response.QueryInfo.QueryID)
- assert.NotZero(t, response.QueryInfo.Timestamp)
- assert.Equal(t, 100*time.Millisecond, response.QueryInfo.ProcessingTime)
- }
- func TestQueryMetadataStruct(t *testing.T) {
- metadata := QueryMetadata{
- QueryID: "query-456",
- Timestamp: time.Now(),
- ProcessingTime: 250 * time.Millisecond,
- }
- assert.Equal(t, "query-456", metadata.QueryID)
- assert.NotZero(t, metadata.Timestamp)
- assert.Equal(t, 250*time.Millisecond, metadata.ProcessingTime)
- }
- func TestOpenCostQueryRequestStruct(t *testing.T) {
- request := OpenCostQueryRequest{
- QueryType: AssetQueryType,
- Window: "7d",
- AssetParams: &AssetQuery{},
- }
- assert.Equal(t, AssetQueryType, request.QueryType)
- assert.Equal(t, "7d", request.Window)
- assert.NotNil(t, request.AssetParams)
- }
- // Test helper functions
- func createTestAllocation(name string) *Allocation {
- now := time.Now()
- return &Allocation{
- Name: name,
- CPUCost: 10.0,
- RAMCost: 5.0,
- GPUCost: 0.0,
- PVCost: 2.0,
- NetworkCost: 1.0,
- SharedCost: 0.5,
- ExternalCost: 0.0,
- TotalCost: 18.5,
- CPUCoreHours: 100.0,
- RAMByteHours: 5000000000.0,
- GPUHours: 0.0,
- PVByteHours: 2000000000.0,
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- }
- func createTestAsset(name string) *Asset {
- now := time.Now()
- return &Asset{
- Type: "node",
- Properties: AssetProperties{
- Category: "compute",
- Provider: "aws",
- Name: name,
- },
- CPUCost: 50.0,
- RAMCost: 25.0,
- GPUCost: 100.0,
- TotalCost: 175.0,
- CPUCoreHours: 500.0,
- RAMByteHours: 25000000000.0,
- GPUHours: 50.0,
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- }
- func createTestCloudCost(name string) *CloudCost {
- now := time.Now()
- return &CloudCost{
- Properties: CloudCostProperties{
- Provider: "aws",
- Service: "ec2",
- },
- Window: TimeWindow{
- Start: now.Add(-24 * time.Hour),
- End: now,
- },
- ListCost: CostMetric{
- Cost: 100.0,
- KubernetesPercent: 80.0,
- },
- NetCost: CostMetric{
- Cost: 95.0,
- KubernetesPercent: 80.0,
- },
- }
- }
- // Test MCP server response structures
- func TestAllocationResponseStruct(t *testing.T) {
- allocation := createTestAllocation("test-namespace")
- allocationSet := &AllocationSet{
- Name: "test-namespace",
- Properties: map[string]string{
- "namespace": "test-namespace",
- },
- Allocations: []*Allocation{allocation},
- }
- response := AllocationResponse{
- Allocations: map[string]*AllocationSet{
- "test-namespace": allocationSet,
- },
- }
- require.NotNil(t, response.Allocations)
- assert.Len(t, response.Allocations, 1)
- assert.Contains(t, response.Allocations, "test-namespace")
- allocSet := response.Allocations["test-namespace"]
- assert.Equal(t, "test-namespace", allocSet.Name)
- assert.Len(t, allocSet.Allocations, 1)
- alloc := allocSet.Allocations[0]
- assert.Equal(t, "test-namespace", alloc.Name)
- assert.Equal(t, 10.0, alloc.CPUCost)
- assert.Equal(t, 5.0, alloc.RAMCost)
- assert.Equal(t, 18.5, alloc.TotalCost)
- }
- func TestAssetResponseStruct(t *testing.T) {
- asset := createTestAsset("test-node")
- assetSet := &AssetSet{
- Name: "test-node",
- Assets: []*Asset{asset},
- }
- response := AssetResponse{
- Assets: map[string]*AssetSet{
- "test-node": assetSet,
- },
- }
- require.NotNil(t, response.Assets)
- assert.Len(t, response.Assets, 1)
- assert.Contains(t, response.Assets, "test-node")
- assetSetResult := response.Assets["test-node"]
- assert.Equal(t, "test-node", assetSetResult.Name)
- assert.Len(t, assetSetResult.Assets, 1)
- assetResult := assetSetResult.Assets[0]
- assert.Equal(t, "node", assetResult.Type)
- assert.Equal(t, 50.0, assetResult.CPUCost)
- assert.Equal(t, 25.0, assetResult.RAMCost)
- assert.Equal(t, 100.0, assetResult.GPUCost)
- assert.Equal(t, 175.0, assetResult.TotalCost)
- }
- func TestCloudCostResponseStruct(t *testing.T) {
- cloudCost := createTestCloudCost("aws-ec2")
- cloudCostSet := &CloudCostSet{
- Name: "aws-ec2",
- CloudCosts: []*CloudCost{cloudCost},
- Window: &TimeWindow{
- Start: time.Now().Add(-24 * time.Hour),
- End: time.Now(),
- },
- }
- response := CloudCostResponse{
- CloudCosts: map[string]*CloudCostSet{
- "aws-ec2": cloudCostSet,
- },
- Summary: &CloudCostSummary{
- TotalNetCost: 95.0,
- TotalAmortizedCost: 90.0,
- TotalInvoicedCost: 100.0,
- KubernetesPercent: 80.0,
- },
- }
- require.NotNil(t, response.CloudCosts)
- assert.Len(t, response.CloudCosts, 1)
- assert.Contains(t, response.CloudCosts, "aws-ec2")
- costSet := response.CloudCosts["aws-ec2"]
- assert.Equal(t, "aws-ec2", costSet.Name)
- assert.Len(t, costSet.CloudCosts, 1)
- cost := costSet.CloudCosts[0]
- assert.Equal(t, "aws", cost.Properties.Provider)
- assert.Equal(t, "ec2", cost.Properties.Service)
- assert.Equal(t, 100.0, cost.ListCost.Cost)
- assert.Equal(t, 95.0, cost.NetCost.Cost)
- require.NotNil(t, response.Summary)
- assert.Equal(t, 95.0, response.Summary.TotalNetCost)
- assert.Equal(t, 80.0, response.Summary.KubernetesPercent)
- }
- // Test allocation set functionality
- func TestAllocationSetTotalCost(t *testing.T) {
- alloc1 := createTestAllocation("alloc1")
- alloc1.TotalCost = 10.0
- alloc2 := createTestAllocation("alloc2")
- alloc2.TotalCost = 15.0
- allocSet := &AllocationSet{
- Name: "test-set",
- Allocations: []*Allocation{alloc1, alloc2},
- }
- totalCost := allocSet.TotalCost()
- assert.Equal(t, 25.0, totalCost)
- }
- // Test asset properties
- func TestAssetProperties(t *testing.T) {
- props := AssetProperties{
- Category: "compute",
- Provider: "aws",
- Account: "123456789",
- Project: "my-project",
- Service: "ec2",
- Cluster: "prod-cluster",
- Name: "worker-node-1",
- ProviderID: "i-1234567890abcdef0",
- }
- assert.Equal(t, "compute", props.Category)
- assert.Equal(t, "aws", props.Provider)
- assert.Equal(t, "123456789", props.Account)
- assert.Equal(t, "my-project", props.Project)
- assert.Equal(t, "ec2", props.Service)
- assert.Equal(t, "prod-cluster", props.Cluster)
- assert.Equal(t, "worker-node-1", props.Name)
- assert.Equal(t, "i-1234567890abcdef0", props.ProviderID)
- }
- // Test cloud cost properties
- func TestCloudCostProperties(t *testing.T) {
- props := CloudCostProperties{
- ProviderID: "i-1234567890abcdef0",
- Provider: "aws",
- AccountID: "123456789",
- AccountName: "my-account",
- InvoiceEntityID: "entity-123",
- InvoiceEntityName: "My Company",
- RegionID: "us-east-1",
- AvailabilityZone: "us-east-1a",
- Service: "ec2",
- Category: "compute",
- Labels: map[string]string{
- "environment": "production",
- "team": "platform",
- },
- }
- assert.Equal(t, "i-1234567890abcdef0", props.ProviderID)
- assert.Equal(t, "aws", props.Provider)
- assert.Equal(t, "123456789", props.AccountID)
- assert.Equal(t, "my-account", props.AccountName)
- assert.Equal(t, "entity-123", props.InvoiceEntityID)
- assert.Equal(t, "My Company", props.InvoiceEntityName)
- assert.Equal(t, "us-east-1", props.RegionID)
- assert.Equal(t, "us-east-1a", props.AvailabilityZone)
- assert.Equal(t, "ec2", props.Service)
- assert.Equal(t, "compute", props.Category)
- assert.Equal(t, "production", props.Labels["environment"])
- assert.Equal(t, "platform", props.Labels["team"])
- }
- // Test cost metric
- func TestCostMetric(t *testing.T) {
- metric := CostMetric{
- Cost: 100.0,
- KubernetesPercent: 80.0,
- }
- assert.Equal(t, 100.0, metric.Cost)
- assert.Equal(t, 80.0, metric.KubernetesPercent)
- }
- // Test time window
- func TestTimeWindow(t *testing.T) {
- now := time.Now()
- window := TimeWindow{
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- assert.True(t, window.Start.Before(window.End))
- assert.Equal(t, 24*time.Hour, window.End.Sub(window.Start))
- }
- // Test node overhead
- func TestNodeOverhead(t *testing.T) {
- overhead := NodeOverhead{
- RamOverheadFraction: 0.1,
- CpuOverheadFraction: 0.05,
- OverheadCostFraction: 0.15,
- }
- assert.Equal(t, 0.1, overhead.RamOverheadFraction)
- assert.Equal(t, 0.05, overhead.CpuOverheadFraction)
- assert.Equal(t, 0.15, overhead.OverheadCostFraction)
- }
- // Test asset breakdown
- func TestAssetBreakdown(t *testing.T) {
- breakdown := AssetBreakdown{
- Idle: 10.0,
- Other: 5.0,
- System: 15.0,
- User: 70.0,
- }
- assert.Equal(t, 10.0, breakdown.Idle)
- assert.Equal(t, 5.0, breakdown.Other)
- assert.Equal(t, 15.0, breakdown.System)
- assert.Equal(t, 70.0, breakdown.User)
- }
- // Test cloud cost summary
- func TestCloudCostSummary(t *testing.T) {
- summary := CloudCostSummary{
- TotalNetCost: 1000.0,
- TotalAmortizedCost: 950.0,
- TotalInvoicedCost: 1100.0,
- KubernetesPercent: 85.0,
- ProviderBreakdown: map[string]float64{
- "aws": 800.0,
- "gcp": 200.0,
- },
- ServiceBreakdown: map[string]float64{
- "ec2": 600.0,
- "s3": 200.0,
- "rds": 200.0,
- },
- RegionBreakdown: map[string]float64{
- "us-east-1": 600.0,
- "us-west-2": 400.0,
- },
- }
- assert.Equal(t, 1000.0, summary.TotalNetCost)
- assert.Equal(t, 950.0, summary.TotalAmortizedCost)
- assert.Equal(t, 1100.0, summary.TotalInvoicedCost)
- assert.Equal(t, 85.0, summary.KubernetesPercent)
- assert.Equal(t, 800.0, summary.ProviderBreakdown["aws"])
- assert.Equal(t, 200.0, summary.ProviderBreakdown["gcp"])
- assert.Equal(t, 600.0, summary.ServiceBreakdown["ec2"])
- assert.Equal(t, 200.0, summary.ServiceBreakdown["s3"])
- assert.Equal(t, 600.0, summary.RegionBreakdown["us-east-1"])
- assert.Equal(t, 400.0, summary.RegionBreakdown["us-west-2"])
- }
- // Test default values
- func TestAllocationQueryDefaultValues(t *testing.T) {
- query := AllocationQuery{}
- // Test default values
- assert.Equal(t, time.Duration(0), query.Step)
- assert.False(t, query.Accumulate)
- assert.False(t, query.ShareIdle)
- assert.Empty(t, query.Aggregate)
- assert.False(t, query.IncludeIdle)
- assert.False(t, query.IdleByNode)
- assert.False(t, query.IncludeProportionalAssetResourceCosts)
- assert.False(t, query.IncludeAggregatedMetadata)
- assert.False(t, query.ShareLB)
- }
- func TestCloudCostQueryDefaultValues(t *testing.T) {
- query := CloudCostQuery{}
- // Test default values
- assert.Empty(t, query.Aggregate)
- assert.Empty(t, query.Accumulate)
- assert.Empty(t, query.Filter)
- assert.Empty(t, query.Provider)
- assert.Empty(t, query.Service)
- assert.Empty(t, query.Category)
- assert.Empty(t, query.Region)
- assert.Empty(t, query.AccountID)
- }
- // Test edge cases
- func TestEdgeCases(t *testing.T) {
- t.Run("zero duration step", func(t *testing.T) {
- query := AllocationQuery{
- Step: 0,
- }
- assert.Equal(t, time.Duration(0), query.Step)
- })
- t.Run("negative duration step", func(t *testing.T) {
- query := AllocationQuery{
- Step: -1 * time.Hour,
- }
- assert.Equal(t, -1*time.Hour, query.Step)
- })
- t.Run("very large duration step", func(t *testing.T) {
- query := AllocationQuery{
- Step: 365 * 24 * time.Hour, // 1 year
- }
- assert.Equal(t, 365*24*time.Hour, query.Step)
- })
- t.Run("empty aggregate string", func(t *testing.T) {
- query := AllocationQuery{
- Aggregate: "",
- }
- assert.Empty(t, query.Aggregate)
- })
- t.Run("comma separated aggregate", func(t *testing.T) {
- query := AllocationQuery{
- Aggregate: "namespace,cluster,node",
- }
- assert.Equal(t, "namespace,cluster,node", query.Aggregate)
- })
- }
- // dummyQuerier captures the last QueryRequest it received
- type dummyQuerier struct {
- last cloudcost.QueryRequest
- }
- func (dq *dummyQuerier) Query(_ context.Context, req cloudcost.QueryRequest) (*opencost.CloudCostSetRange, error) {
- dq.last = req
- // Return empty set range
- ccsr, _ := opencost.NewCloudCostSetRange(time.Now().Add(-24*time.Hour), time.Now(), opencost.AccumulateOptionDay, "")
- return ccsr, nil
- }
- func TestBuildCloudCostQueryRequest_AccumulateParsing(t *testing.T) {
- s := &MCPServer{}
- req := cloudcost.QueryRequest{}
- params := &CloudCostQuery{
- Aggregate: "provider,service",
- Accumulate: "week",
- }
- out := s.buildCloudCostQueryRequest(req, params)
- assert.Equal(t, []string{"provider", "service"}, out.AggregateBy)
- assert.NotEqual(t, opencost.AccumulateOptionNone, out.Accumulate)
- }
- func TestBuildCloudCostQueryRequest_FilterString(t *testing.T) {
- s := &MCPServer{}
- req := cloudcost.QueryRequest{}
- params := &CloudCostQuery{
- Filter: `provider:"gcp" and service:"Compute Engine"`,
- }
- out := s.buildCloudCostQueryRequest(req, params)
- assert.NotNil(t, out.Filter)
- }
- func TestBuildFilterFromParams_SupportedFieldsOnly(t *testing.T) {
- s := &MCPServer{}
- params := &CloudCostQuery{
- Provider: "gcp",
- ProviderID: "cluster-1",
- Service: "Compute Engine",
- Category: "compute",
- AccountID: "acct-123",
- InvoiceEntityID: "inv-456",
- Region: "us-central1", // intentionally set; ignored by builder
- Labels: map[string]string{
- "goog-k8s-cluster-name": "cluster-1",
- },
- }
- f := s.buildFilterFromParams(params)
- assert.NotNil(t, f)
- }
- func TestBuildFilterFromParams_LabelOnly(t *testing.T) {
- s := &MCPServer{}
- params := &CloudCostQuery{
- Labels: map[string]string{"environment": "prod"},
- }
- f := s.buildFilterFromParams(params)
- assert.NotNil(t, f)
- }
- func TestQueryCloudCosts_QuerierCapture(t *testing.T) {
- dq := &dummyQuerier{}
- s := &MCPServer{cloudQuerier: dq}
- req := &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "5d",
- CloudCostParams: &CloudCostQuery{
- Aggregate: "provider,service",
- Accumulate: "week",
- Provider: "gcp",
- },
- }
- _, err := s.QueryCloudCosts(context.Background(), req)
- require.NoError(t, err)
- assert.Equal(t, []string{"provider", "service"}, dq.last.AggregateBy)
- assert.NotEqual(t, opencost.AccumulateOptionNone, dq.last.Accumulate)
- }
- // ---- Tests for MCP server end-to-end behavior ----
- func TestProcessMCPRequest_CloudCostDispatch(t *testing.T) {
- dq := &dummyQuerier{}
- s := &MCPServer{cloudQuerier: dq}
- req := &MCPRequest{
- Query: &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "3d",
- CloudCostParams: &CloudCostQuery{
- Aggregate: "provider",
- Accumulate: "day",
- Provider: "gcp",
- },
- },
- }
- resp, err := s.ProcessMCPRequest(context.Background(), req)
- require.NoError(t, err)
- require.NotNil(t, resp)
- require.NotNil(t, resp.Data)
- }
- func TestProcessMCPRequest_UnsupportedType(t *testing.T) {
- s := &MCPServer{}
- req := &MCPRequest{
- Query: &OpenCostQueryRequest{
- QueryType: QueryType("unknown"),
- Window: "1d",
- },
- }
- _, err := s.ProcessMCPRequest(context.Background(), req)
- require.Error(t, err)
- }
- func TestProcessMCPRequest_ValidationError(t *testing.T) {
- s := &MCPServer{}
- // Missing window
- req := &MCPRequest{
- Query: &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "",
- },
- }
- _, err := s.ProcessMCPRequest(context.Background(), req)
- require.Error(t, err)
- }
- // ---- Additional comprehensive tests for missing functionality ----
- func TestNewMCPServer(t *testing.T) {
- costModel := &costmodel.CostModel{}
- provider := &mockProvider{}
- cloudQuerier := &dummyQuerier{}
- server := NewMCPServer(costModel, provider, cloudQuerier)
- require.NotNil(t, server)
- assert.Equal(t, costModel, server.costModel)
- assert.Equal(t, provider, server.provider)
- assert.Equal(t, cloudQuerier, server.cloudQuerier)
- }
- // Mock provider for testing
- type mockProvider struct{}
- func (mp *mockProvider) GetConfig() (*models.CustomPricing, error) { return nil, nil }
- func (mp *mockProvider) AllNodePricing() (interface{}, error) { return nil, nil }
- func (mp *mockProvider) ClusterInfo() (map[string]string, error) { return nil, nil }
- func (mp *mockProvider) GetAddresses() ([]byte, error) { return nil, nil }
- func (mp *mockProvider) GetDisks() ([]byte, error) { return nil, nil }
- func (mp *mockProvider) GetOrphanedResources() ([]models.OrphanedResource, error) { return nil, nil }
- func (mp *mockProvider) NodePricing(models.Key) (*models.Node, models.PricingMetadata, error) {
- return nil, models.PricingMetadata{}, nil
- }
- func (mp *mockProvider) GpuPricing(map[string]string) (string, error) { return "", nil }
- func (mp *mockProvider) PVPricing(models.PVKey) (*models.PV, error) { return nil, nil }
- func (mp *mockProvider) NetworkPricing() (*models.Network, error) { return nil, nil }
- func (mp *mockProvider) LoadBalancerPricing() (*models.LoadBalancer, error) { return nil, nil }
- func (mp *mockProvider) DownloadPricingData() error { return nil }
- func (mp *mockProvider) GetKey(map[string]string, *clustercache.Node) models.Key { return nil }
- func (mp *mockProvider) GetPVKey(*clustercache.PersistentVolume, map[string]string, string) models.PVKey {
- return nil
- }
- func (mp *mockProvider) UpdateConfig(io.Reader, string) (*models.CustomPricing, error) {
- return nil, nil
- }
- func (mp *mockProvider) UpdateConfigFromConfigMap(map[string]string) (*models.CustomPricing, error) {
- return nil, nil
- }
- func (mp *mockProvider) GetManagementPlatform() (string, error) { return "", nil }
- func (mp *mockProvider) ApplyReservedInstancePricing(map[string]*models.Node) {}
- func (mp *mockProvider) ServiceAccountStatus() *models.ServiceAccountStatus { return nil }
- func (mp *mockProvider) PricingSourceStatus() map[string]*models.PricingSource { return nil }
- func (mp *mockProvider) ClusterManagementPricing() (string, float64, error) { return "", 0, nil }
- func (mp *mockProvider) CombinedDiscountForNode(string, bool, float64, float64) float64 { return 0 }
- func (mp *mockProvider) Regions() []string { return nil }
- func (mp *mockProvider) PricingSourceSummary() interface{} { return nil }
- func TestGenerateQueryID(t *testing.T) {
- // Test that generateQueryID returns a non-empty string
- id1 := generateQueryID()
- id2 := generateQueryID()
- assert.NotEmpty(t, id1)
- assert.NotEmpty(t, id2)
- assert.NotEqual(t, id1, id2) // Should be different each time
- assert.Contains(t, id1, "query-")
- }
- func TestTransformAllocationSet_NilInput(t *testing.T) {
- result := transformAllocationSet(nil)
- require.NotNil(t, result)
- assert.NotNil(t, result.Allocations)
- assert.Len(t, result.Allocations, 0)
- }
- func TestTransformAllocationSet_EmptyInput(t *testing.T) {
- emptySet := &opencost.AllocationSet{
- Allocations: map[string]*opencost.Allocation{},
- }
- result := transformAllocationSet(emptySet)
- require.NotNil(t, result)
- assert.Contains(t, result.Allocations, "allocations")
- assert.Len(t, result.Allocations["allocations"].Allocations, 0)
- }
- func TestTransformAssetSet_NilInput(t *testing.T) {
- result := transformAssetSet(nil)
- require.NotNil(t, result)
- assert.NotNil(t, result.Assets)
- assert.Len(t, result.Assets, 0)
- }
- func TestTransformAssetSet_EmptyInput(t *testing.T) {
- emptySet := &opencost.AssetSet{
- Assets: map[string]opencost.Asset{},
- }
- result := transformAssetSet(emptySet)
- require.NotNil(t, result)
- assert.Contains(t, result.Assets, "assets")
- assert.Len(t, result.Assets["assets"].Assets, 0)
- }
- func TestBuildFilterFromParams_EmptyParams(t *testing.T) {
- s := &MCPServer{}
- params := &CloudCostQuery{}
- filter := s.buildFilterFromParams(params)
- assert.Nil(t, filter)
- }
- func TestBuildFilterFromParams_RegionIgnored(t *testing.T) {
- s := &MCPServer{}
- params := &CloudCostQuery{
- Region: "us-east-1", // Should be ignored
- }
- filter := s.buildFilterFromParams(params)
- assert.Nil(t, filter) // Should return nil since only region is set
- }
- func TestBuildFilterFromParams_EmptyLabelKey(t *testing.T) {
- s := &MCPServer{}
- params := &CloudCostQuery{
- Labels: map[string]string{
- "": "value1", // Empty key should be ignored
- "valid": "value2",
- },
- }
- filter := s.buildFilterFromParams(params)
- assert.NotNil(t, filter)
- }
- func TestBuildCloudCostQueryRequest_EmptyParams(t *testing.T) {
- s := &MCPServer{}
- req := cloudcost.QueryRequest{}
- params := &CloudCostQuery{}
- result := s.buildCloudCostQueryRequest(req, params)
- assert.Equal(t, req, result) // Should return unchanged request
- }
- func TestBuildCloudCostQueryRequest_InvalidFilterString(t *testing.T) {
- s := &MCPServer{}
- req := cloudcost.QueryRequest{}
- params := &CloudCostQuery{
- Filter: "invalid filter syntax !!!",
- }
- result := s.buildCloudCostQueryRequest(req, params)
- // Should not panic and should return request with nil filter
- assert.Nil(t, result.Filter)
- }
- func TestQueryCloudCosts_NilCloudQuerier(t *testing.T) {
- s := &MCPServer{cloudQuerier: nil}
- req := &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "24h",
- }
- _, err := s.QueryCloudCosts(context.Background(), req)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "cloud cost querier not configured")
- }
- func TestQueryCloudCosts_InvalidWindow(t *testing.T) {
- s := &MCPServer{cloudQuerier: &dummyQuerier{}}
- req := &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "invalid-window",
- }
- _, err := s.QueryCloudCosts(context.Background(), req)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "failed to parse window")
- }
- func TestQueryAssets_InvalidWindow(t *testing.T) {
- s := &MCPServer{}
- req := &OpenCostQueryRequest{
- QueryType: AssetQueryType,
- Window: "invalid-window",
- }
- _, err := s.QueryAssets(req)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "failed to parse window")
- }
- func TestQueryAllocations_InvalidWindow(t *testing.T) {
- s := &MCPServer{}
- req := &OpenCostQueryRequest{
- QueryType: AllocationQueryType,
- Window: "invalid-window",
- }
- _, err := s.QueryAllocations(req)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "failed to parse window")
- }
- func TestProcessMCPRequest_ResponseMetadata(t *testing.T) {
- dq := &dummyQuerier{}
- s := &MCPServer{cloudQuerier: dq}
- req := &MCPRequest{
- Query: &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "1h",
- },
- }
- resp, err := s.ProcessMCPRequest(context.Background(), req)
- require.NoError(t, err)
- require.NotNil(t, resp)
- // Check response metadata
- assert.NotEmpty(t, resp.QueryInfo.QueryID)
- assert.NotZero(t, resp.QueryInfo.Timestamp)
- assert.Greater(t, resp.QueryInfo.ProcessingTime, time.Duration(0))
- }
- func TestCloudCostQuery_NewFields(t *testing.T) {
- query := CloudCostQuery{
- InvoiceEntityID: "entity-123",
- ProviderID: "provider-456",
- Labels: map[string]string{
- "environment": "prod",
- "team": "platform",
- },
- }
- assert.Equal(t, "entity-123", query.InvoiceEntityID)
- assert.Equal(t, "provider-456", query.ProviderID)
- assert.Equal(t, "prod", query.Labels["environment"])
- assert.Equal(t, "platform", query.Labels["team"])
- }
- // ---- Tests for Efficiency Tool ----
- func TestEfficiencyQueryStruct(t *testing.T) {
- bufferMultiplier := 1.4
- query := EfficiencyQuery{
- Aggregate: "pod",
- Filter: "namespace:production",
- EfficiencyBufferMultiplier: &bufferMultiplier,
- }
- assert.Equal(t, "pod", query.Aggregate)
- assert.Equal(t, "namespace:production", query.Filter)
- assert.NotNil(t, query.EfficiencyBufferMultiplier)
- assert.Equal(t, 1.4, *query.EfficiencyBufferMultiplier)
- }
- func TestEfficiencyQueryDefaultValues(t *testing.T) {
- query := EfficiencyQuery{}
- assert.Empty(t, query.Aggregate)
- assert.Empty(t, query.Filter)
- assert.Nil(t, query.EfficiencyBufferMultiplier)
- }
- func TestEfficiencyMetricStruct(t *testing.T) {
- now := time.Now()
- metric := EfficiencyMetric{
- Name: "test-pod",
- CPUEfficiency: 0.5,
- MemoryEfficiency: 0.6,
- CPUCoresRequested: 2.0,
- CPUCoresUsed: 1.0,
- RAMBytesRequested: 2147483648, // 2GB
- RAMBytesUsed: 1288490188, // ~1.2GB
- RecommendedCPURequest: 1.2,
- RecommendedRAMRequest: 1546188226, // ~1.44GB
- ResultingCPUEfficiency: 0.833,
- ResultingMemoryEfficiency: 0.833,
- CurrentTotalCost: 10.0,
- RecommendedCost: 6.0,
- CostSavings: 4.0,
- CostSavingsPercent: 40.0,
- EfficiencyBufferMultiplier: 1.2,
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- assert.Equal(t, "test-pod", metric.Name)
- assert.Equal(t, 0.5, metric.CPUEfficiency)
- assert.Equal(t, 0.6, metric.MemoryEfficiency)
- assert.Equal(t, 2.0, metric.CPUCoresRequested)
- assert.Equal(t, 1.0, metric.CPUCoresUsed)
- assert.Equal(t, 2147483648.0, metric.RAMBytesRequested)
- assert.Equal(t, 1288490188.0, metric.RAMBytesUsed)
- assert.Equal(t, 1.2, metric.RecommendedCPURequest)
- assert.Equal(t, 1546188226.0, metric.RecommendedRAMRequest)
- assert.Equal(t, 0.833, metric.ResultingCPUEfficiency)
- assert.Equal(t, 0.833, metric.ResultingMemoryEfficiency)
- assert.Equal(t, 10.0, metric.CurrentTotalCost)
- assert.Equal(t, 6.0, metric.RecommendedCost)
- assert.Equal(t, 4.0, metric.CostSavings)
- assert.Equal(t, 40.0, metric.CostSavingsPercent)
- assert.Equal(t, 1.2, metric.EfficiencyBufferMultiplier)
- assert.True(t, metric.Start.Before(metric.End))
- }
- func TestEfficiencyResponseStruct(t *testing.T) {
- now := time.Now()
- metric1 := &EfficiencyMetric{
- Name: "pod-1",
- CPUEfficiency: 0.5,
- MemoryEfficiency: 0.6,
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- metric2 := &EfficiencyMetric{
- Name: "pod-2",
- CPUEfficiency: 0.7,
- MemoryEfficiency: 0.8,
- Start: now.Add(-24 * time.Hour),
- End: now,
- }
- response := EfficiencyResponse{
- Efficiencies: []*EfficiencyMetric{metric1, metric2},
- }
- require.NotNil(t, response.Efficiencies)
- assert.Len(t, response.Efficiencies, 2)
- assert.Equal(t, "pod-1", response.Efficiencies[0].Name)
- assert.Equal(t, "pod-2", response.Efficiencies[1].Name)
- }
- func TestSafeDiv(t *testing.T) {
- tests := []struct {
- name string
- numerator float64
- denominator float64
- expected float64
- }{
- {"normal division", 10.0, 2.0, 5.0},
- {"zero denominator", 10.0, 0.0, 0.0},
- {"zero numerator", 0.0, 2.0, 0.0},
- {"both zero", 0.0, 0.0, 0.0},
- {"negative values", -10.0, 2.0, -5.0},
- {"fractional result", 5.0, 2.0, 2.5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := safeDiv(tt.numerator, tt.denominator)
- assert.Equal(t, tt.expected, result)
- })
- }
- }
- func TestComputeEfficiencyMetric_NilAllocation(t *testing.T) {
- result := computeEfficiencyMetric(nil, 1.2)
- assert.Nil(t, result)
- }
- func TestComputeEfficiencyMetric_ZeroMinutes(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now,
- End: now, // Same time, so 0 minutes
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- assert.Nil(t, result)
- }
- func TestComputeEfficiencyMetric_ValidAllocation(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- // 24 hours = 1440 minutes
- CPUCoreHours: 24.0, // 1 core for 24 hours
- RAMByteHours: 24.0e9, // ~1GB for 24 hours
- CPUCoreRequestAverage: 2.0, // Requested 2 cores
- RAMBytesRequestAverage: 2.0e9, // Requested 2GB
- CPUCost: 10.0,
- RAMCost: 5.0,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- assert.Equal(t, "test-pod", result.Name)
- assert.Equal(t, 2.0, result.CPUCoresRequested)
- assert.Equal(t, 2.0e9, result.RAMBytesRequested)
- assert.Equal(t, 1.0, result.CPUCoresUsed) // 24 core-hours / 24 hours = 1 core
- assert.Equal(t, 1.0e9, result.RAMBytesUsed) // 24GB-hours / 24 hours = 1GB
- assert.Equal(t, 0.5, result.CPUEfficiency) // 1 / 2 = 0.5
- assert.Equal(t, 0.5, result.MemoryEfficiency) // 1GB / 2GB = 0.5
- assert.Equal(t, 1.2, result.RecommendedCPURequest) // 1 * 1.2 = 1.2
- assert.Equal(t, 1.2e9, result.RecommendedRAMRequest) // 1GB * 1.2 = 1.2GB
- assert.Equal(t, 1.2, result.EfficiencyBufferMultiplier)
- assert.Greater(t, result.CostSavings, 0.0)
- }
- func TestComputeEfficiencyMetric_CustomBufferMultiplier(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 24.0,
- RAMByteHours: 24.0e9,
- CPUCoreRequestAverage: 2.0,
- RAMBytesRequestAverage: 2.0e9,
- CPUCost: 10.0,
- RAMCost: 5.0,
- }
- // Test with 1.4 buffer multiplier (40% headroom)
- result := computeEfficiencyMetric(alloc, 1.4)
- require.NotNil(t, result)
- assert.Equal(t, 1.4, result.RecommendedCPURequest) // 1 * 1.4 = 1.4
- assert.Equal(t, 1.4e9, result.RecommendedRAMRequest) // 1GB * 1.4 = 1.4GB
- assert.Equal(t, 1.4, result.EfficiencyBufferMultiplier)
- // Resulting efficiency should be usage / recommended
- expectedCPUEff := 1.0 / 1.4
- expectedMemEff := 1.0e9 / 1.4e9
- assert.InDelta(t, expectedCPUEff, result.ResultingCPUEfficiency, 0.001)
- assert.InDelta(t, expectedMemEff, result.ResultingMemoryEfficiency, 0.001)
- }
- func TestComputeEfficiencyMetric_MinimumThresholds(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- // Very small usage
- CPUCoreHours: 0.00001, // 0.000000417 cores average
- RAMByteHours: 100, // ~4 bytes average
- CPUCoreRequestAverage: 0.1,
- RAMBytesRequestAverage: 1000,
- CPUCost: 0.001,
- RAMCost: 0.001,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // Should enforce minimum CPU (0.001 cores)
- assert.Equal(t, efficiencyMinCPU, result.RecommendedCPURequest)
- // Should enforce minimum RAM (1MB)
- assert.Equal(t, float64(efficiencyMinRAM), result.RecommendedRAMRequest)
- }
- func TestComputeEfficiencyMetric_NoRequests(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 24.0,
- RAMByteHours: 24.0e9,
- CPUCoreRequestAverage: 0.0, // No requests set
- RAMBytesRequestAverage: 0.0, // No requests set
- CPUCost: 10.0,
- RAMCost: 5.0,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // Efficiency should be 0 when no requests are set
- assert.Equal(t, 0.0, result.CPUEfficiency)
- assert.Equal(t, 0.0, result.MemoryEfficiency)
- // Recommendations should still be calculated based on usage
- assert.Equal(t, 1.2, result.RecommendedCPURequest)
- assert.Equal(t, 1.2e9, result.RecommendedRAMRequest)
- }
- func TestComputeEfficiencyMetric_OverProvisioned(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 12.0, // 0.5 cores average
- RAMByteHours: 12.0e9, // 0.5GB average
- CPUCoreRequestAverage: 4.0, // Requested 4 cores (over-provisioned)
- RAMBytesRequestAverage: 8.0e9, // Requested 8GB (over-provisioned)
- CPUCost: 40.0,
- RAMCost: 20.0,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // Low efficiency due to over-provisioning
- assert.Equal(t, 0.125, result.CPUEfficiency) // 0.5 / 4 = 0.125
- assert.Equal(t, 0.0625, result.MemoryEfficiency) // 0.5GB / 8GB = 0.0625
- // Recommendations should be much lower
- assert.Equal(t, 0.6, result.RecommendedCPURequest) // 0.5 * 1.2 = 0.6
- assert.Equal(t, 0.6e9, result.RecommendedRAMRequest) // 0.5GB * 1.2 = 0.6GB
- // Should have significant cost savings
- assert.Greater(t, result.CostSavings, 0.0)
- assert.Greater(t, result.CostSavingsPercent, 50.0)
- }
- func TestComputeEfficiencyMetric_UnderProvisioned(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 48.0, // 2 cores average
- RAMByteHours: 48.0e9, // 2GB average
- CPUCoreRequestAverage: 1.0, // Requested 1 core (under-provisioned)
- RAMBytesRequestAverage: 1.0e9, // Requested 1GB (under-provisioned)
- CPUCost: 10.0,
- RAMCost: 5.0,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // High efficiency (>100%) due to under-provisioning
- assert.Equal(t, 2.0, result.CPUEfficiency) // 2 / 1 = 2.0
- assert.Equal(t, 2.0, result.MemoryEfficiency) // 2GB / 1GB = 2.0
- // Recommendations should be higher than current requests
- assert.Equal(t, 2.4, result.RecommendedCPURequest) // 2 * 1.2 = 2.4
- assert.Equal(t, 2.4e9, result.RecommendedRAMRequest) // 2GB * 1.2 = 2.4GB
- }
- func TestComputeEfficiencyMetric_CostCalculations(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 24.0,
- RAMByteHours: 24.0e9,
- CPUCoreRequestAverage: 2.0,
- RAMBytesRequestAverage: 2.0e9,
- CPUCost: 10.0, // $10 for CPU
- RAMCost: 5.0, // $5 for RAM
- NetworkCost: 1.0, // $1 for network
- SharedCost: 0.5, // $0.5 shared
- ExternalCost: 0.5, // $0.5 external
- GPUCost: 1.0, // $1 for GPU
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // Current total cost should include all costs
- expectedCurrentCost := 10.0 + 5.0 + 1.0 + 0.5 + 0.5 + 1.0 // = 18.0
- assert.Equal(t, expectedCurrentCost, result.CurrentTotalCost)
- // Recommended cost should be lower due to right-sizing
- assert.Less(t, result.RecommendedCost, result.CurrentTotalCost)
- // Cost savings should be positive
- assert.Greater(t, result.CostSavings, 0.0)
- assert.Equal(t, result.CurrentTotalCost-result.RecommendedCost, result.CostSavings)
- // Cost savings percent should be calculated correctly
- expectedPercent := (result.CostSavings / result.CurrentTotalCost) * 100
- assert.InDelta(t, expectedPercent, result.CostSavingsPercent, 0.001)
- }
- func TestComputeEfficiencyMetric_OtherCostsPreserved(t *testing.T) {
- now := time.Now()
- alloc := &opencost.Allocation{
- Name: "test-pod",
- Start: now.Add(-24 * time.Hour),
- End: now,
- CPUCoreHours: 24.0,
- RAMByteHours: 24.0e9,
- CPUCoreRequestAverage: 2.0,
- RAMBytesRequestAverage: 2.0e9,
- CPUCost: 10.0,
- RAMCost: 5.0,
- NetworkCost: 2.0, // Fixed cost
- SharedCost: 1.0, // Fixed cost
- ExternalCost: 1.0, // Fixed cost
- GPUCost: 0.0,
- }
- result := computeEfficiencyMetric(alloc, 1.2)
- require.NotNil(t, result)
- // The "other costs" (Network, Shared, External, GPU) should be preserved
- // in the recommended cost calculation
- otherCosts := 2.0 + 1.0 + 1.0 + 0.0 // = 4.0
- // CPU and RAM costs should be reduced based on right-sizing
- // Original: 10.0 + 5.0 = 15.0
- // Usage: 1 core + 1GB
- // Recommended: 1.2 cores + 1.2GB
- // Cost is calculated based on REQUESTED amounts (2 cores, 2GB)
- cpuCostPerCoreHour := 10.0 / (2.0 * 24.0) // CPU cost / (requested cores * hours)
- ramCostPerByteHour := 5.0 / (2.0e9 * 24.0) // RAM cost / (requested bytes * hours)
- expectedRecommendedCPUCost := 1.2 * 24.0 * cpuCostPerCoreHour
- expectedRecommendedRAMCost := 1.2e9 * 24.0 * ramCostPerByteHour
- expectedRecommendedTotal := expectedRecommendedCPUCost + expectedRecommendedRAMCost + otherCosts
- assert.InDelta(t, expectedRecommendedTotal, result.RecommendedCost, 0.01)
- }
- func TestQueryEfficiency_InvalidWindow(t *testing.T) {
- s := &MCPServer{}
- req := &OpenCostQueryRequest{
- QueryType: EfficiencyQueryType,
- Window: "invalid-window",
- }
- _, err := s.QueryEfficiency(req)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "failed to parse window")
- }
- func TestQueryEfficiency_DefaultBufferMultiplier(t *testing.T) {
- // Test that default buffer multiplier is 1.2 when not specified
- req := &OpenCostQueryRequest{
- QueryType: EfficiencyQueryType,
- Window: "24h",
- EfficiencyParams: &EfficiencyQuery{
- // EfficiencyBufferMultiplier not set - should default to 1.2
- },
- }
- assert.Nil(t, req.EfficiencyParams.EfficiencyBufferMultiplier)
- }
- func TestQueryEfficiency_CustomBufferMultiplier(t *testing.T) {
- bufferMultiplier := 1.4
- req := &OpenCostQueryRequest{
- QueryType: EfficiencyQueryType,
- Window: "24h",
- EfficiencyParams: &EfficiencyQuery{
- EfficiencyBufferMultiplier: &bufferMultiplier,
- },
- }
- assert.NotNil(t, req.EfficiencyParams.EfficiencyBufferMultiplier)
- assert.Equal(t, 1.4, *req.EfficiencyParams.EfficiencyBufferMultiplier)
- }
- func TestQueryEfficiency_WithFilter(t *testing.T) {
- req := &OpenCostQueryRequest{
- QueryType: EfficiencyQueryType,
- Window: "7d",
- EfficiencyParams: &EfficiencyQuery{
- Aggregate: "pod",
- Filter: "namespace:production",
- },
- }
- assert.Equal(t, "pod", req.EfficiencyParams.Aggregate)
- assert.Equal(t, "namespace:production", req.EfficiencyParams.Filter)
- }
- func TestQueryEfficiency_WithAggregation(t *testing.T) {
- req := &OpenCostQueryRequest{
- QueryType: EfficiencyQueryType,
- Window: "7d",
- EfficiencyParams: &EfficiencyQuery{
- Aggregate: "namespace,controller",
- },
- }
- assert.Equal(t, "namespace,controller", req.EfficiencyParams.Aggregate)
- }
- func TestEfficiencyConstants(t *testing.T) {
- // Test that efficiency constants are defined correctly
- assert.Equal(t, 1.2, efficiencyBufferMultiplier)
- assert.Equal(t, 0.001, efficiencyMinCPU)
- assert.Equal(t, 1024*1024, efficiencyMinRAM)
- }
- func TestEfficiencyQueryType(t *testing.T) {
- assert.Equal(t, QueryType("efficiency"), EfficiencyQueryType)
- }
- // TestTransformCloudCostSetRange_NilPointerHandling verifies that nil pointer dereferences
- // are prevented in transformCloudCostSetRange for issue #3502
- func TestTransformCloudCostSetRange_NilPointerHandling(t *testing.T) {
- now := time.Now().UTC()
- start := now.Add(-24 * time.Hour)
- end := now
- tests := []struct {
- name string
- ccsr *opencost.CloudCostSetRange
- expectedCostCount int
- expectEmpty bool
- }{
- {
- name: "nil CloudCostSetRange",
- ccsr: nil,
- expectEmpty: true,
- },
- {
- name: "nil CloudCostSet in slice",
- ccsr: &opencost.CloudCostSetRange{CloudCostSets: []*opencost.CloudCostSet{nil}},
- expectEmpty: true,
- },
- {
- name: "CloudCostSet with nil Window.Start",
- ccsr: &opencost.CloudCostSetRange{
- CloudCostSets: []*opencost.CloudCostSet{
- {CloudCosts: map[string]*opencost.CloudCost{}, Window: opencost.NewWindow(nil, &end)},
- },
- },
- expectEmpty: true,
- },
- {
- name: "CloudCostSet with nil Window.End",
- ccsr: &opencost.CloudCostSetRange{
- CloudCostSets: []*opencost.CloudCostSet{
- {CloudCosts: map[string]*opencost.CloudCost{}, Window: opencost.NewWindow(&start, nil)},
- },
- },
- expectEmpty: true,
- },
- {
- name: "CloudCost item with nil Window.Start",
- ccsr: &opencost.CloudCostSetRange{
- CloudCostSets: []*opencost.CloudCostSet{
- {
- CloudCosts: map[string]*opencost.CloudCost{
- "cost1": {
- Properties: &opencost.CloudCostProperties{Provider: "aws"},
- Window: opencost.NewWindow(nil, &end),
- NetCost: opencost.CostMetric{Cost: 100.0},
- },
- },
- Window: opencost.NewClosedWindow(start, end),
- },
- },
- },
- expectedCostCount: 0,
- },
- {
- name: "Mixed valid and invalid items",
- ccsr: &opencost.CloudCostSetRange{
- CloudCostSets: []*opencost.CloudCostSet{
- {
- CloudCosts: map[string]*opencost.CloudCost{
- "invalid": nil,
- "valid": {
- Properties: &opencost.CloudCostProperties{Provider: "gcp"},
- Window: opencost.NewClosedWindow(start, end),
- NetCost: opencost.CostMetric{Cost: 200.0, KubernetesPercent: 0.5},
- },
- },
- Window: opencost.NewClosedWindow(start, end),
- },
- },
- },
- expectedCostCount: 1,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Should not panic
- result := transformCloudCostSetRange(tt.ccsr)
- require.NotNil(t, result)
- require.NotNil(t, result.CloudCosts)
- require.NotNil(t, result.Summary)
- if tt.expectEmpty {
- assert.Empty(t, result.CloudCosts)
- assert.Equal(t, 0.0, result.Summary.TotalNetCost)
- } else {
- totalCosts := 0
- for _, set := range result.CloudCosts {
- totalCosts += len(set.CloudCosts)
- }
- assert.Equal(t, tt.expectedCostCount, totalCosts)
- }
- })
- }
- }
- // contextAwareQuerier is a mock querier that checks for context cancellation
- type contextAwareQuerier struct {
- contextWasCancelled bool
- }
- func (caq *contextAwareQuerier) Query(ctx context.Context, req cloudcost.QueryRequest) (*opencost.CloudCostSetRange, error) {
- // Check if context is already cancelled
- select {
- case <-ctx.Done():
- caq.contextWasCancelled = true
- return nil, ctx.Err()
- default:
- // Return empty set range
- ccsr, _ := opencost.NewCloudCostSetRange(time.Now().Add(-24*time.Hour), time.Now(), opencost.AccumulateOptionDay, "")
- return ccsr, nil
- }
- }
- func TestQueryCloudCosts_ContextCancellation(t *testing.T) {
- // Create a context that is already cancelled
- ctx, cancel := context.WithCancel(context.Background())
- cancel() // Cancel immediately
- // Create a context-aware mock querier
- caq := &contextAwareQuerier{}
- s := &MCPServer{cloudQuerier: caq}
- req := &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "1d",
- }
- // Query should fail with context cancelled error
- _, err := s.QueryCloudCosts(ctx, req)
- // Verify context cancellation was detected
- assert.Error(t, err)
- assert.True(t, caq.contextWasCancelled, "Context cancellation should be detected by querier")
- assert.ErrorIs(t, err, context.Canceled, "Error should be context.Canceled")
- }
- func TestProcessMCPRequest_ContextPropagation(t *testing.T) {
- // Test that context is properly propagated through ProcessMCPRequest
- ctx := context.Background()
- dq := &dummyQuerier{}
- s := &MCPServer{cloudQuerier: dq}
- req := &MCPRequest{
- Query: &OpenCostQueryRequest{
- QueryType: CloudCostQueryType,
- Window: "1d",
- },
- }
- resp, err := s.ProcessMCPRequest(ctx, req)
- require.NoError(t, err)
- require.NotNil(t, resp)
- // Verify that the querier was called (context was propagated)
- assert.NotNil(t, dq.last)
- }
|