Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
// Package agentstore is the transport boundary between pulse-agent and pulse-api.
//
// The agent runs with the narrow read-only host access it needs and never exposes a
// network endpoint; the API never reaches out to the host. Both processes already share
// PostgreSQL on the internal-only Compose network (see deploy/compose.yaml and ADR-0010),
// so the agent writes bounded, normalized snapshots into the database and the API reads
// the most recent one per capability. This keeps the privilege separation required by
// SYSTEM_ARCHITECTURE section "pulse-agent" without adding an inbound port to the agent.
//
// Freshness is deliberately the reader's problem, not the writer's: a snapshot carries
// the time the agent observed it, and the reader decides whether that is still usable.
// A capability with no snapshot, or one older than its freshness window, resolves to
// Unknown and never to Healthy (ADR-0008).
package agentstore
import (
"context"
"encoding/json"
"errors"
"time"
)
// Capability identifies one bounded telemetry surface an agent can report. The set is
// closed: a reader must never accept a capability it does not recognize, because an
// unknown capability cannot be normalized or bounded.
type Capability string
const (
CapabilityHost Capability = "host"
CapabilityProcesses Capability = "processes"
CapabilityContainers Capability = "containers"
CapabilityArray Capability = "array"
CapabilityDisks Capability = "disks"
CapabilityPools Capability = "pools"
CapabilityShares Capability = "shares"
)
// Capabilities lists every capability the platform recognizes, in a stable order.
func Capabilities() []Capability {
return []Capability{
CapabilityHost, CapabilityProcesses, CapabilityContainers,
CapabilityArray, CapabilityDisks, CapabilityPools, CapabilityShares,
}
}
// Valid reports whether the capability is one this platform recognizes.
func (c Capability) Valid() bool {
for _, known := range Capabilities() {
if c == known {
return true
}
}
return false
}
// MaxPayloadBytes bounds a single snapshot. The largest realistic payload is the process
// inventory at the documented scale target; this leaves generous headroom while keeping a
// misbehaving or compromised agent from filling the database.
const MaxPayloadBytes = 2 << 20
// ErrNoSnapshot is returned by Reader.Latest when the capability has never been reported.
// It is an expected condition on a fresh install, not a failure: callers translate it into
// an Unknown status with an explicit reason.
var ErrNoSnapshot = errors.New("no agent snapshot recorded")
// Snapshot is one bounded observation of a single capability.
type Snapshot struct {
// AgentID identifies the reporting agent.
AgentID string
// Capability is the telemetry surface this payload describes.
Capability Capability
// ObservedAt is when the agent read the underlying source, in UTC.
ObservedAt time.Time
// ReceivedAt is when the store accepted the snapshot, in UTC. It is set by the
// writer implementation, never by the agent, so a skewed agent clock cannot make
// stale data look fresh.
ReceivedAt time.Time
// Payload is the domain RawSnapshot for this capability, JSON encoded.
Payload json.RawMessage
}
// Age reports how long ago the agent observed this snapshot.
func (s Snapshot) Age(now time.Time) time.Duration { return now.Sub(s.ObservedAt) }
// Writer is the narrow interface pulse-agent depends on. The agent must not be able to
// read other agents' data or mutate anything else.
type Writer interface {
// Put records the newest snapshot for one capability, replacing any previous one.
// Implementations reject an unknown capability, an oversized payload, a zero or
// future ObservedAt, and payloads that are not valid JSON objects.
Put(ctx context.Context, snapshot Snapshot) error
}
// Reader is the narrow interface pulse-api depends on.
type Reader interface {
// Latest returns the most recent snapshot for the capability, or ErrNoSnapshot.
Latest(ctx context.Context, capability Capability) (Snapshot, error)
}
// Store is the combined boundary. Only the migration-owning implementation satisfies it.
type Store interface {
Writer
Reader
}
+246
View File
@@ -0,0 +1,246 @@
package agentstore
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// MaxAgentIDBytes bounds the reporting agent identifier. It matches the column check in
// migration 0016 so a rejection surfaces as a terse store error rather than a constraint
// violation from PostgreSQL.
const MaxAgentIDBytes = 128
// MaxClockSkew is the only tolerance granted to an agent clock that runs ahead of the
// database. The agent and the database share one host in the supported topology, so real
// skew is sub-millisecond; one second absorbs scheduling jitter while staying far below
// the tightest freshness window, so a skewed clock cannot mask meaningful staleness.
const MaxClockSkew = time.Second
// ErrUnavailable is returned when the store has no database pool. Callers translate it
// into an Unknown status exactly as they do ErrNoSnapshot.
var ErrUnavailable = errors.New("agent snapshot store is unavailable")
// PostgresStore is the only implementation of Store. It owns the agent_snapshots table
// created by migration 0016 and keeps exactly one row per (agent, capability).
//
// Transaction boundary: Put commits the latest snapshot and any derived capacity samples
// atomically. Latest remains a single read statement.
type PostgresStore struct {
// Pool is the shared pgx pool. A nil pool makes every call return ErrUnavailable.
Pool *pgxpool.Pool
// Clock supplies ReceivedAt and the future-observation check. It exists for tests;
// production leaves it nil and the store uses the wall clock in UTC.
Clock func() time.Time
}
var _ Store = PostgresStore{}
func (s PostgresStore) now() time.Time {
if s.Clock == nil {
return time.Now().UTC()
}
return s.Clock().UTC()
}
// Put records the newest snapshot for one capability, replacing any previous one for the
// same agent. It rejects an unknown capability, a missing or oversized payload, a payload
// that is not a JSON object, and a zero or future ObservedAt.
//
// ReceivedAt is always taken from the store clock: the caller's value is ignored so a
// skewed or hostile agent clock cannot make stale data look freshly received.
//
// A snapshot that is older than the row already stored is accepted but does not overwrite
// it, so a delayed retry cannot resurrect superseded telemetry.
func (s PostgresStore) Put(ctx context.Context, snapshot Snapshot) error {
row, err := prepare(snapshot, s.now())
if err != nil {
return err
}
if s.Pool == nil {
return ErrUnavailable
}
tx, err := s.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin agent snapshot write: %w", err)
}
defer func() {
rollbackCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tx.Rollback(rollbackCtx)
}()
if _, err := tx.Exec(ctx, `INSERT INTO agent_snapshots (agent_id, capability, observed_at, received_at, payload)
VALUES ($1, $2, $3, $4, $5::jsonb)
ON CONFLICT (agent_id, capability) DO UPDATE
SET observed_at = EXCLUDED.observed_at, received_at = EXCLUDED.received_at, payload = EXCLUDED.payload
WHERE agent_snapshots.observed_at <= EXCLUDED.observed_at`,
row.AgentID, string(row.Capability), row.ObservedAt, row.ReceivedAt, []byte(row.Payload)); err != nil {
return fmt.Errorf("write agent snapshot: %w", err)
}
if err := persistCapacitySamples(ctx, tx, row); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit agent snapshot write: %w", err)
}
return nil
}
type capacitySample struct {
Kind string `json:"kind"`
ID string `json:"id"`
Name string `json:"name"`
ObservedAt time.Time `json:"observed_at"`
UsedBytes uint64 `json:"used_bytes"`
CapacityBytes uint64 `json:"capacity_bytes"`
}
func persistCapacitySamples(ctx context.Context, tx pgx.Tx, snapshot Snapshot) error {
samples, err := capacitySamples(snapshot)
if err != nil {
return err
}
if len(samples) == 0 {
return nil
}
payload, err := json.Marshal(samples)
if err != nil {
return fmt.Errorf("encode capacity samples: %w", err)
}
_, err = tx.Exec(ctx, `INSERT INTO capacity_samples (entity_kind,entity_id,entity_name,source_id,sampled_at,observed_at,used_bytes,capacity_bytes)
SELECT sample.kind,sample.id,sample.name,$1,
date_trunc('day',sample.observed_at) + floor(extract(hour FROM sample.observed_at)/6)*interval '6 hours',
sample.observed_at,sample.used_bytes,sample.capacity_bytes
FROM jsonb_to_recordset($2::jsonb) AS sample(kind text,id text,name text,observed_at timestamptz,used_bytes bigint,capacity_bytes bigint)
ON CONFLICT (entity_kind,entity_id,source_id,sampled_at) DO UPDATE
SET entity_name=EXCLUDED.entity_name,observed_at=EXCLUDED.observed_at,used_bytes=EXCLUDED.used_bytes,capacity_bytes=EXCLUDED.capacity_bytes
WHERE capacity_samples.observed_at <= EXCLUDED.observed_at`, snapshot.AgentID, payload)
if err != nil {
return fmt.Errorf("persist capacity samples: %w", err)
}
return nil
}
func capacitySamples(snapshot Snapshot) ([]capacitySample, error) {
kind := ""
collection := ""
capacityField := ""
switch snapshot.Capability {
case CapabilityShares:
kind, collection = "share", "shares"
case CapabilityPools:
kind, collection, capacityField = "pool", "pools", "usableBytes"
case CapabilityDisks:
kind, collection, capacityField = "disk", "disks", "sizeBytes"
default:
return nil, nil
}
var document map[string]json.RawMessage
if err := json.Unmarshal(snapshot.Payload, &document); err != nil {
return nil, errors.New("decode capacity snapshot payload")
}
var items []map[string]json.RawMessage
if err := json.Unmarshal(document[collection], &items); err != nil {
return nil, fmt.Errorf("decode %s capacity collection", kind)
}
samples := make([]capacitySample, 0, len(items))
for _, item := range items {
var id, name string
var used, capacity uint64
if json.Unmarshal(item["id"], &id) != nil || json.Unmarshal(item["name"], &name) != nil || json.Unmarshal(item["usedBytes"], &used) != nil {
continue
}
if capacityField != "" {
if json.Unmarshal(item[capacityField], &capacity) != nil {
continue
}
}
observed := snapshot.ObservedAt
if kind == "share" {
var sizeObserved time.Time
if json.Unmarshal(item["sizeObservedAt"], &sizeObserved) == nil && !sizeObserved.IsZero() {
observed = sizeObserved.UTC()
}
}
id, name = strings.TrimSpace(id), strings.TrimSpace(name)
if id == "" || name == "" || len(id) > 128 || len(name) > 255 || used > math.MaxInt64 || capacity > math.MaxInt64 || observed.IsZero() || observed.After(snapshot.ReceivedAt.Add(MaxClockSkew)) {
continue
}
samples = append(samples, capacitySample{Kind: kind, ID: id, Name: name, ObservedAt: observed, UsedBytes: used, CapacityBytes: capacity})
}
return samples, nil
}
// Latest returns the most recent snapshot for the capability across every reporting
// agent, or ErrNoSnapshot when none has been recorded.
func (s PostgresStore) Latest(ctx context.Context, capability Capability) (Snapshot, error) {
if !capability.Valid() {
return Snapshot{}, fmt.Errorf("unknown agent capability %q", capability)
}
if s.Pool == nil {
return Snapshot{}, ErrUnavailable
}
return scanSnapshot(s.Pool.QueryRow(ctx, `SELECT agent_id, capability, observed_at, received_at, payload
FROM agent_snapshots WHERE capability = $1 ORDER BY observed_at DESC, received_at DESC LIMIT 1`,
string(capability)))
}
// scanSnapshot reads one row and maps the absence of a row onto ErrNoSnapshot, which is an
// expected condition on a fresh install rather than a failure.
func scanSnapshot(row pgx.Row) (Snapshot, error) {
var (
snapshot Snapshot
name string
payload []byte
)
err := row.Scan(&snapshot.AgentID, &name, &snapshot.ObservedAt, &snapshot.ReceivedAt, &payload)
if errors.Is(err, pgx.ErrNoRows) {
return Snapshot{}, ErrNoSnapshot
}
if err != nil {
return Snapshot{}, fmt.Errorf("read agent snapshot: %w", err)
}
snapshot.Capability = Capability(name)
snapshot.ObservedAt = snapshot.ObservedAt.UTC()
snapshot.ReceivedAt = snapshot.ReceivedAt.UTC()
snapshot.Payload = json.RawMessage(payload)
return snapshot, nil
}
// prepare enforces every bound the Writer contract documents and returns the row the
// store persists. It is the single place where an inbound snapshot is trusted, and it
// overwrites ReceivedAt with the store clock so the caller cannot influence freshness.
func prepare(snapshot Snapshot, now time.Time) (Snapshot, error) {
if snapshot.AgentID == "" || len(snapshot.AgentID) > MaxAgentIDBytes {
return Snapshot{}, errors.New("agent id is required and bounded")
}
if !snapshot.Capability.Valid() {
return Snapshot{}, fmt.Errorf("unknown agent capability %q", snapshot.Capability)
}
if len(snapshot.Payload) == 0 {
return Snapshot{}, errors.New("agent snapshot payload is required")
}
if len(snapshot.Payload) > MaxPayloadBytes {
return Snapshot{}, fmt.Errorf("agent snapshot payload exceeds %d bytes", MaxPayloadBytes)
}
if snapshot.ObservedAt.IsZero() {
return Snapshot{}, errors.New("agent snapshot observed time is required")
}
if snapshot.ObservedAt.After(now.Add(MaxClockSkew)) {
return Snapshot{}, errors.New("agent snapshot observed time is in the future")
}
var object map[string]json.RawMessage
if err := json.Unmarshal(snapshot.Payload, &object); err != nil || object == nil {
return Snapshot{}, errors.New("agent snapshot payload must be a JSON object")
}
snapshot.ObservedAt = snapshot.ObservedAt.UTC()
snapshot.ReceivedAt = now
return snapshot, nil
}
@@ -0,0 +1,110 @@
package agentstore
import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
"github.com/itworx/pulse/internal/database"
)
// TestPostgreSQLSnapshotRoundTrip exercises the real table created by migration 0016. It
// skips unless PULSE_TEST_DATABASE_URL points at a disposable database.
func TestPostgreSQLSnapshotRoundTrip(t *testing.T) {
dsn := os.Getenv("PULSE_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("PULSE_TEST_DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
pool, err := database.NewPool(ctx, database.Config{URL: dsn})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := database.Migrate(ctx, pool); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `DELETE FROM agent_snapshots WHERE agent_id = 'integration-agent'`); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `DELETE FROM capacity_samples WHERE source_id = 'integration-agent'`); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cleanup, cancelCleanup := context.WithTimeout(context.Background(), 15*time.Second)
defer cancelCleanup()
_, _ = pool.Exec(cleanup, `DELETE FROM agent_snapshots WHERE agent_id = 'integration-agent'`)
_, _ = pool.Exec(cleanup, `DELETE FROM capacity_samples WHERE source_id = 'integration-agent'`)
})
now := time.Now().UTC().Truncate(time.Microsecond)
store := PostgresStore{Pool: pool, Clock: func() time.Time { return now }}
readOwn := func() Snapshot {
t.Helper()
stored := Snapshot{AgentID: "integration-agent", Capability: CapabilityPools}
var payload []byte
if err := pool.QueryRow(ctx, `SELECT observed_at,received_at,payload FROM agent_snapshots WHERE agent_id=$1 AND capability=$2`, stored.AgentID, stored.Capability).Scan(&stored.ObservedAt, &stored.ReceivedAt, &payload); err != nil {
t.Fatal(err)
}
stored.Payload = payload
return stored
}
if _, err := store.Latest(ctx, CapabilityPools); err != nil && !errors.Is(err, ErrNoSnapshot) {
t.Fatalf("unexpected error reading an empty capability: %v", err)
}
first := Snapshot{AgentID: "integration-agent", Capability: CapabilityPools, ObservedAt: now.Add(-30 * time.Second), ReceivedAt: now.Add(72 * time.Hour), Payload: json.RawMessage(`{"pools":[{"id":"cache","name":"Cache","usedBytes":100,"usableBytes":1000}],"generation":1}`)}
if err := store.Put(ctx, first); err != nil {
t.Fatal(err)
}
stored := readOwn()
if !stored.ReceivedAt.Equal(now) {
t.Fatalf("received at = %s, want the store clock %s", stored.ReceivedAt, now)
}
if !stored.ObservedAt.Equal(first.ObservedAt) {
t.Fatalf("observed at = %s, want %s", stored.ObservedAt, first.ObservedAt)
}
newer := first
newer.ObservedAt = now.Add(-5 * time.Second)
newer.Payload = json.RawMessage(`{"pools":[{"id":"cache","name":"Cache","usedBytes":200,"usableBytes":1000}],"generation":2}`)
if err := store.Put(ctx, newer); err != nil {
t.Fatal(err)
}
older := first
older.ObservedAt = now.Add(-120 * time.Second)
older.Payload = json.RawMessage(`{"pools":[{"id":"cache","name":"Cache","usedBytes":50,"usableBytes":1000}],"generation":3}`)
if err := store.Put(ctx, older); err != nil {
t.Fatal(err)
}
stored = readOwn()
var decoded struct {
Generation int `json:"generation"`
}
if err := json.Unmarshal(stored.Payload, &decoded); err != nil {
t.Fatal(err)
}
if decoded.Generation != 2 {
t.Fatalf("a delayed retry must not resurrect superseded telemetry, got generation %d", decoded.Generation)
}
var rows int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM agent_snapshots WHERE agent_id = 'integration-agent'`).Scan(&rows); err != nil {
t.Fatal(err)
}
if rows != 1 {
t.Fatalf("agent snapshot rows = %d, want exactly one per (agent, capability)", rows)
}
var sampleRows int
var usedBytes int64
if err := pool.QueryRow(ctx, `SELECT count(*),max(used_bytes) FROM capacity_samples WHERE source_id='integration-agent' AND entity_kind='pool' AND entity_id='cache'`).Scan(&sampleRows, &usedBytes); err != nil {
t.Fatal(err)
}
if sampleRows != 1 || usedBytes != 200 {
t.Fatalf("six-hour bucket was not idempotent or accepted an older retry: rows=%d used=%d", sampleRows, usedBytes)
}
}
+218
View File
@@ -0,0 +1,218 @@
package agentstore
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
)
func fixedClock(now time.Time) func() time.Time { return func() time.Time { return now } }
func TestPutRejectsEveryDocumentedBound(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
valid := Snapshot{AgentID: "agent-1", Capability: CapabilityHost, ObservedAt: now.Add(-5 * time.Second), Payload: json.RawMessage(`{"identity":{"name":"tower"}}`)}
oversized := make([]byte, MaxPayloadBytes+1)
oversized[0] = '{'
for i := 1; i < len(oversized)-1; i++ {
oversized[i] = ' '
}
oversized[len(oversized)-1] = '}'
cases := []struct {
name string
mutate func(Snapshot) Snapshot
contains string
}{
{"missing agent", func(s Snapshot) Snapshot { s.AgentID = ""; return s }, "agent id"},
{"oversized agent", func(s Snapshot) Snapshot { s.AgentID = strings.Repeat("a", MaxAgentIDBytes+1); return s }, "agent id"},
{"unknown capability", func(s Snapshot) Snapshot { s.Capability = "gpu"; return s }, "unknown agent capability"},
{"empty capability", func(s Snapshot) Snapshot { s.Capability = ""; return s }, "unknown agent capability"},
{"missing payload", func(s Snapshot) Snapshot { s.Payload = nil; return s }, "payload is required"},
{"oversized payload", func(s Snapshot) Snapshot { s.Payload = oversized; return s }, "exceeds"},
{"zero observed at", func(s Snapshot) Snapshot { s.ObservedAt = time.Time{}; return s }, "observed time is required"},
{"future observed at", func(s Snapshot) Snapshot { s.ObservedAt = now.Add(time.Hour); return s }, "in the future"},
{"array payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`[]`); return s }, "JSON object"},
{"scalar payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`42`); return s }, "JSON object"},
{"null payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`null`); return s }, "JSON object"},
{"corrupt payload", func(s Snapshot) Snapshot { s.Payload = json.RawMessage(`{"a":`); return s }, "JSON object"},
}
store := PostgresStore{Clock: fixedClock(now)}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
err := store.Put(context.Background(), testCase.mutate(valid))
if err == nil {
t.Fatal("expected rejection")
}
if errors.Is(err, ErrUnavailable) {
t.Fatalf("bounds must be enforced before availability: %v", err)
}
if !strings.Contains(err.Error(), testCase.contains) {
t.Fatalf("error %q does not mention %q", err, testCase.contains)
}
})
}
if err := store.Put(context.Background(), valid); !errors.Is(err, ErrUnavailable) {
t.Fatalf("a valid snapshot with no pool must report unavailability, got %v", err)
}
}
func TestPutAcceptsEveryKnownCapability(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
for _, capability := range Capabilities() {
snapshot := Snapshot{AgentID: "agent-1", Capability: capability, ObservedAt: now, Payload: json.RawMessage(`{}`)}
if _, err := prepare(snapshot, now); err != nil {
t.Fatalf("capability %q rejected: %v", capability, err)
}
}
}
func TestCapacitySamplesAreBoundedAndCapabilityAware(t *testing.T) {
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
snapshot := Snapshot{AgentID: "agent-1", Capability: CapabilityShares, ObservedAt: now, ReceivedAt: now, Payload: json.RawMessage(`{"shares":[{"id":"media","name":"Media","usedBytes":123,"sizeObservedAt":"2026-08-12T01:00:00Z"},{"id":"","name":"invalid","usedBytes":1}]}`)}
samples, err := capacitySamples(snapshot)
if err != nil || len(samples) != 1 || samples[0].Kind != "share" || samples[0].ID != "media" || samples[0].UsedBytes != 123 {
t.Fatalf("share capacity extraction failed: %+v, %v", samples, err)
}
snapshot.Capability = CapabilityHost
samples, err = capacitySamples(snapshot)
if err != nil || len(samples) != 0 {
t.Fatalf("non-capacity capability emitted samples: %+v, %v", samples, err)
}
snapshot.Capability = CapabilityPools
snapshot.Payload = json.RawMessage(`{"pools":[{"id":"cache","name":"Cache","usedBytes":"invalid","usableBytes":1000}]}`)
samples, err = capacitySamples(snapshot)
if err != nil || len(samples) != 0 {
t.Fatalf("malformed capacity values must fail closed: %+v, %v", samples, err)
}
}
func TestPrepareTakesReceivedAtFromStoreClock(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
// A hostile agent claims it was received in the future and observed just now.
row, err := prepare(Snapshot{
AgentID: "agent-1",
Capability: CapabilityProcesses,
ObservedAt: now.Add(-90 * time.Second),
ReceivedAt: now.Add(48 * time.Hour),
Payload: json.RawMessage(`{"processes":[]}`),
}, now)
if err != nil {
t.Fatal(err)
}
if !row.ReceivedAt.Equal(now) {
t.Fatalf("received at = %s, want the store clock %s", row.ReceivedAt, now)
}
if age := row.Age(now); age != 90*time.Second {
t.Fatalf("age = %s, want 90s", age)
}
}
func TestPrepareToleratesOnlyBenignClockSkew(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
base := Snapshot{AgentID: "agent-1", Capability: CapabilityHost, Payload: json.RawMessage(`{}`)}
base.ObservedAt = now.Add(MaxClockSkew)
if _, err := prepare(base, now); err != nil {
t.Fatalf("skew within tolerance must be accepted: %v", err)
}
base.ObservedAt = now.Add(MaxClockSkew + time.Millisecond)
if _, err := prepare(base, now); err == nil {
t.Fatal("skew beyond tolerance must be rejected")
}
if MaxClockSkew >= 30*time.Second {
t.Fatal("clock skew tolerance must stay well below the tightest freshness window")
}
}
func TestPrepareNormalizesObservedAtToUTC(t *testing.T) {
zone := time.FixedZone("CEST", 2*60*60)
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
row, err := prepare(Snapshot{AgentID: "agent-1", Capability: CapabilityShares, ObservedAt: now.Add(-time.Minute).In(zone), Payload: json.RawMessage(`{}`)}, now)
if err != nil {
t.Fatal(err)
}
if row.ObservedAt.Location() != time.UTC {
t.Fatalf("observed at location = %s, want UTC", row.ObservedAt.Location())
}
}
func TestLatestRejectsUnknownCapabilityBeforeTouchingTheDatabase(t *testing.T) {
store := PostgresStore{}
if _, err := store.Latest(context.Background(), "gpu"); err == nil || !strings.Contains(err.Error(), "unknown agent capability") {
t.Fatalf("unexpected error %v", err)
}
if _, err := store.Latest(context.Background(), CapabilityHost); !errors.Is(err, ErrUnavailable) {
t.Fatalf("unexpected error %v", err)
}
}
// stubRow stands in for one PostgreSQL row so the ErrNoSnapshot and decoding paths of
// Latest can be exercised without a live database.
type stubRow struct {
err error
values []any
}
func (r stubRow) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
for index, target := range dest {
switch typed := target.(type) {
case *string:
*typed = r.values[index].(string)
case *time.Time:
*typed = r.values[index].(time.Time)
case *[]byte:
*typed = r.values[index].([]byte)
default:
return errors.New("unsupported destination")
}
}
return nil
}
func TestLatestReportsErrNoSnapshotWhenNothingWasRecorded(t *testing.T) {
if _, err := scanSnapshot(stubRow{err: pgx.ErrNoRows}); !errors.Is(err, ErrNoSnapshot) {
t.Fatalf("missing row must map to ErrNoSnapshot, got %v", err)
}
failure := errors.New("connection reset")
_, err := scanSnapshot(stubRow{err: failure})
if err == nil || errors.Is(err, ErrNoSnapshot) || !errors.Is(err, failure) {
t.Fatalf("a read failure must not look like an absent snapshot, got %v", err)
}
}
func TestLatestDecodesRowIntoUTCSnapshot(t *testing.T) {
zone := time.FixedZone("CEST", 2*60*60)
observed := time.Date(2026, 8, 4, 12, 0, 0, 0, zone)
received := observed.Add(time.Second)
snapshot, err := scanSnapshot(stubRow{values: []any{"agent-1", string(CapabilityDisks), observed, received, []byte(`{"disks":[]}`)}})
if err != nil {
t.Fatal(err)
}
if snapshot.AgentID != "agent-1" || snapshot.Capability != CapabilityDisks {
t.Fatalf("unexpected identity %+v", snapshot)
}
if snapshot.ObservedAt.Location() != time.UTC || snapshot.ReceivedAt.Location() != time.UTC {
t.Fatalf("timestamps must be UTC: %+v", snapshot)
}
if string(snapshot.Payload) != `{"disks":[]}` {
t.Fatalf("unexpected payload %s", snapshot.Payload)
}
}
func TestStoreClockDefaultsToWallClockInUTC(t *testing.T) {
store := PostgresStore{}
if location := store.now().Location(); location != time.UTC {
t.Fatalf("store clock location = %s, want UTC", location)
}
fixed := time.Date(2026, 8, 4, 12, 0, 0, 0, time.FixedZone("CEST", 2*60*60))
if got := (PostgresStore{Clock: fixedClock(fixed)}).now(); got.Location() != time.UTC || !got.Equal(fixed) {
t.Fatalf("store clock = %s, want the injected instant in UTC", got)
}
}