Files
Agent-System/internal/artifactvault/publisher_test.go
T
weichenandCursor ad9b67d599 feat(artifactvault): close same-tx staging, recover-apply, and GET zero-write gaps
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>
2026-09-14 12:52:28 +00:00

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 }