test(recovery): prove ai adapter metadata resilience

This commit is contained in:
rcourtman
2026-03-26 23:03:46 +00:00
parent 7fd033210c
commit 266a504f21
2 changed files with 111 additions and 0 deletions
@@ -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)
}
}