Approve/commit now write FAV staging in the domain transaction, recover-apply materializes scratch collections, and unpublished or GET paths cannot mutate the registry. Co-authored-by: Cursor <cursoragent@cursor.com>
267 lines
8.8 KiB
Go
267 lines
8.8 KiB
Go
package artifactvault
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testStore(t *testing.T) *FilesystemStore {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
st, err := NewFilesystemStore(dir, "vault-id-test", []byte("0123456789abcdef0123456789abcdef"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return st
|
|
}
|
|
|
|
func TestPublisherIdempotentAndCrashSafe(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
facts := Facts{
|
|
ArtifactType: TypeAgentCommit, CandidateStatus: "committed", OutputSnapshotID: 4,
|
|
ChangeKind: "agent_commit", ChangeToSnapshotID: 4, Classification: ClassificationOK,
|
|
Payload: []byte("# final\n"),
|
|
}
|
|
req := PublishRequest{
|
|
ArtifactType: TypeAgentCommit, Environment: "dev", ProjectExternalKey: "proj-a",
|
|
LogicalResourceID: "doc_a", SourceEventKind: "agent_commit", SourceEventID: "ev-1",
|
|
Payload: []byte("# final\n"), TerminalState: "committed",
|
|
}
|
|
art1, _, err := reg.Request(ctx, facts, req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
art2, _, err := reg.Request(ctx, facts, req)
|
|
if err != nil || art1.UUID != art2.UUID {
|
|
t.Fatalf("idempotent request: %v %s %s", err, art1.UUID, art2.UUID)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cov := reg.Coverage(ctx)
|
|
if cov.Verified != 1 {
|
|
t.Fatalf("verified=%d", cov.Verified)
|
|
}
|
|
b, sha, err := pub.DegradedPayload(ctx, "dev", "doc_a", "proj-a")
|
|
if err != nil || string(b) != "# final\n" || sha == "" {
|
|
t.Fatalf("degraded: %v %q", err, b)
|
|
}
|
|
}
|
|
|
|
func TestPublisherRejectsBadSHAAndCrossProjectKeys(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
facts := Facts{
|
|
ArtifactType: TypeSealedVersion, VersionStatus: "sealed", SealedAt: ptrTime(),
|
|
HeadSnapshotID: 2, Classification: ClassificationOK, Payload: []byte("same-bytes"),
|
|
}
|
|
aReq := PublishRequest{ArtifactType: TypeSealedVersion, Environment: "dev", ProjectExternalKey: "p1", LogicalResourceID: "d1", SourceEventKind: "seal", SourceEventID: "s1", Payload: []byte("same-bytes"), TerminalState: "sealed"}
|
|
bReq := aReq
|
|
bReq.ProjectExternalKey = "p2"
|
|
bReq.LogicalResourceID = "d2"
|
|
bReq.SourceEventID = "s2"
|
|
if _, _, err := reg.Request(ctx, facts, aReq); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := reg.Request(ctx, facts, bReq); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reg.ActiveRefs(ctx, "p1", SHA256Hex([]byte("same-bytes"))) != 1 {
|
|
t.Fatal("p1 refs")
|
|
}
|
|
if reg.ActiveRefs(ctx, "p2", SHA256Hex([]byte("same-bytes"))) != 1 {
|
|
t.Fatal("p2 refs")
|
|
}
|
|
_ = reg.Tombstone(ctx, "unused", "test", "test")
|
|
}
|
|
|
|
func TestFilesystemRefusesRuntimeDataDev(t *testing.T) {
|
|
root := filepath.Join(t.TempDir(), "data", "dev")
|
|
_ = os.MkdirAll(root, 0o755)
|
|
if _, err := NewFilesystemStore(root, "id", []byte("0123456789abcdef0123456789abcdef")); err == nil {
|
|
t.Fatal("must refuse data/dev")
|
|
}
|
|
}
|
|
|
|
func TestTombstoneUnionDeny(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
now := time.Now().UTC()
|
|
facts := Facts{ArtifactType: TypeApprovedDocument, DocState: "approved", ApprovedAt: &now, ReviewSnapshotID: 8, Classification: ClassificationOK, Payload: []byte("ok body")}
|
|
req := PublishRequest{ArtifactType: TypeApprovedDocument, Environment: "dev", ProjectExternalKey: "p", LogicalResourceID: "doc", SourceEventKind: "approved", SourceEventID: "ap1", Payload: []byte("ok body"), TerminalState: "approved"}
|
|
art, _, err := reg.Request(ctx, facts, req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = reg.Tombstone(ctx, art.UUID, "legal", "admin")
|
|
if _, _, err := pub.DegradedPayload(ctx, "dev", "doc", "p"); err == nil {
|
|
t.Fatal("tombstone must deny read")
|
|
}
|
|
}
|
|
|
|
func TestLastReferencePurgeAndLegalHold(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
now := time.Now().UTC()
|
|
payload := []byte("shared-bytes")
|
|
facts := Facts{ArtifactType: TypeSealedVersion, VersionStatus: "sealed", SealedAt: &now, HeadSnapshotID: 2, Classification: ClassificationOK, Payload: payload}
|
|
req := PublishRequest{ArtifactType: TypeSealedVersion, Environment: "dev", ProjectExternalKey: "p1", LogicalResourceID: "d1", SourceEventKind: "seal", SourceEventID: "s1", Payload: payload, TerminalState: "sealed"}
|
|
if _, _, err := reg.Request(ctx, facts, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reg.ActiveRefs(ctx, "p1", SHA256Hex(payload)) != 1 {
|
|
t.Fatal("expected one active ref")
|
|
}
|
|
if err := CanPurge(false, 1); err == nil {
|
|
t.Fatal("must not purge while refs remain")
|
|
}
|
|
if err := CanPurge(true, 0); err == nil {
|
|
t.Fatal("legal hold blocks purge")
|
|
}
|
|
if err := CanPurge(false, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestFilesystemIdentityMismatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
key := []byte("0123456789abcdef0123456789abcdef")
|
|
if _, err := NewFilesystemStore(dir, "id-a", key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := NewFilesystemStore(dir, "id-b", key); err == nil {
|
|
t.Fatal("identity mismatch must fail closed")
|
|
}
|
|
}
|
|
|
|
func TestUnpublishedWithoutCommitMarkerInvisible(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
facts := Facts{
|
|
ArtifactType: TypeAgentCommit, CandidateStatus: "committed", OutputSnapshotID: 4,
|
|
ChangeKind: "agent_commit", ChangeToSnapshotID: 4, Classification: ClassificationOK,
|
|
Payload: []byte("# pending\n"),
|
|
}
|
|
req := PublishRequest{
|
|
ArtifactType: TypeAgentCommit, Environment: "dev", ProjectExternalKey: "proj-a",
|
|
LogicalResourceID: "doc_pending", SourceEventKind: "agent_commit", SourceEventID: "ev-pending",
|
|
Payload: []byte("# pending\n"), TerminalState: "committed",
|
|
}
|
|
if _, _, err := reg.Request(ctx, facts, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := pub.DegradedPayload(ctx, "dev", "doc_pending", "proj-a"); err == nil {
|
|
t.Fatal("requested but unpublished must be invisible")
|
|
}
|
|
}
|
|
|
|
func TestPublisherRetriesAfterMissingCommit(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := NewMemoryRegistry()
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: reg, Store: store}
|
|
payload := []byte("# retry-me\n")
|
|
facts := Facts{
|
|
ArtifactType: TypeAgentCommit, CandidateStatus: "committed", OutputSnapshotID: 5,
|
|
ChangeKind: "agent_commit", ChangeToSnapshotID: 5, Classification: ClassificationOK,
|
|
Payload: payload,
|
|
}
|
|
req := PublishRequest{
|
|
ArtifactType: TypeAgentCommit, Environment: "dev", ProjectExternalKey: "proj-a",
|
|
LogicalResourceID: "doc_retry", SourceEventKind: "agent_commit", SourceEventID: "ev-retry",
|
|
Payload: payload, TerminalState: "committed",
|
|
}
|
|
art, _, err := reg.Request(ctx, facts, req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.RemoveAll(filepath.Join(store.Root, "commits", art.UUID)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := pub.DegradedPayload(ctx, "dev", "doc_retry", "proj-a"); err == nil {
|
|
t.Fatal("missing commit marker must hide payload")
|
|
}
|
|
if err := reg.MarkFailed(ctx, art.UUID, "commit_missing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _, err := pub.DegradedPayload(ctx, "dev", "doc_retry", "proj-a")
|
|
if err != nil || string(b) != string(payload) {
|
|
t.Fatalf("retry after exclusive blob/manifest: %v %q", err, b)
|
|
}
|
|
}
|
|
|
|
type requestCountingReg struct {
|
|
Registry
|
|
requests int
|
|
}
|
|
|
|
func (r *requestCountingReg) Request(ctx context.Context, facts Facts, req PublishRequest) (*Artifact, *Staging, error) {
|
|
r.requests++
|
|
return r.Registry.Request(ctx, facts, req)
|
|
}
|
|
|
|
func TestDegradedPayloadDoesNotWrite(t *testing.T) {
|
|
ctx := context.Background()
|
|
mem := NewMemoryRegistry()
|
|
count := &requestCountingReg{Registry: mem}
|
|
store := testStore(t)
|
|
pub := &Publisher{Reg: count, Store: store}
|
|
facts := Facts{
|
|
ArtifactType: TypeAgentCommit, CandidateStatus: "committed", OutputSnapshotID: 6,
|
|
ChangeKind: "agent_commit", ChangeToSnapshotID: 6, Classification: ClassificationOK,
|
|
Payload: []byte("# get\n"),
|
|
}
|
|
req := PublishRequest{
|
|
ArtifactType: TypeAgentCommit, Environment: "dev", ProjectExternalKey: "proj-a",
|
|
LogicalResourceID: "doc_get", SourceEventKind: "agent_commit", SourceEventID: "ev-get",
|
|
Payload: []byte("# get\n"), TerminalState: "committed",
|
|
}
|
|
if _, _, err := mem.Request(ctx, facts, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pub.Drain(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
count.requests = 0
|
|
if _, _, err := pub.DegradedPayload(ctx, "dev", "doc_get", "proj-a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count.requests != 0 {
|
|
t.Fatalf("GET/degraded read wrote Request %d times", count.requests)
|
|
}
|
|
}
|
|
|
|
func ptrTime() *time.Time { t := time.Now().UTC(); return &t }
|