feat: add resumable file archive jobs and offline verification

This commit is contained in:
2026-09-23 10:50:49 +08:00
parent 2d8467e50c
commit 6eb429e21f
19 changed files with 1160 additions and 15 deletions

View File

@@ -8,12 +8,14 @@ import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/retention"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
@@ -300,6 +302,40 @@ func TestIntegrationRetentionDiskGrowthAndPreview(t *testing.T) {
if _, err := store.ExportRetention(ctx, apiToken, retention.ArchiveInput{Kind: "tasks", ExpectedRevision: 1}); err == nil {
t.Fatal("business bearer token accepted as console admin")
}
jobSpec := retention.JobSpec{NamespaceID: boot.NamespaceID, Kind: "tasks", PolicyRevision: 1, BatchSize: 37}
jobDir := filepath.Join(t.TempDir(), "retention-job")
jobOptions := retention.JobOptions{MaxBatches: 1, MaxBytes: 64 << 20}
exportJob := func(ctx context.Context, job core.ID, in retention.ArchiveInput) (retention.ArchiveSnapshot, error) {
return store.ExportRetentionForJob(ctx, boot.NamespaceID, job, in)
}
firstJob, err := retention.RunArchive(ctx, jobDir, jobSpec, jobOptions, exportJob)
if err != nil || firstJob.Completed || firstJob.Roots != 37 {
t.Fatalf("archive job first batch=%+v err=%v", firstJob, err)
}
jobOptions.MaxBatches = 10
finishedJob, err := retention.RunArchive(ctx, jobDir, jobSpec, jobOptions, exportJob)
if err != nil || !finishedJob.Completed || finishedJob.Roots != 254 || finishedJob.Batches != 7 || finishedJob.JobID != firstJob.JobID {
t.Fatalf("archive job resumed=%+v err=%v", finishedJob, err)
}
verifiedJob, err := retention.VerifyArchive(ctx, jobDir)
if err != nil || verifiedJob != finishedJob {
t.Fatalf("offline job verification=%+v err=%v", verifiedJob, err)
}
var jobAudits int
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM orbit_audit_events WHERE namespace_id=? AND actor_id=? AND action='retention.archive.export'", boot.NamespaceID, "archive:"+string(finishedJob.JobID)).Scan(&jobAudits); err != nil || jobAudits != 7 {
t.Fatalf("job audit count=%d err=%v", jobAudits, err)
}
if _, err := db.ExecContext(ctx, "UPDATE orbit_namespaces SET enabled=FALSE WHERE id=?", boot.NamespaceID); err != nil {
t.Fatal(err)
}
if _, err := store.ExportRetentionForJob(ctx, boot.NamespaceID, finishedJob.JobID, retention.ArchiveInput{Kind: "tasks", ExpectedRevision: 1}); faultCode(err) != "PERMISSION_DENIED" {
t.Fatalf("disabled namespace exported: %v", err)
}
if _, err := db.ExecContext(ctx, "UPDATE orbit_namespaces SET enabled=TRUE WHERE id=?", boot.NamespaceID); err != nil {
t.Fatal(err)
}
t.Logf("durable archive job: roots=%d batches=%d audit_rows=%d offline_verified=true", finishedJob.Roots, finishedJob.Batches, jobAudits)
policyChange := retention.UpdateInput{ExpectedRevision: 1, Enabled: true, IdempotencyDays: 90, TaskHistoryDays: 2, WorkflowHistoryDays: 30, EventLogDays: 30, DeliveryHistoryDays: 30, InboxHistoryDays: 30, AuditLogDays: 365, Reason: "verify frozen cursor boundary"}
if _, err := store.UpdateRetentionPolicy(ctx, consoleToken, "growth-retention-revise", policyChange); err != nil {
t.Fatal(err)