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