Files
rcourtman 5288b64d40 fix(patrol): preserve refusals and expose measured summary evidence
A provider policy refusal was classified as a connection failure, while
summary tools mixed high-utilisation heuristics with unchecked health claims.
Preserve explicit refusals before tool recovery and provide retained metrics
with source scope, observation timestamps and bucket extrema for diagnosis.

Record the live qualification limits and the shared temporal tier-query gap.
2026-09-05 22:29:48 +01:00

68 lines
2.7 KiB
Go

package reporting
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
"github.com/stretchr/testify/require"
)
func TestMetricEvidenceRetainsObservedCoverageAcrossRestart(t *testing.T) {
cfg := metrics.DefaultConfig(t.TempDir())
cfg.RetentionRaw = 8 * 24 * time.Hour
store, err := metrics.NewStore(cfg)
require.NoError(t, err)
start := time.Now().Add(-6 * time.Hour).Truncate(time.Minute)
for i, delta := range []time.Duration{0, 2 * time.Minute, time.Hour} {
store.Write("node", "native-node", "memory", float64(88+i), start.Add(delta))
}
require.NoError(t, store.Close())
// A retained point can carry a mean and separately recorded extrema.
// Preserve both the peak and its timestamp rather than using its mean.
db, err := sql.Open("sqlite", cfg.DBPath)
require.NoError(t, err)
_, err = db.Exec("UPDATE metrics SET min_value = 80, max_value = 100 WHERE timestamp = ?", start.Add(2*time.Minute).Unix())
require.NoError(t, err)
require.NoError(t, db.Close())
store, err = metrics.NewStore(cfg)
require.NoError(t, err)
defer store.Close()
engine := NewReportEngine(EngineConfig{MetricsStore: store})
narrator := &stubNarrator{err: errors.New("must not run")}
req := MetricReportRequest{ResourceID: "canonical-agent", MetricsResourceID: "native-node", ResourceType: "node", Start: start.Add(-18 * time.Hour), End: start.Add(6 * time.Hour), Narrator: narrator}
evidence, err := engine.MetricEvidenceFor(context.Background(), req)
require.NoError(t, err)
require.Equal(t, "canonical-agent", evidence.ResourceID)
reading := evidence.Metrics["memory"]
require.Equal(t, 3, reading.RetainedPoints)
require.Equal(t, "%", reading.Unit)
require.Equal(t, 89.0, reading.Mean)
require.Equal(t, 90.0, reading.Latest)
require.Equal(t, 80.0, reading.Min)
require.Equal(t, 100.0, reading.Max)
require.True(t, reading.MinBucketAt.Equal(start.Add(2*time.Minute)))
require.True(t, reading.MaxBucketAt.Equal(start.Add(2*time.Minute)))
require.True(t, reading.FirstAt.Equal(start))
require.True(t, reading.LastAt.Equal(start.Add(time.Hour)))
require.Equal(t, (58 * time.Minute).Seconds(), reading.MaxGapSeconds)
require.Empty(t, narrator.seen.ResourceID, "evidence must not invoke report judgment")
req.MetricsResourceID = "unobserved-node"
empty, err := engine.MetricEvidenceFor(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, empty.Metrics)
require.Empty(t, empty.Metrics)
require.NoError(t, store.Close())
_, err = engine.MetricEvidenceFor(context.Background(), req)
require.Error(t, err, "query errors must not become empty evidence")
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = engine.MetricEvidenceFor(ctx, req)
require.ErrorIs(t, err, context.Canceled)
}