Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015143912-740dfbd72456
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261015143912-740dfbd72456
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261015143912-740dfbd72456
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261015143912-740dfbd72456
github.com/aws/aws-lambda-go v1.47.0
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4
github.com/aws/aws-sdk-go-v2/service/lambda v1.89.0
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4 h1:cXoxVl0G696HrsXGc0t2jKH5iHUQe+i9pL/q4dnFPSc=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012205319-ad67030387e4/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4 h1:PCPhtB3yJsLdg6zitDgNxgntHGq3unUn0NoyV1u2PwY=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261012205319-ad67030387e4/go.mod h1:fiu6WQmUGRdViiOf45QV235Cowq8j0yn8ahGWCGEFn8=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4 h1:dbkTxvqS1xWTvCSKulDWL0UIuQSZ1JTBRw14OW/U+wU=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261012205319-ad67030387e4/go.mod h1:8vqO2ZlDzjX/Z4bESZVp/IenhTifkySeF0VJwQKihLA=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4 h1:T3woCHq7rBuExSd0+BI1BYfF0Pd5AMgpngnc8Bqepd0=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261012205319-ad67030387e4/go.mod h1:J4H0b34EwQQ4p9cM3amsIU9tFmbzecS8E00ybjVp56g=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015143912-740dfbd72456 h1:ZPVVIwrZ28I+f+Fe6DUK6g/nVGIJj06nNhI32M7rt9I=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015143912-740dfbd72456/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261015143912-740dfbd72456 h1:3lNYgah7tF+bnOXv619jgiSguUhpiI2I2sFmLrj7Ba8=
github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261015143912-740dfbd72456/go.mod h1:uOUuTVRjTm0Fhu/Vra2M2NES0w4pvZUSfZLsGyCd18s=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261015143912-740dfbd72456 h1:agVjEMOqGsehx98Lb4IyIPlCzf6VIBNHjGekUfCxkpU=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20261015143912-740dfbd72456/go.mod h1:8vqO2ZlDzjX/Z4bESZVp/IenhTifkySeF0VJwQKihLA=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261015143912-740dfbd72456 h1:+pjt8F5bHFHz9OFYEOXUESdIE1KVSfdQH3URqsjaIfw=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20261015143912-740dfbd72456/go.mod h1:KV+Ov04+ZMBGv1wA0s9lEHLinlg1p74XrNafXYLwKmE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
Expand Down
18 changes: 18 additions & 0 deletions internal/config/store_postgres_ladder_basis_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,21 @@ func TestLadderNetting_NullBasisStillFailsLoudBeforeBuilder(t *testing.T) {
require.Error(t, err)
require.False(t, built)
}

func TestLadderAmend_CommitmentRowKeepsNativeAmountAfterReload(t *testing.T) {
f := newCeilingFixture(t)
_, ids := f.plan(fptr(10), fptr(0), fptr(100), 5)
f.setBasis(ids[0], "commitment", "1/2")
require.NoError(t, f.amend(ids[0], 4))
var amount float64
var basis, rate string
require.NoError(t, f.store.db.QueryRow(f.ctx, "SELECT amount_usd_hr,amount_basis,commitment_rate FROM ladder_tranches WHERE id=$1", ids[0]).Scan(&amount, &basis, &rate))
require.Equal(t, 4.0, amount, "the amend stores the native commitment amount, not an on-demand conversion")
require.Equal(t, "commitment", basis)
require.Equal(t, "1/2", rate)
rows, err := inFlightLadderRows(f.ctx, f.store.db, true, f.cfgID, time.Now())
require.NoError(t, err)
require.Len(t, rows, 1)
require.Equal(t, 4.0, rows[0].CommitmentUSDPerHour)
require.Equal(t, 8.0, mustOnDemand(t, rows))
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (f *completenessHTTP) Do(req *http.Request) (*http.Response, error) {
case "AWSInsightsIndexService.GetReservationPurchaseRecommendation":
allowed := map[string]bool{
"Amazon Elastic Compute Cloud - Compute": true, "Amazon Relational Database Service": true,
"Amazon ElastiCache": true, "Amazon OpenSearch Service": true, "Amazon Redshift": true,
"Amazon ElastiCache": true, "Amazon OpenSearch Service": true, "Amazon Redshift": true, "Amazon MemoryDB Service": true,
}
if !allowed[input.Service] {
f.unexpected.Add(1)
Expand Down Expand Up @@ -393,7 +393,8 @@ func assertSavingsPlansPersistence(t *testing.T, rows []config.RecommendationRec
require.NotNil(t, row.SavingsPercentage)
require.Equal(t, 25.0, *row.SavingsPercentage)
require.NotNil(t, row.MonthlyCost)
monthly := 1460.0
// Fixture: $2/hr commitment, $3 upfront. Recurring = (2 x 8760 x years - 3) / (12 x years).
monthly := (2.0*8760*float64(row.Term) - 3) / (12 * float64(row.Term))
if row.Payment == "all-upfront" {
monthly = 0
}
Expand Down
2 changes: 1 addition & 1 deletion internal/scheduler/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ func TestScheduler_CollectAzureRecommendations_AllAccountsFailLoud(t *testing.T)
require.Error(t, err, "collectAzureRecommendations must fail loud when the only account has unusable credentials (COR-03)")
assert.Contains(t, err.Error(), "Azure: all 1 accounts failed",
"the per-account fan-out must surface the all-accounts-failed shape so the scheduler keeps stale rows and records last_collection_error")
assert.Contains(t, err.Error(), "all 4 Azure recommendation services failed")
assert.Contains(t, err.Error(), "all 5 Azure recommendation services failed")
assert.Contains(t, err.Error(), fixtureMarker)
assert.Positive(t, atomic.LoadInt32(&calls), "the managed-identity fixture must receive token requests")
assert.Nil(t, recs)
Expand Down
101 changes: 60 additions & 41 deletions internal/server/handler_ladder.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,10 @@ func (app *Application) processOneLadderConfig(
// have not yet been bought. Tranches completed after `now` are counted too (see
// below): a safe under-buy, and a deliberate deviation from go's InFlight doc.
//
// Units: tranche amounts are stored exactly as Allocate emits them, which is
// ON-DEMAND-EQUIVALENT $/hr until Allocate converts to commitment dollars
// (go#349). Do not convert here. The store nets each stored row to
// on-demand-equivalent dollars by its own basis and rate.
// Units: Allocate sizes the gap in on-demand-equivalent dollars and emits
// tranches in commitment dollars with the rate they were sized with. Rows are
// stored as basis=commitment plus that rate; the store nets each row back to
// on-demand-equivalent dollars by its own rate.
//
// Persistence is APPEND-ONLY (see persistLadderRun): every run inserts its new
// scheduled tranches and NEVER supersedes existing ones. Netting already
Expand Down Expand Up @@ -317,6 +317,18 @@ func (app *Application) executeLadderRun(
return fmt.Errorf("GetLayerStates: %w", err)
}

// Rates are provider data, fetched outside the transaction. No capability or
// a fetch error fails the run: the allocator never gets a guessed rate. An
// empty map is valid and yields a Hold-only run.
rater, ok := capability.(pkgladder.CommitmentRateCapability)
if !ok {
return fmt.Errorf("capability %T does not provide commitment rates", capability)
}
rates, err := rater.GetCommitmentRates(ctx, scope, term, paymentOpt)
if err != nil {
return fmt.Errorf("GetCommitmentRates: %w", err)
}

// L5 netting runs inside the persist transaction (PlanAndSaveLadderRun):
// the store reads the config's in-flight commitment (scheduled tranches
// still within config.LadderFireGrace plus fired, unresolved ones) under
Expand All @@ -327,7 +339,7 @@ func (app *Application) executeLadderRun(
// provider reads above stay outside the transaction: no lock is held across
// network I/O.
in := &ladderRunPlanner{
dbCfg: dbCfg, engineCfg: engineCfg, baseline: baseline, layerStates: layerStates,
dbCfg: dbCfg, engineCfg: engineCfg, baseline: baseline, layerStates: layerStates, rates: rates,
layers: capability.SupportedLayers(), term: term, paymentOpt: paymentOpt, now: now,
}
savedRun, err := app.Config.PlanAndSaveLadderRun(ctx, dbCfg.ID, now, in.plan)
Expand All @@ -347,6 +359,7 @@ type ladderRunPlanner struct {
engineCfg pkgladder.LadderConfig
baseline pkgladder.UsageBaseline
layerStates map[pkgladder.LayerType]pkgladder.LayerState
rates map[pkgladder.LayerType]float64
layers []pkgladder.LayerSpec
term pkgladder.Term
paymentOpt pkgladder.PaymentOption
Expand All @@ -356,18 +369,20 @@ type ladderRunPlanner struct {
}

func (p *ladderRunPlanner) plan(rows []config.LadderInFlightRow) (*config.LadderRunDB, []config.LadderTrancheDB, error) {
inFlight, err := config.LadderInFlightOnDemand(rows)
if err != nil {
return nil, nil, fmt.Errorf("net in-flight tranches: %w", err)
// Allocate rejects a row on a layer outside Layers, so it cannot be silently un-netted.
inFlight := make([]pkgladder.InFlightTranche, len(rows))
for i, r := range rows {
inFlight[i] = pkgladder.InFlightTranche{Layer: r.Layer, CommitmentUSDPerHour: r.CommitmentUSDPerHour, Rate: r.Rate}
}
allocResult, err := pkgladder.Allocate(&pkgladder.AllocationInput{
Now: p.now,
Baseline: p.baseline,
LayerStates: p.layerStates,
Layers: p.layers,
Config: p.engineCfg,
DataSources: []string{dataSourceAWSCostExplorer},
InFlightUSDPerHour: &inFlight,
Now: p.now,
Baseline: p.baseline,
LayerStates: p.layerStates,
Layers: p.layers,
Config: p.engineCfg,
DataSources: []string{dataSourceAWSCostExplorer},
InFlight: inFlight,
CommitmentRates: p.rates,
})
if err != nil {
return nil, nil, fmt.Errorf("allocate: %w", err)
Expand Down Expand Up @@ -539,8 +554,8 @@ func ladderConfigToEngine(dbCfg *config.LadderConfigDB, accountID string) (pkgla
// allocation rather than recomputed independently:
// - target = baseline low-water * targetCoveragePct/100 (the coverage goal),
// - existing = sum of per-layer ExistingUSDPerHour,
// - gap = sum of the planned allocation gaps (what is actually being placed
// this run), NOT target - existing.
// - gap = sum of the planned allocations' on-demand-equivalent gaps (the unit
// of target and existing), NOT target - existing.
//
// When the baseline low-water is nil the run is a Hold-only no-op (Allocate
// returns no allocations), so target and gap stay nil (never 0-coerced);
Expand Down Expand Up @@ -600,9 +615,10 @@ func assembleLadderPlan(
target = new(big.Rat).Mul(low, pctFrac)
}
}
// gap is what is actually being placed this run (Σ allocation gaps),
// consistent with total_hourly_commit, not target - existing.
gap = sumAllocationGaps(allocResult.Allocations)
// gap is what is being placed this run in on-demand-equivalent dollars,
// so it compares with target and existing; total_hourly_commit is the
// same placement in commitment dollars.
gap = sumAllocationOnDemandGaps(allocResult.Allocations)
}

return &pkgladder.LadderPlan{
Expand Down Expand Up @@ -638,26 +654,24 @@ func ratToFloat64Ptr(r *big.Rat) *float64 {
return &f
}

// sumAllocationGaps sums the GapUSDPerHour across all allocations as an exact
// *big.Rat. It returns a zero (non-nil) rat for an empty allocation set: a
// sumAllocationOnDemandGaps sums the on-demand-equivalent gap across all
// allocations as an exact *big.Rat, the unit of target and existing. It returns a zero (non-nil) rat for an empty allocation set: a
// Hold-only run plans exactly $0 of new commitment, which is a meaningful
// value here (what is being placed), not "not computed".
func sumAllocationGaps(allocs []pkgladder.Allocation) *big.Rat {
func sumAllocationOnDemandGaps(allocs []pkgladder.Allocation) *big.Rat {
sum := new(big.Rat)
for _, a := range allocs {
if a.GapUSDPerHour != nil {
sum.Add(sum, a.GapUSDPerHour)
if a.OnDemandEquivalentUSDPerHour != nil {
sum.Add(sum, a.OnDemandEquivalentUSDPerHour)
}
}
return sum
}

// sumTrancheRowAmounts sums the amounts of the tranche rows a run persists.
// This is the non-nullable total_hourly_commit stored in the run row; it is 0
// for a no-op (Hold-only) plan, which is the correct semantic (zero new
// commitment, not "not computed"). Units are whatever Allocate emits:
// on-demand-equivalent $/hr until the commitment-dollar conversion lands
// (go#349); this code stores amounts unchanged.
// This is the non-nullable total_hourly_commit stored in the run row, in
// commitment dollars; it is 0 for a no-op (Hold-only) plan, which is the
// correct semantic (zero new commitment, not "not computed").
func sumTrancheRowAmounts(rows []config.LadderTrancheDB) float64 {
var sum float64
for i := range rows {
Expand Down Expand Up @@ -707,18 +721,23 @@ func buildTrancheDBRows(tranches []pkgladder.Tranche, runID string, configID *st
}
amountUSDHr, _ := r.Float64()
runIDCopy := runID
basis := config.LadderAmountOnDemand // The pinned pre-go349 SDK emits OD amounts.
if tr.CommitmentRate == "" {
return nil, fmt.Errorf("tranche %s: empty commitment rate", tr.ID)
}
basis := config.LadderAmountCommitment
rate := tr.CommitmentRate
rows = append(rows, config.LadderTrancheDB{
ID: tr.ID,
ConfigID: configID,
RunID: &runIDCopy,
LayerType: tr.Layer,
Term: tr.Term,
PaymentOption: tr.PaymentOption,
Status: pkgladder.TrancheStatusScheduled,
AmountUSDHr: amountUSDHr,
AmountBasis: &basis,
ScheduledDate: tr.FireAfter,
ID: tr.ID,
ConfigID: configID,
RunID: &runIDCopy,
LayerType: tr.Layer,
Term: tr.Term,
PaymentOption: tr.PaymentOption,
Status: pkgladder.TrancheStatusScheduled,
AmountUSDHr: amountUSDHr,
AmountBasis: &basis,
CommitmentRate: &rate,
ScheduledDate: tr.FireAfter,
})
}
return rows, nil
Expand Down
2 changes: 1 addition & 1 deletion internal/server/handler_ladder_lock_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TestLadderLock_MixedBasisReachesActualPlanner(t *testing.T) {
require.Equal(t, 5.0, total)
var basis string
require.NoError(t, e.conn.QueryRow(ctx, "SELECT amount_basis FROM ladder_tranches WHERE run_id=$1 LIMIT 1", id).Scan(&basis))
require.Equal(t, string(od), basis)
require.Equal(t, string(native), basis)
}

func TestLadderLock_UnknownBasisPersistsNoRun(t *testing.T) {
Expand Down
99 changes: 99 additions & 0 deletions internal/server/handler_ladder_rates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package server

import (
"encoding/json"
"errors"
"testing"
"time"

pkgladder "github.com/LeanerCloud/cloud-commitments-go/pkg/ladder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/LeanerCloud/cloud-commitments-platform/internal/config"
"github.com/LeanerCloud/cloud-commitments-platform/internal/testutil"
)

var ratesTestNow = time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)

// runWithRates plans one run (low-water 12.5 -> target 10, one base allocation
// of 10 on-demand-equivalent) and returns the store and the error.
func runWithRates(t *testing.T, store *ladderTestStore, cap pkgladder.LadderCapability) error {
t.Helper()
dbCfg := validTestDBConfig("cfg-rates")
app := &Application{Config: store}
return app.executeLadderRun(testutil.TestContext(t), &dbCfg, cap, "123456789012", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, ratesTestNow)
}

func TestExecuteLadderRun_RateHalfPersistsCommitmentDollars(t *testing.T) {
store := &ladderTestStore{}
cap := &fakeLadderCapability{t: t, baseline: testBaselineLowWater(12.5),
rates: map[pkgladder.LayerType]float64{pkgladder.LayerConvertibleRI: 0.5}}
require.NoError(t, runWithRates(t, store, cap))

require.Len(t, store.savedTranches, 1)
tr := store.savedTranches[0]
assert.InDelta(t, 5.0, tr.AmountUSDHr, 1e-9, "10 on-demand at rate 1/2 is 5 commitment")
require.NotNil(t, tr.AmountBasis)
assert.Equal(t, config.LadderAmountCommitment, *tr.AmountBasis)
require.NotNil(t, tr.CommitmentRate)
assert.Equal(t, "1/2", *tr.CommitmentRate)

run := store.savedRun
assert.InDelta(t, 5.0, run.TotalHourlyCommit, 1e-9, "total_hourly_commit stays commitment dollars")
require.NotNil(t, run.GapUSDHr)
assert.InDelta(t, 10.0, *run.GapUSDHr, 1e-9, "gap_usd_hr is on-demand-equivalent, comparable with target")
}

func TestExecuteLadderRun_PlanJSONGapIsOnDemand(t *testing.T) {
store := &ladderTestStore{}
cap := &fakeLadderCapability{t: t, baseline: testBaselineLowWater(12.5),
rates: map[pkgladder.LayerType]float64{pkgladder.LayerConvertibleRI: 0.5}}
require.NoError(t, runWithRates(t, store, cap))
var dto ladderPlanJSONDTO
require.NoError(t, json.Unmarshal(store.savedRun.Plan, &dto))
require.NotNil(t, dto.GapUSDHr)
assert.InDelta(t, 10.0, *dto.GapUSDHr, 1e-9)
}

func TestExecuteLadderRun_NoRateCapabilityIsErrored(t *testing.T) {
store := &ladderTestStore{}
cap := &fakeLadderCapability{t: t, baseline: testBaseline()}
// Embedding the interface hides GetCommitmentRates.
err := runWithRates(t, store, struct{ pkgladder.LadderCapability }{cap})
require.ErrorContains(t, err, "commitment rates")
assert.Nil(t, store.savedRun)
}

func TestExecuteLadderRun_RateFetchErrorIsErrored(t *testing.T) {
store := &ladderTestStore{}
cap := &fakeLadderCapability{t: t, baseline: testBaseline(), ratesErr: errors.New("ce throttled")}
err := runWithRates(t, store, cap)
require.ErrorContains(t, err, "ce throttled")
assert.Nil(t, store.savedRun)
}

func TestExecuteLadderRun_EmptyRateMapIsHoldOnlyAndPersists(t *testing.T) {
store := &ladderTestStore{}
cap := &fakeLadderCapability{t: t, baseline: testBaselineLowWater(12.5), rates: map[pkgladder.LayerType]float64{}}
require.NoError(t, runWithRates(t, store, cap))
require.NotNil(t, store.savedRun)
assert.Empty(t, store.savedTranches)
assert.Zero(t, store.savedRun.TotalHourlyCommit)
}

func TestExecuteLadderRun_InFlightOnUnsupportedLayerFailsLoud(t *testing.T) {
store := &ladderTestStore{inFlightRows: []config.LadderInFlightRow{{Layer: pkgladder.LayerComputeSP, CommitmentUSDPerHour: 1, Rate: 1}}}
cap := &fakeLadderCapability{t: t, baseline: testBaseline()}
err := runWithRates(t, store, cap)
require.ErrorContains(t, err, "not in the supported layers")
assert.Nil(t, store.savedRun)
}

func TestBuildTrancheDBRows_EmptyCommitmentRateErrors(t *testing.T) {
cfgID := "cfg"
_, err := buildTrancheDBRows([]pkgladder.Tranche{{
ID: "tr-norate", Layer: pkgladder.LayerEC2InstanceSP, AmountUSDPerHour: "5", FireAfter: ratesTestNow,
}}, "run", &cfgID)
require.ErrorContains(t, err, "tr-norate")
}
Loading
Loading