mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 10:52:08 +00:00
test(recovery): prove ai adapter metadata resilience
This commit is contained in:
@@ -218,6 +218,11 @@ metadata. If a stored recovery row contains bad `subject_ref_json`,
|
||||
`repository_ref_json`, or `details_json`, the list endpoints should log and
|
||||
drop only the malformed derived field for that row rather than returning `500`
|
||||
for the entire recovery points or rollups surface.
|
||||
That same fail-soft contract also applies to downstream consumers that reuse
|
||||
those shared store reads, including recovery-backed reporting, alert rollups,
|
||||
and tenant-scoped AI recovery-point adapters. A malformed metadata blob may
|
||||
degrade row-local enrichment, but it must not take down adjacent readers that
|
||||
consume the same canonical recovery store.
|
||||
That same hook-boundary normalization also owns the runtime recovery display
|
||||
model. Canonical recovery points and rollups must expose `display.itemLabel`
|
||||
and `display.itemType` to recovery consumers, while legacy transport fields
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
|
||||
recoverymanager "github.com/rcourtman/pulse-go-rewrite/internal/recovery/manager"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestRecoveryPointsMCPAdapter_ListPointsToleratesMalformedPersistedMetadata(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
manager := recoverymanager.New(mtp)
|
||||
store, err := manager.StoreForOrg("default")
|
||||
if err != nil {
|
||||
t.Fatalf("StoreForOrg(default): %v", err)
|
||||
}
|
||||
|
||||
completedAt := time.Date(2026, 2, 22, 7, 45, 0, 0, time.UTC)
|
||||
if err := store.UpsertPoints(context.Background(), []recovery.RecoveryPoint{
|
||||
{
|
||||
ID: "adapter-point-bad-json",
|
||||
Provider: recovery.ProviderProxmoxPBS,
|
||||
Kind: recovery.KindBackup,
|
||||
Mode: recovery.ModeRemote,
|
||||
Outcome: recovery.OutcomeSuccess,
|
||||
SubjectResourceID: "vm-321",
|
||||
SubjectRef: &recovery.ExternalRef{
|
||||
Type: "proxmox-vm",
|
||||
Name: "321",
|
||||
},
|
||||
RepositoryRef: &recovery.ExternalRef{
|
||||
Type: "pbs-datastore",
|
||||
Name: "archive-store",
|
||||
},
|
||||
CompletedAt: &completedAt,
|
||||
Details: map[string]any{
|
||||
"comment": "nightly archive",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("UpsertPoints(): %v", err)
|
||||
}
|
||||
|
||||
persistence, err := mtp.GetPersistence("default")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPersistence(default): %v", err)
|
||||
}
|
||||
corruptRecoveryAdapterRowJSON(
|
||||
t,
|
||||
filepath.Join(persistence.DataDir(), "recovery", "recovery.db"),
|
||||
"adapter-point-bad-json",
|
||||
)
|
||||
|
||||
adapter := NewRecoveryPointsMCPAdapter(manager, "default")
|
||||
points, total, err := adapter.ListPoints(context.Background(), recovery.ListPointsOptions{
|
||||
Page: 1,
|
||||
Limit: 50,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListPoints() error = %v, want graceful degradation", err)
|
||||
}
|
||||
if total != 1 || len(points) != 1 {
|
||||
t.Fatalf("ListPoints() total=%d len=%d, want 1/1", total, len(points))
|
||||
}
|
||||
if points[0].SubjectRef != nil {
|
||||
t.Fatalf("expected malformed item ref to be omitted, got %#v", points[0].SubjectRef)
|
||||
}
|
||||
if points[0].RepositoryRef != nil {
|
||||
t.Fatalf("expected malformed repository ref to be omitted, got %#v", points[0].RepositoryRef)
|
||||
}
|
||||
if points[0].Details != nil {
|
||||
t.Fatalf("expected malformed details to be omitted, got %#v", points[0].Details)
|
||||
}
|
||||
if points[0].Display.SubjectLabel != "321" {
|
||||
t.Fatalf("display.subjectLabel = %q, want %q", points[0].Display.SubjectLabel, "321")
|
||||
}
|
||||
if points[0].Display.ItemType != "vm" {
|
||||
t.Fatalf("display.itemType = %q, want %q", points[0].Display.ItemType, "vm")
|
||||
}
|
||||
}
|
||||
|
||||
func corruptRecoveryAdapterRowJSON(t *testing.T, dbPath string, rowID string) {
|
||||
t.Helper()
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("sql.Open(%q): %v", dbPath, err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
|
||||
if _, err := db.ExecContext(
|
||||
context.Background(),
|
||||
"UPDATE recovery_points SET subject_ref_json = '{', repository_ref_json = '{', details_json = '{' WHERE id = ?",
|
||||
rowID,
|
||||
); err != nil {
|
||||
t.Fatalf("corrupt recovery row json: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user