mirror of
https://github.com/coder/coder.git
synced 2026-11-01 04:47:17 +08:00
Populate `FeatureAgentRuntimeHours.Actual` on every entitlements refresh for licenses that grant the feature. A new `GetTotalUsageHBAgentRuntimeV1` query sums `runtime_ms` over the license's usage period, reading `usage_events` directly: `hb_agent_runtime_v1` is exactly one row per hourly bucket deployment-wide with `created_at` at the bucket start, enforced by the unique partial index introduced in #27983. The measurement reuses the shared `measureUsage` policy from #27984 through a new `AgentRuntimeMsFn` closure (usage publisher subject): failures publish the stable `LicenseAgentRuntimeUsageUnavailableErrorText` and log the cause. Usage is floored to whole hours, matching the unit of the `agent_runtime_hours_*` claims, and at most one warning is emitted per refresh: reaching the allocation supersedes the advisory soft limit. The dashboard renders the soft-limit advisory muted without a sales link and treats the runtime usage-unavailable text as a diagnostic. **Precise usage.** `Feature.ActualMs` (JSON `actual_ms`), set only for `agent_runtime_hours`, carries the exact stored milliseconds backing the floored `Actual` so clients can render fractional hours (e.g. `10.3`). It has the same freshness as `Actual`; the whole-hour warning thresholds are unchanged. **Unlimited licenses.** A license minted with the unlimited (`-1`) allocation decodes to an enabled feature with a nil `Limit` (#27984), so the warning write-back now guards the allocation dereference: no thresholds can exist for an unlimited license, so no runtime hours warning is ever emitted, while `Actual` is still measured and published. `Feature.Compare` is unchanged; for usage-period features the issued-at/end dates decide first, so a metered feature outranks an unlimited one only on an exact timestamp tie, an edge pinned by a `TestFeatureComparison` case and documented on `decodeAgentRuntimeHours`. **Grandfathered premium licenses.** Premium licenses without `agent_runtime_hours_*` claims are now granted the feature disabled with a zero limit over the license term, identical to an explicit `allocation: 0`: usage is measured and published for every Premium deployment, and chatd's pooled admission (#27902) caps concurrent agentic chats until a license with a positive allocation is added. The default carries a fixed early `UsagePeriod.IssuedAt` (2026-08-01, the same mechanism as the managed-agents default) so any license actually carrying the claims outranks it in the `AddFeature` merge regardless of the licenses' relative issue dates; the constant must stay earlier than the earliest legitimately issued claim-bearing license. Zero allocations (explicit or grandfathered) emit no deployment-wide warning banner: those deployments are steered by the in-page upgrade CTA and the concurrency cap. Enterprise licenses are unchanged. Part 3 of a 3-PR stack splitting up #27796 (see there for review history). Stack: #27983 → #27984 → this PR. Closes CODAGT-852.
51 lines
2.2 KiB
Go
51 lines
2.2 KiB
Go
package codersdk_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// TestLicenseAgentRuntimeHoursWarningTexts pins the warning-text prefix
|
|
// couplings consumed by the dashboard's LicenseBanner
|
|
// (site/src/modules/dashboard/LicenseBanner).
|
|
func TestLicenseAgentRuntimeHoursWarningTexts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Cut rather than Split so a template losing its placeholder fails the
|
|
// test instead of silently turning the whole message into the "prefix".
|
|
templatePrefix := func(text, placeholder string) string {
|
|
t.Helper()
|
|
prefix, _, ok := strings.Cut(text, placeholder)
|
|
require.True(t, ok, "template %q must contain placeholder %q", text, placeholder)
|
|
return prefix
|
|
}
|
|
|
|
aiGovNearLimitPrefix := templatePrefix(codersdk.LicenseAIGovernance90PercentWarningText, "%d%%")
|
|
aiGovOverLimitPrefix := templatePrefix(codersdk.LicenseAIGovernanceOverLimitWarningText, "%d")
|
|
softLimitPrefix := templatePrefix(codersdk.LicenseAgentRuntimeHoursSoftLimitWarningText, "%d")
|
|
|
|
runtimeTexts := map[string]string{
|
|
"SoftLimit": codersdk.LicenseAgentRuntimeHoursSoftLimitWarningText,
|
|
"AllocationReached": codersdk.LicenseAgentRuntimeHoursAllocationReachedWarningText,
|
|
}
|
|
for name, text := range runtimeTexts {
|
|
// isMutedWarning renders near-limit matches muted, and
|
|
// isAIGovernanceWarning matches either AI Governance prefix to
|
|
// suppress the banner's client-side over-limit fallback.
|
|
require.False(t, strings.HasPrefix(text, aiGovNearLimitPrefix),
|
|
"%s warning must not share the AI Governance near-limit prefix %q", name, aiGovNearLimitPrefix)
|
|
require.False(t, strings.HasPrefix(text, aiGovOverLimitPrefix),
|
|
"%s warning must not share the AI Governance over-limit prefix %q", name, aiGovOverLimitPrefix)
|
|
}
|
|
|
|
// isMutedWarning renders soft-limit matches muted and messageLink drops
|
|
// their sales link, so the allocation-reached warning must not match.
|
|
allocationReachedText := codersdk.LicenseAgentRuntimeHoursAllocationReachedWarningText
|
|
require.False(t, strings.HasPrefix(allocationReachedText, softLimitPrefix),
|
|
"the soft-limit prefix must not classify the allocation-reached warning")
|
|
}
|