| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464 |
- 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(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(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(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(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(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(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(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)
- }
- })
- }
- }
|