批准 dataset 分类入库;SKIP LOCKED 分批;销毁默认 fail closed。restore 演练拒绝共享 env DSN。 Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
7.1 KiB
Go
193 lines
7.1 KiB
Go
package dataset
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/artifactvault"
|
|
)
|
|
|
|
type TrackResult struct {
|
|
Name string
|
|
OK bool
|
|
Detail string
|
|
}
|
|
|
|
func RunEightTracks(ctx context.Context, registryPath string) []TrackResult {
|
|
now := time.Now().UTC()
|
|
body := []byte("# controlled corpus body for u9 tracks")
|
|
approvedAt := now.Add(-time.Hour)
|
|
|
|
out := make([]TrackResult, 0, 8)
|
|
out = append(out, track("clean", func() error {
|
|
enum := NewMemoryEnumerator()
|
|
job := NewJob("clean", "dev", "scratch-clean", 38)
|
|
st, err := NewMemoryRunner(enum, nil).RunBatch(ctx, job, artifactvault.KindAgentCommit, 0, 50)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if st.Scanned != 0 || st.Succeeded != 0 {
|
|
return fmt.Errorf("clean must be empty: %+v", st)
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("oldest-supported", func() error {
|
|
enum := NewMemoryEnumerator()
|
|
enum.Seed(artifactvault.KindAgentCommit, []artifactvault.Candidate{{
|
|
Kind: artifactvault.KindAgentCommit, SourcePK: 1, LogicalID: "doc_oldest", SnapshotID: 3,
|
|
EventID: "commit:oldest", Payload: body, SourceSchema: OldestSupportedSchema,
|
|
}})
|
|
job := NewJob("oldest", "oldest-supported", "scratch-oldest", OldestSupportedSchema)
|
|
st, err := NewMemoryRunner(enum, nil).RunBatch(ctx, job, artifactvault.KindAgentCommit, 0, 50)
|
|
if err != nil || st.Succeeded != 1 || st.Quarantined != 0 {
|
|
return fmt.Errorf("oldest: %+v %v", st, err)
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("previous", func() error {
|
|
enum := NewMemoryEnumerator()
|
|
enum.Seed(artifactvault.KindAgentCommit, []artifactvault.Candidate{{
|
|
Kind: artifactvault.KindAgentCommit, SourcePK: 2, LogicalID: "doc_prev", SnapshotID: 5,
|
|
EventID: "commit:prev", Payload: body, SourceSchema: 37,
|
|
}})
|
|
job := NewJob("prev", "previous-release", "scratch-prev", 37)
|
|
st, err := NewMemoryRunner(enum, nil).RunBatch(ctx, job, artifactvault.KindAgentCommit, 0, 50)
|
|
if err != nil || st.Succeeded != 1 {
|
|
return fmt.Errorf("previous: %+v %v", st, err)
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("hazard", func() error {
|
|
enum := NewMemoryEnumerator()
|
|
enum.Seed(artifactvault.KindAgentCommit, []artifactvault.Candidate{
|
|
{Kind: artifactvault.KindAgentCommit, SourcePK: 3, LogicalID: "missing", SnapshotID: 0, Payload: body},
|
|
{Kind: artifactvault.KindAgentCommit, SourcePK: artifactvault.HighIDThreshold + 2, LogicalID: "high", SnapshotID: 1, Payload: body},
|
|
})
|
|
enum.Seed(artifactvault.KindApproved, []artifactvault.Candidate{{
|
|
Kind: artifactvault.KindApproved, SourcePK: 4, LogicalID: "fork", SnapshotID: 8, HeadSnapshotID: 9,
|
|
LedgerSHA: "aa", ProjectionSHA: "bb", Payload: body, ApprovedAt: &approvedAt,
|
|
}})
|
|
enum.Seed(artifactvault.KindSnapshot, []artifactvault.Candidate{{
|
|
Kind: artifactvault.KindSnapshot, SourcePK: 5, LogicalID: "legacy", SchemaVer: 0, PayloadSHA: "abc", Payload: body,
|
|
}})
|
|
reg := artifactvault.NewMemoryRegistry()
|
|
run := NewMemoryRunner(enum, reg)
|
|
job := NewJob("hazard", "dev", "scratch-hazard", 38)
|
|
var quar int
|
|
for _, kind := range []string{artifactvault.KindAgentCommit, artifactvault.KindApproved, artifactvault.KindSnapshot} {
|
|
st, err := run.RunBatch(ctx, job, kind, 0, 50)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
quar = st.Quarantined
|
|
}
|
|
if quar < 4 || reg.QuarantineCount(ctx) < 4 {
|
|
return fmt.Errorf("hazard quarantine=%d registry=%d", quar, reg.QuarantineCount(ctx))
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("import", func() error {
|
|
reg, err := LoadRegistryFile(registryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
approved := 0
|
|
for _, d := range reg.ListApproved() {
|
|
if d.Status == "approved" || d.Status == "imported" {
|
|
approved++
|
|
}
|
|
}
|
|
if approved < 6 || approved > 10 {
|
|
return fmt.Errorf("approved dataset count %d not in 6-10", approved)
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("reload", func() error {
|
|
enum := NewMemoryEnumerator()
|
|
enum.Seed(artifactvault.KindAgentCommit, []artifactvault.Candidate{{
|
|
Kind: artifactvault.KindAgentCommit, SourcePK: 11, LogicalID: "doc_reload", SnapshotID: 6,
|
|
EventID: "commit:reload", Payload: body,
|
|
}})
|
|
mem := artifactvault.NewMemoryRegistry()
|
|
run := NewMemoryRunner(enum, mem)
|
|
job := NewJob("reload", "dev", "scratch-reload", 38)
|
|
if _, err := run.RunBatch(ctx, job, artifactvault.KindAgentCommit, 0, 50); err != nil {
|
|
return err
|
|
}
|
|
job.Status = JobRunning
|
|
job.Checkpoints = map[string]Checkpoint{}
|
|
if _, err := run.RunBatch(ctx, job, artifactvault.KindAgentCommit, 0, 50); err != nil {
|
|
return err
|
|
}
|
|
if mem.Coverage(ctx).Required != 1 {
|
|
return fmt.Errorf("reload duplicated artifacts required=%d", mem.Coverage(ctx).Required)
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("rollback", func() error {
|
|
req := DestroyRequest{
|
|
TargetDSN: "postgres://app@127.0.0.1:5433/agent_system_dev",
|
|
TargetIdentity: "dev", Manifest: DeleteManifest{DatasetIDs: []string{"pinned-clean"}, StorageIdentity: "dev"},
|
|
RecoverableLocation: "/tmp/scratch-restore", CreatedAt: now.Add(-8 * 24 * time.Hour),
|
|
QuarantineUntil: now.Add(-time.Hour), Approval1: "alice", Approval2: "bob",
|
|
Coverage: artifactvault.Coverage{Required: 1, Verified: 1}, BackupOK: true, CorpusOK: true, VaultReady: true,
|
|
Now: now,
|
|
}
|
|
if err := EvaluateDestroy(req); err == nil {
|
|
return fmt.Errorf("destroy into agent_system_dev must fail")
|
|
}
|
|
req.TargetDSN = "postgres://app@127.0.0.1:5544/agent_rel_scratch"
|
|
req.TargetIdentity = "scratch-u9"
|
|
req.Manifest.StorageIdentity = "scratch-u9"
|
|
if err := EvaluateDestroy(req); err != nil {
|
|
return fmt.Errorf("scratch destroy with gates: %w", err)
|
|
}
|
|
req.BackupOK = false
|
|
if err := EvaluateDestroy(req); err == nil {
|
|
return fmt.Errorf("missing backup must block destroy")
|
|
}
|
|
if err := RequireScratchRestore("postgres://x/agent_system_main", "main", "aaa", "aaa", true); err == nil {
|
|
return fmt.Errorf("restore into main must fail")
|
|
}
|
|
if err := RequireScratchRestore("postgres://x/fav_scratch", "scratch-1", "abc", "abc", true); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}))
|
|
out = append(out, track("artifact-recovery", func() error {
|
|
dest := filepath.Join(os.TempDir(), fmt.Sprintf("scratch-u9-%d", now.UnixNano()))
|
|
defer func() { _ = os.RemoveAll(dest) }()
|
|
plan := artifactvault.RecoverPlan{
|
|
Mode: artifactvault.RecoverAsCollection, TargetIdentity: "scratch-db", SourceIdentity: "vault",
|
|
PlanHash: "u9-recover", HasAdmin: true, ScratchOnly: true,
|
|
}
|
|
okBody := []byte("# recovered corpus\n")
|
|
arts := []artifactvault.Artifact{{UUID: "art-ok", LogicalResourceID: "doc_ok", PayloadSHA256: artifactvault.SHA256Hex(okBody)}}
|
|
_, err := artifactvault.ApplyRecoverAsCollection(plan, dest, arts, map[string][]byte{"art-ok": okBody}, nil)
|
|
return err
|
|
}))
|
|
return out
|
|
}
|
|
|
|
func track(name string, fn func() error) TrackResult {
|
|
if err := fn(); err != nil {
|
|
return TrackResult{Name: name, OK: false, Detail: err.Error()}
|
|
}
|
|
return TrackResult{Name: name, OK: true, Detail: "PASS"}
|
|
}
|
|
|
|
func AllTracksPass(rs []TrackResult) error {
|
|
for _, r := range rs {
|
|
if !r.OK {
|
|
return fmt.Errorf("track %s: %s", r.Name, r.Detail)
|
|
}
|
|
}
|
|
if len(rs) != 8 {
|
|
return fmt.Errorf("expected 8 tracks, got %d", len(rs))
|
|
}
|
|
return nil
|
|
}
|