test(coverage): agentexec codecs and licensing service branch coverage

Add purely-additive branch-coverage tests for freshly-landed, previously-
untested backend surface:
- internal/agentexec: docker container update codec (decode/validate/bind/
  digest/result-cross-validation), residual host apt codec validation arms
  (host update + storage cleanup decode/bind/result/operation-query identity),
  and NormalizeDeployMaxParallel clamping.
- pkg/licensing: service helpers (unionFeatures, safeIntFromInt64,
  remainingDaysCeil, ensureGracePeriodEnd, Set/Get Evaluator+StateMachine,
  IsValid, CurrentUnsafeForTesting, dev-mode) and activation GrantEnvelope
  ParseExpiresAt + uncapped-core-monitoring tier classification.
No source changed.
This commit is contained in:
rcourtman
2026-07-14 14:45:55 +01:00
parent f50bcce2dc
commit e1985528d9
5 changed files with 2073 additions and 0 deletions
@@ -0,0 +1,748 @@
package agentexec
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/operationreceipt"
)
// --- JSON / identity helpers for coverage tests ---
func covHostUpdateDigest(actionID, operation string, opVersion int, hash string) string {
d, err := hostUpdateRequestDigest(HostUpdatePayload{
ActionID: actionID,
Operation: operation,
OperationVersion: opVersion,
ExpectedInventoryHash: hash,
})
if err != nil {
panic(err)
}
return d
}
func covHostStorageCleanupDigest(actionID, operation string, opVersion int, fingerprint string) string {
d, err := hostStorageCleanupRequestDigest(HostStorageCleanupPayload{
ActionID: actionID,
Operation: operation,
OperationVersion: opVersion,
ExpectedFingerprint: fingerprint,
})
if err != nil {
panic(err)
}
return d
}
func covBuildHostUpdatePayloadJSON(t *testing.T, requestID, actionID, operation string, opVersion int, hash string, timeout int) []byte {
t.Helper()
raw, err := json.Marshal(map[string]any{
"request_id": requestID,
"action_id": actionID,
"operation": operation,
"operation_version": opVersion,
"request_digest": covHostUpdateDigest(actionID, operation, opVersion, hash),
"expected_inventory_hash": hash,
"timeout": timeout,
})
if err != nil {
t.Fatal(err)
}
return raw
}
func covBuildHostStorageCleanupPayloadJSON(t *testing.T, requestID, actionID, operation string, opVersion int, fingerprint string, timeout int) []byte {
t.Helper()
raw, err := json.Marshal(map[string]any{
"request_id": requestID,
"action_id": actionID,
"operation": operation,
"operation_version": opVersion,
"request_digest": covHostStorageCleanupDigest(actionID, operation, opVersion, fingerprint),
"expected_fingerprint": fingerprint,
"timeout": timeout,
})
if err != nil {
t.Fatal(err)
}
return raw
}
func covWithJSONField(t *testing.T, raw []byte, key string, value any) []byte {
t.Helper()
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
m[key] = value
out, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return out
}
func covHostUpdateIdentity(agentID string) operationreceipt.Identity {
req := HostUpdatePayload{
RequestID: "cov.dispatch.1",
ActionID: "cov-action",
Operation: HostUpdateOperationInstall,
ExpectedInventoryHash: "sha256:" + strings.Repeat("a", 64),
}
if err := BindHostUpdatePayload(&req); err != nil {
panic(err)
}
return HostUpdateOperationIdentity(agentID, req)
}
func covHostCleanupIdentity(agentID string) operationreceipt.Identity {
req := HostStorageCleanupPayload{
RequestID: "cov.dispatch.1",
ActionID: "cov-action",
Operation: HostStorageCleanupOperationPackageCache,
ExpectedFingerprint: "sha256:" + strings.Repeat("c", 64),
}
if err := BindHostStorageCleanupPayload(&req); err != nil {
panic(err)
}
return HostStorageCleanupOperationIdentity(agentID, req)
}
func covTerminalRecord(identity operationreceipt.Identity, resultKind string, resultVersion int, result json.RawMessage, terminalAt time.Time) *operationreceipt.Record {
return &operationreceipt.Record{
Identity: identity,
State: operationreceipt.StateTerminal,
AcceptedAt: terminalAt.Add(-10 * time.Minute),
StartedAt: terminalAt.Add(-9 * time.Minute),
TerminalAt: terminalAt,
ResultKind: resultKind,
ResultVersion: resultVersion,
Result: result,
}
}
func covAcceptedRecord(identity operationreceipt.Identity, acceptedAt time.Time) *operationreceipt.Record {
return &operationreceipt.Record{
Identity: identity,
State: operationreceipt.StateAccepted,
AcceptedAt: acceptedAt,
}
}
// ---------------------------------------------------------------------------
// DecodeHostUpdatePayload — apt_codec.go:32
// ---------------------------------------------------------------------------
func TestCoverageDecodeHostUpdatePayloadRejectionArms(t *testing.T) {
hash := "sha256:" + strings.Repeat("a", 64)
valid := covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30)
// Success case.
t.Run("valid payload decodes", func(t *testing.T) {
got, err := DecodeHostUpdatePayload(valid)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.RequestID != "r1" || got.ActionID != "a1" || got.Operation != HostUpdateOperationInstall {
t.Fatalf("decoded payload mismatch: %#v", got)
}
})
for _, tc := range []struct {
name string
body []byte
wantSub string
}{
{"empty payload", []byte(""), "empty"},
{"whitespace only", []byte(" \n\t "), "empty"},
{"malformed JSON", []byte(`{"request_id":"incomplete`), ""},
{"unknown field rejected", covWithJSONField(t, valid, "rogue_field", true), "unknown field"},
{"trailing JSON", append(append([]byte{}, valid...), '{', '}'), "trailing"},
{"missing request id", covBuildHostUpdatePayloadJSON(t, "", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30), "request id is required"},
{"empty action id", covBuildHostUpdatePayloadJSON(t, "r1", "", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30), "action id is required"},
{"unsupported operation", covBuildHostUpdatePayloadJSON(t, "r1", "a1", "bogus_op", HostAPTOperationVersion, hash, 30), "unsupported host update operation"},
{"unsupported operation version", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, 99, hash, 30), "unsupported host update operation version"},
{"request digest mismatch", covWithJSONField(t, valid, "request_digest", "sha256:"+strings.Repeat("0", 64)), "digest mismatch"},
{"invalid inventory hash", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, "not-a-hash", 30), "expected inventory hash"},
{"negative timeout", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, -1), "timeout"},
{"timeout exceeds maximum", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 9999), "timeout"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := DecodeHostUpdatePayload(tc.body)
if err == nil {
t.Fatal("expected error, got nil")
}
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
}
})
}
}
// ---------------------------------------------------------------------------
// DecodeHostStorageCleanupPayload — apt_codec.go:54
// ---------------------------------------------------------------------------
func TestCoverageDecodeHostStorageCleanupPayloadRejectionArms(t *testing.T) {
fp := "sha256:" + strings.Repeat("c", 64)
valid := covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30)
t.Run("valid payload decodes", func(t *testing.T) {
got, err := DecodeHostStorageCleanupPayload(valid)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.RequestID != "r1" || got.ActionID != "a1" || got.Operation != HostStorageCleanupOperationPackageCache {
t.Fatalf("decoded payload mismatch: %#v", got)
}
})
for _, tc := range []struct {
name string
body []byte
wantSub string
}{
{"empty payload", []byte(""), "empty"},
{"whitespace only", []byte("\t\n "), "empty"},
{"malformed JSON", []byte(`{"request_id": invalid`), ""},
{"unknown field rejected", covWithJSONField(t, valid, "rogue_field", 42), "unknown field"},
{"trailing JSON", append(append([]byte{}, valid...), '{', '}'), "trailing"},
{"missing request id", covBuildHostStorageCleanupPayloadJSON(t, "", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30), "invalid request id"},
{"empty action id", covBuildHostStorageCleanupPayloadJSON(t, "r1", "", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30), "invalid action id"},
{"unsupported operation", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", "bogus_cleanup", HostAPTOperationVersion, fp, 30), "unsupported host storage cleanup operation"},
{"unsupported operation version", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, 77, fp, 30), "unsupported host storage cleanup operation version"},
{"request digest mismatch", covWithJSONField(t, valid, "request_digest", "sha256:"+strings.Repeat("9", 64)), "digest mismatch"},
{"invalid fingerprint", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, "garbage-fp", 30), "expected cleanup fingerprint"},
{"negative timeout", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, -1), "timeout"},
{"timeout exceeds maximum", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 9999), "timeout"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := DecodeHostStorageCleanupPayload(tc.body)
if err == nil {
t.Fatal("expected error, got nil")
}
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
}
})
}
}
// ---------------------------------------------------------------------------
// ValidateOperationQueryResultForIdentity — apt_codec.go:98
// ---------------------------------------------------------------------------
func TestCoverageValidateOperationQueryResultForIdentity(t *testing.T) {
now := time.Now().UTC()
updateIdentity := covHostUpdateIdentity("agent-1")
cleanupIdentity := covHostCleanupIdentity("agent-1")
// A mismatched identity for the identity-mismatch arm.
otherIdentity := updateIdentity
otherIdentity.ActionID = "different-action"
// A bare identity with an unsupported operation kind (still normalisable).
bogusIdentity := operationreceipt.Identity{
AttemptID: "cov.dispatch.1",
ActionID: "cov-action",
OperationKind: "bogus_operation",
OperationVersion: 1,
RequestDigest: "sha256:" + strings.Repeat("a", 64),
AgentID: "agent-1",
}
// Valid not-found result (success path: nil record, no error).
notFoundOK := operationreceipt.QueryResult{Version: operationreceipt.ProtocolVersion, Status: operationreceipt.QueryNotFound}
// Not-found with a stray record.
notFoundWithRecord := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryNotFound,
Record: covAcceptedRecord(updateIdentity, now),
}
// Unsupported version.
badVersion := operationreceipt.QueryResult{Version: 99, Status: operationreceipt.QueryNotFound}
// Found-terminal but nil record.
nilRecord := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
}
// Identity mismatch (record identity differs).
mismatchResult := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundInterrupted,
Record: covAcceptedRecord(otherIdentity, now),
}
// ValidateRecord failure (zero AcceptedAt).
badRecord := &operationreceipt.Record{
Identity: updateIdentity,
State: operationreceipt.StateAccepted,
AcceptedAt: time.Time{},
}
validateRecordFail := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundInterrupted,
Record: badRecord,
}
// Unknown status (neither terminal nor interrupted).
unknownStatus := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: "bogus_status",
Record: covAcceptedRecord(updateIdentity, now),
}
// Interrupted status with a terminal-state record (state mismatch).
interruptedTerminal := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundInterrupted,
Record: covTerminalRecord(updateIdentity, "any", 1, json.RawMessage(`{}`), now),
}
// Terminal status with a non-terminal-state record.
terminalNonTerminalRecord := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covAcceptedRecord(updateIdentity, now),
}
// Valid interrupted result (success path).
validInterrupted := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundInterrupted,
Record: covAcceptedRecord(updateIdentity, now),
}
// Terminal but receivedAt is zero.
zeroReceivedAt := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(updateIdentity, HostUpdateReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{}`), now),
}
// Host update envelope mismatch (wrong ResultKind).
updateEnvelopeMismatch := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(updateIdentity, "wrong.kind", HostAPTReceiptVersion, json.RawMessage(`{}`), now),
}
// Host update decode error (correct envelope, malformed payload).
updateDecodeError := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(updateIdentity, HostUpdateReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{"rogue":true}`), now),
}
// Host storage cleanup envelope mismatch.
cleanupEnvelopeMismatch := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(cleanupIdentity, "wrong.kind", HostAPTReceiptVersion, json.RawMessage(`{}`), now),
}
// Host storage cleanup decode error.
cleanupDecodeError := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(cleanupIdentity, HostStorageCleanupReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{"rogue":true}`), now),
}
// Unsupported operation kind (default switch arm).
unsupportedKind := operationreceipt.QueryResult{
Version: operationreceipt.ProtocolVersion,
Status: operationreceipt.QueryFoundTerminal,
Record: covTerminalRecord(bogusIdentity, "any.kind", 1, json.RawMessage(`{}`), now),
}
for _, tc := range []struct {
name string
result operationreceipt.QueryResult
identity operationreceipt.Identity
receivedAt time.Time
wantSub string
wantOK bool
}{
{"valid not-found returns nil", notFoundOK, updateIdentity, now, "", true},
{"valid interrupted returns nil", validInterrupted, updateIdentity, now, "", true},
{"unsupported version", badVersion, updateIdentity, now, "unsupported operation query result version", false},
{"not-found with record", notFoundWithRecord, updateIdentity, now, "not-found operation query result contains a record", false},
{"nil record identity mismatch", nilRecord, updateIdentity, now, "identity mismatch", false},
{"record identity mismatch", mismatchResult, updateIdentity, now, "identity mismatch", false},
{"validate record failure", validateRecordFail, updateIdentity, now, "invalid operation receipt bounds", false},
{"unknown status mismatch", unknownStatus, updateIdentity, now, "status and record state mismatch", false},
{"interrupted with terminal state", interruptedTerminal, updateIdentity, now, "status and record state mismatch", false},
{"terminal with non-terminal record", terminalNonTerminalRecord, updateIdentity, now, "terminal operation query result requires terminal record", false},
{"zero receivedAt rejected", zeroReceivedAt, updateIdentity, time.Time{}, "invalid or implausibly future", false},
{"host update envelope mismatch", updateEnvelopeMismatch, updateIdentity, now, "host update query result envelope mismatch", false},
{"host update decode error", updateDecodeError, updateIdentity, now, "", false},
{"host cleanup envelope mismatch", cleanupEnvelopeMismatch, cleanupIdentity, now, "host cleanup query result envelope mismatch", false},
{"host cleanup decode error", cleanupDecodeError, cleanupIdentity, now, "", false},
{"unsupported operation kind", unsupportedKind, bogusIdentity, now, "unsupported operation query kind", false},
} {
t.Run(tc.name, func(t *testing.T) {
err := ValidateOperationQueryResultForIdentity(tc.result, tc.identity, tc.receivedAt)
if tc.wantOK {
if err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
return
}
if err == nil {
t.Fatal("expected error, got nil")
}
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
}
})
}
}
// ---------------------------------------------------------------------------
// BindHostUpdatePayload — apt_codec.go:193
// ---------------------------------------------------------------------------
func TestCoverageBindHostUpdatePayload(t *testing.T) {
t.Run("nil payload rejected", func(t *testing.T) {
if err := BindHostUpdatePayload(nil); err == nil || !strings.Contains(err.Error(), "host update payload is required") {
t.Fatalf("nil payload error=%v", err)
}
})
t.Run("valid payload binds version and digest", func(t *testing.T) {
hash := "sha256:" + strings.Repeat("a", 64)
req := &HostUpdatePayload{
RequestID: "bind-1",
ActionID: "act-1",
Operation: HostUpdateOperationInstall,
ExpectedInventoryHash: hash,
}
if err := BindHostUpdatePayload(req); err != nil {
t.Fatalf("bind error: %v", err)
}
if req.OperationVersion != HostAPTOperationVersion {
t.Fatalf("operation version = %d, want %d", req.OperationVersion, HostAPTOperationVersion)
}
wantDigest := covHostUpdateDigest("act-1", HostUpdateOperationInstall, HostAPTOperationVersion, hash)
if req.RequestDigest != wantDigest {
t.Fatalf("request digest = %q, want %q", req.RequestDigest, wantDigest)
}
})
}
// ---------------------------------------------------------------------------
// BindHostStorageCleanupPayload — apt_codec.go:215
// ---------------------------------------------------------------------------
func TestCoverageBindHostStorageCleanupPayload(t *testing.T) {
t.Run("nil payload rejected", func(t *testing.T) {
if err := BindHostStorageCleanupPayload(nil); err == nil || !strings.Contains(err.Error(), "host storage cleanup payload is required") {
t.Fatalf("nil payload error=%v", err)
}
})
t.Run("valid payload binds version and digest", func(t *testing.T) {
fp := "sha256:" + strings.Repeat("c", 64)
req := &HostStorageCleanupPayload{
RequestID: "bind-2",
ActionID: "act-2",
Operation: HostStorageCleanupOperationPackageCache,
ExpectedFingerprint: fp,
}
if err := BindHostStorageCleanupPayload(req); err != nil {
t.Fatalf("bind error: %v", err)
}
if req.OperationVersion != HostAPTOperationVersion {
t.Fatalf("operation version = %d, want %d", req.OperationVersion, HostAPTOperationVersion)
}
wantDigest := covHostStorageCleanupDigest("act-2", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp)
if req.RequestDigest != wantDigest {
t.Fatalf("request digest = %q, want %q", req.RequestDigest, wantDigest)
}
})
}
// ---------------------------------------------------------------------------
// ValidateHostUpdateResultPayload — apt_codec.go:245
// ---------------------------------------------------------------------------
func TestCoverageValidateHostUpdateResultPayload(t *testing.T) {
now := time.Now().UTC()
hashA := "sha256:" + strings.Repeat("a", 64)
hashB := "sha256:" + strings.Repeat("b", 64)
// inconclusiveBase passes the internal validator and reaches every public
// arm when specific fields are tweaked.
inconclusiveBase := HostUpdateResultPayload{
RequestID: "r1",
ActionID: "a1",
ExecutionPhase: HostUpdatePhaseVerify,
Verification: HostUpdateVerificationInconclusive,
}
// A fully valid verified+complete result for the success arm.
validVerified := HostUpdateResultPayload{
RequestID: "r1",
ActionID: "a1",
Success: true,
ExecutionPhase: HostUpdatePhaseComplete,
MutationStarted: true,
HealthChecked: true,
PackageManagerHealthy: true,
Verification: HostUpdateVerificationVerified,
Before: HostPackageUpdateSnapshot{Supported: true, Manager: "apt", InventoryHash: hashA, PendingCount: 1, CheckedAt: now.Add(-2 * time.Minute)},
After: HostPackageUpdateSnapshot{Supported: true, Manager: "apt", InventoryHash: hashB, PendingCount: 0, CheckedAt: now.Add(-time.Minute)},
}
for _, tc := range []struct {
name string
result HostUpdateResultPayload
wantSub string
wantOK bool
}{
{
name: "valid verified complete accepted",
result: validVerified,
wantOK: true,
},
{
name: "empty action id",
result: func() HostUpdateResultPayload { r := inconclusiveBase; r.ActionID = ""; return r }(),
wantSub: "invalid action id",
},
{
name: "unsupported execution phase",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.ExecutionPhase = "bogus_phase"
return r
}(),
wantSub: "unsupported host update execution phase",
},
{
name: "success in wrong phase",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.Success = true
r.ExecutionPhase = HostUpdatePhasePreflight
return r
}(),
wantSub: "successful host update mutation must be in verify or complete phase",
},
{
name: "evidence timestamps invalid chronology",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.Verification = HostUpdateVerificationFailed
r.Before = HostPackageUpdateSnapshot{CheckedAt: now}
r.After = HostPackageUpdateSnapshot{CheckedAt: time.Time{}}
return r
}(),
wantSub: "evidence-bearing host update observation timestamps are invalid",
},
{
name: "mutation state conflicts with phase",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.MutationStarted = true
r.ExecutionPhase = HostUpdatePhasePreflight
return r
}(),
wantSub: "host update mutation state conflicts with execution phase",
},
{
name: "recovery required without mutation",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.RecoveryRequired = true
return r
}(),
wantSub: "host update recovery requirement conflicts with mutation state",
},
{
name: "healthy package manager without health check",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.PackageManagerHealthy = true
return r
}(),
wantSub: "healthy package manager claim requires a completed health check",
},
{
name: "unhealthy package manager after mutation without recovery",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.HealthChecked = true
r.MutationStarted = true
return r
}(),
wantSub: "unhealthy package manager after mutation requires recovery",
},
{
name: "partial install without recovery",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.ExecutionPhase = HostUpdatePhaseInstall
r.MutationStarted = true
return r
}(),
wantSub: "partial host update install requires recovery",
},
{
name: "successful completion cannot require recovery",
result: func() HostUpdateResultPayload {
r := inconclusiveBase
r.Success = true
r.ExecutionPhase = HostUpdatePhaseComplete
r.MutationStarted = true
r.RecoveryRequired = true
return r
}(),
wantSub: "successful host update completion cannot require recovery",
},
{
name: "verified but not complete",
result: func() HostUpdateResultPayload {
r := validVerified
r.ExecutionPhase = HostUpdatePhaseVerify
return r
}(),
wantSub: "verified host update must be complete",
},
} {
t.Run(tc.name, func(t *testing.T) {
err := ValidateHostUpdateResultPayload(&tc.result)
if tc.wantOK {
if err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
return
}
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.wantSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
}
})
}
}
// ---------------------------------------------------------------------------
// ValidateHostStorageCleanupResultPayload — apt_codec.go:289
// ---------------------------------------------------------------------------
func TestCoverageValidateHostStorageCleanupResultPayload(t *testing.T) {
now := time.Now().UTC()
fpA := "sha256:" + strings.Repeat("a", 64)
fpB := "sha256:" + strings.Repeat("b", 64)
inconclusiveBase := HostStorageCleanupResultPayload{
RequestID: "r1",
ActionID: "a1",
ExecutionPhase: HostStorageCleanupPhaseVerify,
Verification: HostStorageCleanupVerificationInconclusive,
}
validVerified := HostStorageCleanupResultPayload{
RequestID: "r1",
ActionID: "a1",
Success: true,
ExecutionPhase: HostStorageCleanupPhaseComplete,
MutationStarted: true,
Verification: HostStorageCleanupVerificationVerified,
Before: HostStorageCleanupSnapshot{Supported: true, Provider: "apt-package-cache", Fingerprint: fpA, ReclaimableBytes: 100, CheckedAt: now.Add(-2 * time.Minute)},
After: HostStorageCleanupSnapshot{Supported: true, Provider: "apt-package-cache", Fingerprint: fpB, ReclaimableBytes: 10, CheckedAt: now.Add(-time.Minute)},
ReclaimedBytes: 90,
}
for _, tc := range []struct {
name string
result HostStorageCleanupResultPayload
wantSub string
wantOK bool
}{
{
name: "valid verified complete accepted",
result: validVerified,
wantOK: true,
},
{
name: "empty action id",
result: func() HostStorageCleanupResultPayload { r := inconclusiveBase; r.ActionID = ""; return r }(),
wantSub: "invalid action id",
},
{
name: "unsupported execution phase",
result: func() HostStorageCleanupResultPayload {
r := inconclusiveBase
r.ExecutionPhase = "bogus_phase"
return r
}(),
wantSub: "unsupported host storage cleanup execution phase",
},
{
name: "success in wrong phase",
result: func() HostStorageCleanupResultPayload {
r := inconclusiveBase
r.Success = true
r.ExecutionPhase = HostStorageCleanupPhasePreflight
return r
}(),
wantSub: "successful host storage cleanup mutation must be in verify or complete phase",
},
{
name: "verified but not complete",
result: func() HostStorageCleanupResultPayload {
r := validVerified
r.ExecutionPhase = HostStorageCleanupPhaseVerify
return r
}(),
wantSub: "verified host storage cleanup must be complete",
},
{
name: "evidence timestamps invalid chronology",
result: func() HostStorageCleanupResultPayload {
r := inconclusiveBase
r.Verification = HostStorageCleanupVerificationFailed
r.Before = HostStorageCleanupSnapshot{CheckedAt: now}
r.After = HostStorageCleanupSnapshot{CheckedAt: time.Time{}}
return r
}(),
wantSub: "evidence-bearing host storage cleanup observation timestamps are invalid",
},
{
name: "mutation state conflicts with phase",
result: func() HostStorageCleanupResultPayload {
r := inconclusiveBase
r.MutationStarted = true
r.ExecutionPhase = HostStorageCleanupPhasePreflight
return r
}(),
wantSub: "host storage cleanup mutation state conflicts with execution phase",
},
} {
t.Run(tc.name, func(t *testing.T) {
err := ValidateHostStorageCleanupResultPayload(&tc.result)
if tc.wantOK {
if err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
return
}
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.wantSub) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
}
})
}
}
@@ -0,0 +1,838 @@
package agentexec
import (
"encoding/json"
"strings"
"testing"
"time"
)
// This file is a table-driven coverage test for the docker container update
// codec. It exercises every public codec function in docker_update_codec.go
// plus the unexported digest helper, with emphasis on the validation guards,
// the strict-JSON rejection shape (unknown fields / trailing data / malformed
// JSON / empty body), and the request/result cross-validation mismatch
// branches. It is the strict-codec sibling of apt_codec_test.go and asserts
// the same closed-contract rejection behaviour.
var (
testValidUpdateContainerID = "abcdef123456"
testValidUpdateContainerIDB = "0123456789ab"
testValidUpdateImageDigest = "sha256:" + strings.Repeat("a", 64)
testValidUpdateImageDigestAlt = "sha256:" + strings.Repeat("b", 64)
testValidUpdateRequestDigest = "sha256:" + strings.Repeat("c", 64)
)
// newBoundUpdatePayload returns a fully bound, valid DockerContainerUpdatePayload
// suitable for both validation and decode round-trips.
func newBoundUpdatePayload(t *testing.T) DockerContainerUpdatePayload {
t.Helper()
payload := DockerContainerUpdatePayload{
RequestID: "update-request-1",
ActionID: "update-action-1",
Runtime: "docker",
ContainerID: testValidUpdateContainerID,
ExpectedImageDigest: testValidUpdateImageDigest,
Timeout: 600,
}
if err := BindDockerContainerUpdatePayload(&payload); err != nil {
t.Fatalf("bind baseline update payload: %v", err)
}
return payload
}
// newValidUpdateResult returns a result that passes ValidateDockerContainerUpdateResultPayload
// on its own (preflight phase, no completion requirements).
func newValidUpdateResult() DockerContainerUpdateResultPayload {
return DockerContainerUpdateResultPayload{
RequestID: "update-request-1",
ActionID: "update-action-1",
Operation: DockerContainerOperationUpdate,
OperationVersion: DockerContainerUpdateOperationVersion,
RequestDigest: testValidUpdateRequestDigest,
ContainerID: testValidUpdateContainerID,
ExecutionPhase: DockerContainerPhasePreflight,
}
}
// newValidCompleteUpdateResult returns a result in the terminal phase that
// satisfies every completion guard (replacement container, mutation done, no
// error, readback observation).
func newValidCompleteUpdateResult(req DockerContainerUpdatePayload) DockerContainerUpdateResultPayload {
observed := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
return DockerContainerUpdateResultPayload{
RequestID: req.RequestID,
ActionID: req.ActionID,
Operation: req.Operation,
OperationVersion: req.OperationVersion,
RequestDigest: req.RequestDigest,
ContainerID: req.ContainerID,
ExecutionPhase: DockerContainerPhaseComplete,
MutationStarted: true,
MutationCompleted: true,
ReadbackRan: true,
NewContainerID: testValidUpdateContainerIDB,
After: DockerContainerLifecycleSnapshot{
ContainerID: testValidUpdateContainerIDB,
State: "running",
Running: true,
ObservedAt: observed,
},
}
}
// --- DecodeDockerContainerUpdatePayload: strict acceptance & rejection ---
func TestDecodeDockerContainerUpdatePayloadStrictAcceptance(t *testing.T) {
valid := newBoundUpdatePayload(t)
validJSON, err := json.Marshal(valid)
if err != nil {
t.Fatal(err)
}
base := string(validJSON)
for _, tc := range []struct {
name string
body string
wantError string
}{
{
name: "valid happy path decodes",
body: base,
},
{
name: "empty body rejected",
body: "",
wantError: "empty",
},
{
name: "whitespace-only body rejected",
body: " \n\t ",
wantError: "empty",
},
{
name: "malformed json rejected",
body: `{"request_id":"r1","action_id":`,
wantError: "EOF",
},
{
name: "unknown field rejected",
body: strings.TrimSuffix(base, "}") + `,"command":"docker pull"}`,
wantError: "unknown field",
},
{
name: "trailing json rejected",
body: base + `{}`,
wantError: "trailing",
},
{
name: "missing required request id rejected at validate step",
body: rejson(t, valid, "request_id", ""),
wantError: "request or action id",
},
{
name: "non-digest expected image rejected at validate step",
body: rejson(t, mutatePayload(valid, func(p *DockerContainerUpdatePayload) { p.ExpectedImageDigest = "busybox:latest" }), "", ""),
wantError: "expected image digest",
},
} {
t.Run(tc.name, func(t *testing.T) {
decoded, err := DecodeDockerContainerUpdatePayload([]byte(tc.body))
if tc.wantError == "" {
if err != nil {
t.Fatalf("expected decode success, got error: %v", err)
}
if decoded.RequestID != valid.RequestID || decoded.ActionID != valid.ActionID {
t.Fatalf("decoded identity drift: %+v", decoded)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got success: %+v", tc.wantError, decoded)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantError)
}
})
}
}
// --- DecodeDockerContainerUpdateResultPayload: strict acceptance & rejection ---
func TestDecodeDockerContainerUpdateResultPayloadStrictAcceptance(t *testing.T) {
validResult := newValidCompleteUpdateResult(newBoundUpdatePayload(t))
validJSON, err := json.Marshal(validResult)
if err != nil {
t.Fatal(err)
}
base := string(validJSON)
for _, tc := range []struct {
name string
body string
wantError string
}{
{
name: "valid complete result decodes",
body: base,
},
{
name: "empty body rejected",
body: "",
wantError: "empty",
},
{
name: "malformed json rejected",
body: `{"request_id":"r1", oops}`,
wantError: "invalid character",
},
{
name: "unknown field rejected",
body: strings.TrimSuffix(base, "}") + `,"verification":"verified"}`,
wantError: "unknown field",
},
{
name: "trailing json rejected",
body: base + ` {}`,
wantError: "trailing",
},
{
name: "missing request id rejected at validate step",
body: rejson(t, validResult, "request_id", ""),
wantError: "result identity",
},
{
name: "unsupported execution phase rejected at validate step",
body: rejson(t, mutateResult(validResult, func(r *DockerContainerUpdateResultPayload) { r.ExecutionPhase = "frobnicate" }), "", ""),
wantError: "execution phase",
},
} {
t.Run(tc.name, func(t *testing.T) {
decoded, err := DecodeDockerContainerUpdateResultPayload([]byte(tc.body))
if tc.wantError == "" {
if err != nil {
t.Fatalf("expected decode success, got error: %v", err)
}
if decoded.RequestID != validResult.RequestID {
t.Fatalf("decoded identity drift: %+v", decoded)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got success: %+v", tc.wantError, decoded)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantError)
}
})
}
}
// --- BindDockerContainerUpdatePayload ---
func TestBindDockerContainerUpdatePayload(t *testing.T) {
t.Run("nil payload rejected", func(t *testing.T) {
if err := BindDockerContainerUpdatePayload(nil); err == nil {
t.Fatal("nil payload was accepted by bind")
}
})
t.Run("bind stamps operation version and recomputes digest", func(t *testing.T) {
payload := DockerContainerUpdatePayload{
RequestID: "r1",
ActionID: "a1",
Runtime: " Docker ",
ContainerID: " ABCDEF123456 ",
ExpectedImageDigest: " SHA256:" + strings.Repeat("A", 64) + " ",
}
if err := BindDockerContainerUpdatePayload(&payload); err != nil {
t.Fatalf("bind failed: %v", err)
}
if payload.Operation != DockerContainerOperationUpdate {
t.Fatalf("operation not stamped: %q", payload.Operation)
}
if payload.OperationVersion != DockerContainerUpdateOperationVersion {
t.Fatalf("operation version not stamped: %d", payload.OperationVersion)
}
if payload.RequestDigest == "" {
t.Fatal("request digest not stamped")
}
// The bound payload must pass validation once its raw fields are
// trimmed/lowercased (validation normalises the same way the digest
// does, so the stamped digest matches).
if err := ValidateDockerContainerUpdatePayload(&payload); err != nil {
t.Fatalf("bound payload did not validate: %v", err)
}
})
}
// --- dockerContainerUpdateRequestDigest (white-box) ---
func TestDockerContainerUpdateRequestDigest(t *testing.T) {
base := DockerContainerUpdatePayload{
ActionID: "action-1",
Operation: DockerContainerOperationUpdate,
OperationVersion: DockerContainerUpdateOperationVersion,
Runtime: "docker",
ContainerID: testValidUpdateContainerID,
ExpectedImageDigest: testValidUpdateImageDigest,
}
digestA, err := dockerContainerUpdateRequestDigest(base)
if err != nil {
t.Fatalf("digest failed: %v", err)
}
if !strings.HasPrefix(digestA, "sha256:") {
t.Fatalf("digest not sha256-prefixed: %q", digestA)
}
t.Run("deterministic for identical input", func(t *testing.T) {
digestB, err := dockerContainerUpdateRequestDigest(base)
if err != nil {
t.Fatal(err)
}
if digestA != digestB {
t.Fatalf("digest not deterministic: %q vs %q", digestA, digestB)
}
})
t.Run("case and whitespace normalised", func(t *testing.T) {
upper := base
upper.Runtime = " DOCKER "
upper.ContainerID = " ABCDEF123456 "
upper.ExpectedImageDigest = " SHA256:" + strings.Repeat("A", 64) + " "
upper.ActionID = " action-1 "
digestUpper, err := dockerContainerUpdateRequestDigest(upper)
if err != nil {
t.Fatal(err)
}
if digestUpper != digestA {
t.Fatalf("digest not normalised for case/whitespace: %q vs %q", digestUpper, digestA)
}
})
t.Run("action id change alters digest", func(t *testing.T) {
changed := base
changed.ActionID = "action-2"
digestChanged, err := dockerContainerUpdateRequestDigest(changed)
if err != nil {
t.Fatal(err)
}
if digestChanged == digestA {
t.Fatal("action id change did not alter digest")
}
})
}
// --- ValidateDockerContainerUpdatePayload: every branch ---
func TestValidateDockerContainerUpdatePayloadBranches(t *testing.T) {
base := newBoundUpdatePayload(t)
for _, tc := range []struct {
name string
mutate func(*DockerContainerUpdatePayload)
wantError string
}{
{
name: "valid happy path",
mutate: func(*DockerContainerUpdatePayload) {},
},
{
name: "missing request id",
mutate: func(p *DockerContainerUpdatePayload) { p.RequestID = "" },
wantError: "request or action id",
},
{
name: "missing action id",
mutate: func(p *DockerContainerUpdatePayload) { p.ActionID = "" },
wantError: "request or action id",
},
{
name: "oversized request id",
mutate: func(p *DockerContainerUpdatePayload) { p.RequestID = strings.Repeat("x", maxRequestIDLength+1) },
wantError: "request or action id",
},
{
name: "oversized action id",
mutate: func(p *DockerContainerUpdatePayload) { p.ActionID = strings.Repeat("y", maxRequestIDLength+1) },
wantError: "request or action id",
},
{
name: "unsupported operation",
mutate: func(p *DockerContainerUpdatePayload) { p.Operation = DockerContainerOperationRestart },
wantError: "unsupported docker container update operation",
},
{
name: "unsupported operation version",
mutate: func(p *DockerContainerUpdatePayload) { p.OperationVersion = 99 },
wantError: "unsupported docker container update operation version",
},
{
name: "unsupported runtime",
mutate: func(p *DockerContainerUpdatePayload) { p.Runtime = "containerd" },
wantError: "unsupported container runtime",
},
{
name: "podman runtime accepted",
mutate: func(p *DockerContainerUpdatePayload) { p.Runtime = "podman" },
wantError: "", // podman is accepted; bound digest already covers it though, so this needs rebinding
},
{
name: "container id wrong format",
mutate: func(p *DockerContainerUpdatePayload) { p.ContainerID = "xyz123" },
wantError: "immutable hexadecimal id",
},
{
name: "container id too short",
mutate: func(p *DockerContainerUpdatePayload) { p.ContainerID = "abc" },
wantError: "immutable hexadecimal id",
},
{
name: "expected image digest not a digest",
mutate: func(p *DockerContainerUpdatePayload) { p.ExpectedImageDigest = "busybox:latest" },
wantError: "invalid docker update expected image digest",
},
{
name: "expected image digest tampered but still pattern-valid triggers digest mismatch",
mutate: func(p *DockerContainerUpdatePayload) { p.ExpectedImageDigest = testValidUpdateImageDigestAlt },
wantError: "request digest mismatch",
},
{
name: "request digest tampered directly",
mutate: func(p *DockerContainerUpdatePayload) { p.RequestDigest = testValidUpdateImageDigestAlt },
wantError: "request digest mismatch",
},
{
name: "negative timeout rejected",
mutate: func(p *DockerContainerUpdatePayload) { p.Timeout = -1 },
wantError: "timeout must be between 0 and",
},
{
name: "timeout exceeding maximum rejected",
mutate: func(p *DockerContainerUpdatePayload) { p.Timeout = maxDockerContainerUpdateTimeoutSeconds + 1 },
wantError: "timeout must be between 0 and",
},
} {
t.Run(tc.name, func(t *testing.T) {
p := base
tc.mutate(&p)
// podman runtime is accepted; its digest must be rebound for the
// digest-mismatch check to stay quiet so we observe the runtime
// branch's accept path rather than a digest drift.
if p.Runtime == "podman" {
if err := BindDockerContainerUpdatePayload(&p); err != nil {
t.Fatalf("rebind for podman failed: %v", err)
}
}
err := ValidateDockerContainerUpdatePayload(&p)
if tc.wantError == "" {
if err != nil {
t.Fatalf("expected validation success, got: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got success", tc.wantError)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantError)
}
})
}
t.Run("nil payload rejected", func(t *testing.T) {
if err := ValidateDockerContainerUpdatePayload(nil); err == nil {
t.Fatal("nil payload accepted")
}
})
t.Run("zero timeout is defaulted", func(t *testing.T) {
p := base
p.Timeout = 0
if err := ValidateDockerContainerUpdatePayload(&p); err != nil {
t.Fatalf("zero timeout should be defaulted, got: %v", err)
}
if p.Timeout != defaultDockerContainerUpdateTimeoutSeconds {
t.Fatalf("timeout not defaulted: got %d want %d", p.Timeout, defaultDockerContainerUpdateTimeoutSeconds)
}
})
t.Run("exact maximum timeout accepted", func(t *testing.T) {
p := base
p.Timeout = maxDockerContainerUpdateTimeoutSeconds
if err := ValidateDockerContainerUpdatePayload(&p); err != nil {
t.Fatalf("maximum timeout should be accepted, got: %v", err)
}
})
}
// --- ValidateDockerContainerUpdateResultPayload: every branch ---
func TestValidateDockerContainerUpdateResultPayloadBranches(t *testing.T) {
base := newValidCompleteUpdateResult(newBoundUpdatePayload(t))
for _, tc := range []struct {
name string
mutate func(*DockerContainerUpdateResultPayload)
wantError string
}{
{
name: "valid complete happy path",
mutate: func(*DockerContainerUpdateResultPayload) {},
wantError: "",
},
{
name: "missing request id",
mutate: func(r *DockerContainerUpdateResultPayload) { r.RequestID = "" },
wantError: "result identity",
},
{
name: "missing action id",
mutate: func(r *DockerContainerUpdateResultPayload) { r.ActionID = "" },
wantError: "result identity",
},
{
name: "oversized request id",
mutate: func(r *DockerContainerUpdateResultPayload) { r.RequestID = strings.Repeat("x", maxRequestIDLength+1) },
wantError: "result identity",
},
{
name: "unsupported operation",
mutate: func(r *DockerContainerUpdateResultPayload) { r.Operation = DockerContainerOperationRestart },
wantError: "unsupported docker update result operation",
},
{
name: "wrong operation version in binding",
mutate: func(r *DockerContainerUpdateResultPayload) { r.OperationVersion = 99 },
wantError: "invalid docker update result binding",
},
{
name: "bad container id in binding",
mutate: func(r *DockerContainerUpdateResultPayload) { r.ContainerID = "xyz123" },
wantError: "invalid docker update result binding",
},
{
name: "bad request digest format in binding",
mutate: func(r *DockerContainerUpdateResultPayload) { r.RequestDigest = "not-a-digest" },
wantError: "invalid docker update result binding",
},
{
name: "unsupported execution phase",
mutate: func(r *DockerContainerUpdateResultPayload) { r.ExecutionPhase = "frobnicate" },
wantError: "execution phase",
},
{
name: "invalid replacement container id",
mutate: func(r *DockerContainerUpdateResultPayload) { r.NewContainerID = "xyz123" },
wantError: "invalid replacement container id",
},
{
name: "error string exceeds bound",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.Error = strings.Repeat("e", 1025)
r.ExecutionPhase = DockerContainerPhaseMutate
r.MutationCompleted = false
},
wantError: "exceeds bounded contract",
},
{
name: "container name exceeds bound",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.ContainerName = strings.Repeat("n", maxDockerContainerNameLength+1)
},
wantError: "exceeds bounded contract",
},
{
name: "backup container name exceeds bound",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.BackupContainer = strings.Repeat("b", maxDockerContainerNameLength+1)
},
wantError: "exceeds bounded contract",
},
{
name: "old image digest exceeds length bound",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.OldImageDigest = strings.Repeat("o", maxDockerImageDigestLength+1)
},
wantError: "digest exceeds bounded contract",
},
{
name: "new image digest exceeds length bound",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.NewImageDigest = strings.Repeat("m", maxDockerImageDigestLength+1)
},
wantError: "digest exceeds bounded contract",
},
{
name: "mutation completed without mutation started",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.ExecutionPhase = DockerContainerPhaseMutate
r.MutationStarted = false
r.MutationCompleted = true
r.NewContainerID = ""
},
wantError: "completed docker update mutation requires mutation start",
},
{
name: "rolled back without rollback attempted",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.ExecutionPhase = DockerContainerPhaseMutate
r.MutationStarted = true
r.MutationCompleted = false
r.RolledBack = true
r.RollbackAttempted = false
r.NewContainerID = ""
r.Error = "create failed"
},
wantError: "rollback success requires a rollback attempt",
},
{
name: "rollback attempted without mutation started",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.ExecutionPhase = DockerContainerPhaseMutate
r.MutationStarted = false
r.MutationCompleted = false
r.RollbackAttempted = true
r.RolledBack = false
r.NewContainerID = ""
},
wantError: "rollback requires mutation start",
},
{
name: "readback ran without after observation",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.ExecutionPhase = DockerContainerPhaseMutate
r.MutationStarted = true
r.MutationCompleted = false
r.ReadbackRan = true
r.After = DockerContainerLifecycleSnapshot{}
r.NewContainerID = ""
},
wantError: "readback requires an observation",
},
{
name: "complete phase with error set",
mutate: func(r *DockerContainerUpdateResultPayload) { r.Error = "something went wrong" },
wantError: "complete docker update requires a replacement container and no error",
},
{
name: "complete phase without mutation completed",
mutate: func(r *DockerContainerUpdateResultPayload) { r.MutationCompleted = false },
wantError: "complete docker update requires a replacement container and no error",
},
{
name: "complete phase without replacement container",
mutate: func(r *DockerContainerUpdateResultPayload) { r.NewContainerID = "" },
wantError: "complete docker update requires a replacement container and no error",
},
} {
t.Run(tc.name, func(t *testing.T) {
r := base
tc.mutate(&r)
err := ValidateDockerContainerUpdateResultPayload(&r)
if tc.wantError == "" {
if err != nil {
t.Fatalf("expected validation success, got: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got success", tc.wantError)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantError)
}
})
}
t.Run("nil result rejected", func(t *testing.T) {
if err := ValidateDockerContainerUpdateResultPayload(nil); err == nil {
t.Fatal("nil result accepted")
}
})
t.Run("preflight result without completion requirements accepted", func(t *testing.T) {
r := newValidUpdateResult()
if err := ValidateDockerContainerUpdateResultPayload(&r); err != nil {
t.Fatalf("preflight result should be accepted, got: %v", err)
}
})
t.Run("verify phase accepted", func(t *testing.T) {
r := newValidCompleteUpdateResult(newBoundUpdatePayload(t))
r.ExecutionPhase = DockerContainerPhaseVerify
if err := ValidateDockerContainerUpdateResultPayload(&r); err != nil {
t.Fatalf("verify phase should be accepted, got: %v", err)
}
})
}
// --- DockerContainerUpdateOperationIdentity ---
func TestDockerContainerUpdateOperationIdentity(t *testing.T) {
req := newBoundUpdatePayload(t)
identity := DockerContainerUpdateOperationIdentity(" agent-42 ", req)
if identity.AttemptID != req.RequestID {
t.Fatalf("attempt id mismatch: %q vs %q", identity.AttemptID, req.RequestID)
}
if identity.ActionID != req.ActionID {
t.Fatalf("action id mismatch: %q vs %q", identity.ActionID, req.ActionID)
}
if identity.OperationKind != req.Operation {
t.Fatalf("operation kind mismatch: %q vs %q", identity.OperationKind, req.Operation)
}
if identity.OperationVersion != req.OperationVersion {
t.Fatalf("operation version mismatch: %d vs %d", identity.OperationVersion, req.OperationVersion)
}
if identity.RequestDigest != req.RequestDigest {
t.Fatalf("request digest mismatch: %q vs %q", identity.RequestDigest, req.RequestDigest)
}
if identity.AgentID != "agent-42" {
t.Fatalf("agent id not trimmed: %q", identity.AgentID)
}
if identity.AgentID == "" {
t.Fatal("empty agent id should still be passed through (trimmed)")
}
}
// --- ValidateDockerContainerUpdateResultForRequest: every cross-validation branch ---
func TestValidateDockerContainerUpdateResultForRequestBranches(t *testing.T) {
req := newBoundUpdatePayload(t)
t.Run("happy path", func(t *testing.T) {
result := newValidCompleteUpdateResult(req)
if err := ValidateDockerContainerUpdateResultForRequest(req, result); err != nil {
t.Fatalf("expected success, got: %v", err)
}
})
t.Run("invalid request rejected before result checked", func(t *testing.T) {
badReq := req
badReq.RequestID = ""
result := newValidCompleteUpdateResult(req)
err := ValidateDockerContainerUpdateResultForRequest(badReq, result)
if err == nil || !strings.Contains(err.Error(), "request or action id") {
t.Fatalf("expected request-id error, got: %v", err)
}
})
t.Run("invalid result rejected", func(t *testing.T) {
result := newValidCompleteUpdateResult(req)
result.ExecutionPhase = "frobnicate"
err := ValidateDockerContainerUpdateResultForRequest(req, result)
if err == nil || !strings.Contains(err.Error(), "execution phase") {
t.Fatalf("expected execution-phase error, got: %v", err)
}
})
// NOTE: result.Operation and result.OperationVersion mismatch branches
// (line 186 of docker_update_codec.go) cannot be exercised here, because
// ValidateDockerContainerUpdateResultPayload requires Operation ==
// DockerContainerOperationUpdate and OperationVersion ==
// DockerContainerUpdateOperationVersion to even pass; the request's own
// validator enforces the same constants. Those two comparison operands
// are therefore effectively dead in the cross-validator and are noted as
// suspected dead branches in GLM_REPORT.md (not fixed).
for _, tc := range []struct {
name string
mutate func(*DockerContainerUpdateResultPayload)
wantError string
}{
{
name: "request id mismatch",
mutate: func(r *DockerContainerUpdateResultPayload) { r.RequestID = "other-request" },
wantError: "identity mismatch",
},
{
name: "action id mismatch",
mutate: func(r *DockerContainerUpdateResultPayload) { r.ActionID = "other-action" },
wantError: "identity mismatch",
},
{
name: "container mismatch on result",
mutate: func(r *DockerContainerUpdateResultPayload) { r.ContainerID = testValidUpdateContainerIDB },
wantError: "container mismatch",
},
{
name: "after-state container mismatch",
mutate: func(r *DockerContainerUpdateResultPayload) {
r.After = DockerContainerLifecycleSnapshot{
ContainerID: "1234567890ab",
ObservedAt: time.Now().UTC(),
}
},
wantError: "after-state container mismatch",
},
} {
t.Run(tc.name, func(t *testing.T) {
result := newValidCompleteUpdateResult(req)
tc.mutate(&result)
err := ValidateDockerContainerUpdateResultForRequest(req, result)
if err == nil {
t.Fatalf("expected error containing %q, got success", tc.wantError)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantError)
}
})
}
t.Run("after container equal to new container by case is accepted", func(t *testing.T) {
result := newValidCompleteUpdateResult(req)
// NewContainerID is lowercase hex; force the after-state to use the
// same id but uppercase to confirm EqualFold passes.
result.NewContainerID = testValidUpdateContainerID
result.After.ContainerID = strings.ToUpper(testValidUpdateContainerID)
if err := ValidateDockerContainerUpdateResultForRequest(req, result); err != nil {
t.Fatalf("expected case-insensitive after-state match to pass, got: %v", err)
}
})
}
// --- helpers ---
// mutatePayload returns a copy of p with the mutator applied. Useful for
// building a base JSON whose fields have already passed Bind, then perturbing
// them for a decode test.
func mutatePayload(p DockerContainerUpdatePayload, mutator func(*DockerContainerUpdatePayload)) DockerContainerUpdatePayload {
cp := p
mutator(&cp)
return cp
}
// mutateResult returns a copy of r with the mutator applied.
func mutateResult(r DockerContainerUpdateResultPayload, mutator func(*DockerContainerUpdateResultPayload)) DockerContainerUpdateResultPayload {
cp := r
mutator(&cp)
return cp
}
// rejson marshals the given value to canonical JSON. If key/value are both
// non-empty, the resulting JSON object has its key overwritten with value
// (used to delete or rewrite a single field while staying strict-decodable).
// When key/value are empty, the value is marshalled unchanged.
func rejson(t *testing.T, v any, key, value string) string {
t.Helper()
raw, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
if key == "" && value == "" {
return string(raw)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if value == "" {
delete(m, key)
} else {
m[key] = value
}
out, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return string(out)
}
+31
View File
@@ -0,0 +1,31 @@
package agentexec
import "testing"
func TestCoverageNormalizeDeployMaxParallel(t *testing.T) {
for _, tc := range []struct {
name string
value int
defaultValue int
want int
}{
{"zero value uses default", 0, 5, 5},
{"negative value uses default", -3, 4, 4},
{"normal passthrough below cap", 3, 5, 3},
{"one passthrough", 1, 5, 1},
{"at cap passthrough", MaxDeployParallel, 5, MaxDeployParallel},
{"over cap clamped to max", 15, 5, MaxDeployParallel},
{"far over cap clamped to max", 1000, 1, MaxDeployParallel},
{"default over cap clamped to max", 0, 25, MaxDeployParallel},
{"default zero stays zero", 0, 0, 0},
{"positive value ignores zero default", 7, 0, 7},
} {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeDeployMaxParallel(tc.value, tc.defaultValue)
if got != tc.want {
t.Fatalf("NormalizeDeployMaxParallel(%d, %d) = %d, want %d",
tc.value, tc.defaultValue, got, tc.want)
}
})
}
}
@@ -0,0 +1,113 @@
package licensing
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// These tests add coverage for previously-uncovered functions in
// activation_types.go. White-box (same package) so unexported helpers can be
// exercised directly.
func TestCoverageParseExpiresAt(t *testing.T) {
t.Run("empty_string_returns_zero", func(t *testing.T) {
g := GrantEnvelope{ExpiresAt: ""}
assert.Equal(t, int64(0), g.ParseExpiresAt())
})
t.Run("zero_value_envelope_returns_zero", func(t *testing.T) {
var g GrantEnvelope
assert.Equal(t, int64(0), g.ParseExpiresAt())
})
t.Run("valid_rfc3339_returns_unix_ts", func(t *testing.T) {
ts := "2025-01-15T10:30:00Z"
g := GrantEnvelope{ExpiresAt: ts}
expected, err := time.Parse(time.RFC3339, ts)
assert.NoError(t, err)
assert.Equal(t, expected.Unix(), g.ParseExpiresAt())
})
t.Run("valid_rfc3339_with_offset_returns_unix_ts", func(t *testing.T) {
ts := "2025-06-30T23:59:59-07:00"
g := GrantEnvelope{ExpiresAt: ts}
expected, err := time.Parse(time.RFC3339, ts)
assert.NoError(t, err)
assert.Equal(t, expected.Unix(), g.ParseExpiresAt())
})
t.Run("invalid_format_returns_zero", func(t *testing.T) {
g := GrantEnvelope{ExpiresAt: "not-a-date"}
assert.Equal(t, int64(0), g.ParseExpiresAt())
})
t.Run("garbage_numeric_returns_zero", func(t *testing.T) {
// A bare unix epoch number is not RFC3339 and must not parse.
g := GrantEnvelope{ExpiresAt: "0"}
assert.Equal(t, int64(0), g.ParseExpiresAt())
})
t.Run("value_receiver_does_not_panic", func(t *testing.T) {
g := GrantEnvelope{ExpiresAt: "2025-01-15T10:30:00Z"}
assert.NotPanics(t, func() {
_ = g.ParseExpiresAt()
})
})
}
func TestCoverageGrantClaimsUseUncappedCoreMonitoring(t *testing.T) {
t.Run("nil_claims_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(nil))
})
t.Run("uncapped_tier_pro_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierPro)}))
})
t.Run("uncapped_tier_free_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierFree)}))
})
t.Run("uncapped_tier_relay_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierRelay)}))
})
t.Run("uncapped_tier_lifetime_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierLifetime)}))
})
t.Run("uncapped_tier_business_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierBusiness)}))
})
t.Run("capped_tier_cloud_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierCloud)}))
})
t.Run("capped_tier_msp_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierMSP)}))
})
t.Run("capped_tier_enterprise_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: string(TierEnterprise)}))
})
t.Run("empty_tier_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: ""}))
})
t.Run("unknown_tier_false", func(t *testing.T) {
assert.False(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: "does-not-exist"}))
})
t.Run("case_insensitive_uppercase_true", func(t *testing.T) {
// IsSelfHostedCoreMonitoringUncappedTier lowercases the input.
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: "PRO"}))
})
t.Run("case_insensitive_mixed_with_whitespace_true", func(t *testing.T) {
assert.True(t, grantClaimsUseUncappedCoreMonitoring(&GrantClaims{Tier: " Pro_Plus "}))
})
}
+343
View File
@@ -0,0 +1,343 @@
package licensing
import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// These tests add coverage for previously-uncovered pure helpers and simple
// *Service accessors/mutators in service.go. They are white-box (same package)
// so unexported helpers can be exercised directly.
// -----------------------------------------------------------------------------
// Pure helpers (no *Service setup required)
// -----------------------------------------------------------------------------
func TestCoverageUnionFeatures(t *testing.T) {
t.Run("both_empty", func(t *testing.T) {
got := unionFeatures(nil, nil)
assert.Empty(t, got)
})
t.Run("first_empty_second_non_empty", func(t *testing.T) {
got := unionFeatures(nil, []string{"a"})
assert.Equal(t, []string{"a"}, got)
})
t.Run("second_empty_first_non_empty", func(t *testing.T) {
got := unionFeatures([]string{"a"}, nil)
assert.Equal(t, []string{"a"}, got)
})
t.Run("overlap_dedup", func(t *testing.T) {
got := unionFeatures([]string{"a", "b"}, []string{"b", "c"})
assert.Equal(t, []string{"a", "b", "c"}, got)
})
t.Run("sorted_output", func(t *testing.T) {
got := unionFeatures([]string{"c", "a"}, []string{"b"})
assert.Equal(t, []string{"a", "b", "c"}, got)
assert.True(t, sliceIsSorted(got), "output must be sorted")
})
t.Run("duplicates_within_same_slice", func(t *testing.T) {
got := unionFeatures([]string{"a", "a"}, nil)
assert.Equal(t, []string{"a"}, got)
})
t.Run("does_not_mutate_input_order_semantics", func(t *testing.T) {
// Unsorted, overlapping inputs from both sides collapse to one sorted set.
got := unionFeatures([]string{"z", "a", "m"}, []string{"m", "q"})
assert.Equal(t, []string{"a", "m", "q", "z"}, got)
})
}
func sliceIsSorted(s []string) bool {
for i := 1; i < len(s); i++ {
if s[i-1] > s[i] {
return false
}
}
return true
}
func TestCoverageSafeIntFromInt64(t *testing.T) {
t.Run("normal_passthrough", func(t *testing.T) {
assert.Equal(t, 100, safeIntFromInt64(100))
})
t.Run("zero", func(t *testing.T) {
assert.Equal(t, 0, safeIntFromInt64(0))
})
t.Run("negative_clamps_to_zero", func(t *testing.T) {
assert.Equal(t, 0, safeIntFromInt64(-1))
assert.Equal(t, 0, safeIntFromInt64(-9999))
})
t.Run("large_negative", func(t *testing.T) {
assert.Equal(t, 0, safeIntFromInt64(math.MinInt64))
})
t.Run("maxint64_clamps_to_maxint", func(t *testing.T) {
maxInt := int(^uint(0) >> 1)
assert.Equal(t, maxInt, safeIntFromInt64(math.MaxInt64))
})
t.Run("large_in_range_value_on_64bit", func(t *testing.T) {
// 1 << 40 fits in a 64-bit int.
assert.Equal(t, 1<<40, safeIntFromInt64(int64(1)<<40))
})
}
func TestCoverageRemainingDaysCeil(t *testing.T) {
const day = int64(86400)
t.Run("expired_returns_zero", func(t *testing.T) {
now := int64(1_000_000)
assert.Equal(t, 0, remainingDaysCeil(now-day, now))
})
t.Run("negative_delta_returns_zero", func(t *testing.T) {
now := int64(2_000_000)
assert.Equal(t, 0, remainingDaysCeil(now-5, now))
})
t.Run("exact_boundary_delta_zero", func(t *testing.T) {
now := int64(3_000_000)
assert.Equal(t, 0, remainingDaysCeil(now, now))
})
t.Run("one_second_rounds_up_to_one_day", func(t *testing.T) {
now := int64(4_000_000)
assert.Equal(t, 1, remainingDaysCeil(now+1, now))
})
t.Run("partial_day_rounds_up", func(t *testing.T) {
now := int64(5_000_000)
// 1.5 days -> ceil -> 2
assert.Equal(t, 2, remainingDaysCeil(now+(day+day/2), now))
})
t.Run("exactly_one_day", func(t *testing.T) {
now := int64(6_000_000)
assert.Equal(t, 1, remainingDaysCeil(now+day, now))
})
t.Run("exactly_two_days", func(t *testing.T) {
now := int64(7_000_000)
assert.Equal(t, 2, remainingDaysCeil(now+2*day, now))
})
}
// -----------------------------------------------------------------------------
// *Service helpers / accessors
// -----------------------------------------------------------------------------
func TestCoverageEnsureGracePeriodEnd(t *testing.T) {
t.Run("nil_license_noop", func(t *testing.T) {
s := NewService()
s.mu.Lock()
s.ensureGracePeriodEnd()
s.mu.Unlock()
// Still no license, no panic, no grace period set.
assert.Nil(t, s.CurrentUnsafeForTesting())
})
t.Run("nil_GracePeriodEnd_sets_from_ExpiresAt_plus_grace", func(t *testing.T) {
s := NewService()
expiresAt := time.Now().Add(24 * time.Hour).Unix()
lic := &License{Claims: Claims{ExpiresAt: expiresAt}}
s.SetCurrentForTesting(lic)
s.mu.Lock()
s.ensureGracePeriodEnd()
s.mu.Unlock()
got := s.CurrentUnsafeForTesting().GracePeriodEnd
require.NotNil(t, got)
expected := time.Unix(expiresAt, 0).Add(DefaultGracePeriod)
assert.True(t, got.Equal(expected), "grace end = expiresAt + DefaultGracePeriod")
})
t.Run("already_set_noop", func(t *testing.T) {
s := NewService()
preExisting := time.Now().Add(48 * time.Hour)
lic := &License{
Claims: Claims{ExpiresAt: time.Now().Add(24 * time.Hour).Unix()},
GracePeriodEnd: &preExisting,
}
s.SetCurrentForTesting(lic)
s.mu.Lock()
s.ensureGracePeriodEnd()
s.mu.Unlock()
got := s.CurrentUnsafeForTesting().GracePeriodEnd
require.NotNil(t, got)
assert.True(t, got.Equal(preExisting), "pre-existing grace period must be preserved")
})
}
func TestCoverageSetEvaluator(t *testing.T) {
t.Run("set_nil", func(t *testing.T) {
s := NewService()
s.SetEvaluator(nil)
assert.Nil(t, s.Evaluator())
})
t.Run("set_non_nil_then_get", func(t *testing.T) {
s := NewService()
eval := NewEvaluator(NewTokenSource(&Claims{Tier: TierPro}))
s.SetEvaluator(eval)
assert.Same(t, eval, s.Evaluator())
})
t.Run("overwrite", func(t *testing.T) {
s := NewService()
first := NewEvaluator(NewTokenSource(&Claims{Tier: TierPro}))
second := NewEvaluator(NewTokenSource(&Claims{Tier: TierProPlus}))
s.SetEvaluator(first)
s.SetEvaluator(second)
assert.Same(t, second, s.Evaluator())
})
}
func TestCoverageSetStateMachine(t *testing.T) {
t.Run("nil_is_noop_safe", func(t *testing.T) {
s := NewService()
// Must not panic and must leave subscription derivation claim-based.
s.SetStateMachine(nil)
assert.NotPanics(t, func() {
_ = s.SubscriptionState()
})
})
t.Run("non_nil_configures_state_machine_hook", func(t *testing.T) {
// When the state-machine hook is configured and a license with a non-empty
// SubState is present, SubscriptionState short-circuits to the claim's
// SubState instead of deriving from expiration/grace logic. We observe
// the toggle using an expired-but-claimed-active license.
s := NewService()
lic := &License{
Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(-365 * 24 * time.Hour).Unix(), // long expired
SubState: SubStateActive,
},
}
s.SetCurrentForTesting(lic)
// Without the hook: expiration derivation wins -> expired.
assert.Equal(t, string(SubStateExpired), s.SubscriptionState())
// With the hook: claim SubState is returned verbatim -> active.
s.SetStateMachine("fake-state-machine")
assert.Equal(t, string(SubStateActive), s.SubscriptionState())
// Clearing the hook restores derived behavior -> expired.
s.SetStateMachine(nil)
assert.Equal(t, string(SubStateExpired), s.SubscriptionState())
})
}
func TestCoverageCurrentUnsafeForTesting(t *testing.T) {
t.Run("nil_when_no_license", func(t *testing.T) {
s := NewService()
assert.Nil(t, s.CurrentUnsafeForTesting())
})
t.Run("returns_internal_pointer", func(t *testing.T) {
s := NewService()
lic := &License{Claims: Claims{Tier: TierPro}}
s.SetCurrentForTesting(lic)
// Unsafe accessor returns the exact internal pointer (not a clone).
assert.Same(t, lic, s.CurrentUnsafeForTesting())
})
}
func TestCoverageIsValid(t *testing.T) {
t.Run("nil_license_false", func(t *testing.T) {
s := NewService()
assert.False(t, s.IsValid())
})
t.Run("active_valid_true", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{
Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
},
})
assert.True(t, s.IsValid())
})
t.Run("lifetime_valid_true", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{
Claims: Claims{Tier: TierLifetime}, // ExpiresAt == 0 -> never expires
})
assert.True(t, s.IsValid())
})
t.Run("expired_past_grace_false", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{
Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(-365 * 24 * time.Hour).Unix(),
},
})
assert.False(t, s.IsValid())
})
t.Run("suspended_claim_false", func(t *testing.T) {
s := NewService()
s.SetCurrentForTesting(&License{
Claims: Claims{
Tier: TierPro,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
SubState: SubStateSuspended,
},
})
assert.False(t, s.IsValid())
})
}
func TestCoverageIsLicenseValidationDevMode(t *testing.T) {
t.Run("unset_is_false", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "")
assert.False(t, IsLicenseValidationDevMode())
})
t.Run("explicit_false", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
assert.False(t, IsLicenseValidationDevMode())
})
t.Run("explicit_true", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
assert.True(t, IsLicenseValidationDevMode())
})
t.Run("true_with_surrounding_whitespace", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", " true ")
assert.True(t, IsLicenseValidationDevMode())
})
t.Run("uppercase_true_is_case_insensitive_match", func(t *testing.T) {
// Uses strings.EqualFold, so "TRUE" also enables dev mode.
t.Setenv("PULSE_LICENSE_DEV_MODE", "TRUE")
assert.True(t, IsLicenseValidationDevMode())
})
t.Run("non_true_word_false", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "yes")
assert.False(t, IsLicenseValidationDevMode())
})
}