Files
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

108 lines
3.6 KiB
Go

package artifactvault
import (
"testing"
"time"
)
func TestEvaluateEligibility_Allowlist(t *testing.T) {
ok := Facts{
ArtifactType: TypeAgentCommit, CandidateStatus: "committed", OutputSnapshotID: 9,
ChangeKind: "agent_commit", ChangeToSnapshotID: 9, Classification: ClassificationOK, Payload: []byte("doc"),
}
if err := EvaluateEligibility(ok); err != nil {
t.Fatalf("committed should pass: %v", err)
}
for _, status := range []string{"ready", "needs_confirmation", "rejected", "stale", "quality_failed"} {
bad := ok
bad.CandidateStatus = status
if err := EvaluateEligibility(bad); err == nil {
t.Fatalf("status %s must reject", status)
}
}
}
func TestEvaluateEligibility_ApprovedRequiresReviewSnapshot(t *testing.T) {
now := time.Now().UTC()
f := Facts{
ArtifactType: TypeApprovedDocument, DocState: "approved", ApprovedAt: &now,
Classification: ClassificationOK, Payload: []byte("approved"),
}
if err := EvaluateEligibility(f); err == nil {
t.Fatal("approved without review_snapshot_id must reject")
}
f.ReviewSnapshotID = 3
if err := EvaluateEligibility(f); err != nil {
t.Fatalf("approved with snapshot should pass: %v", err)
}
}
func TestEvaluateEligibility_TaskWithoutCompletedTurn(t *testing.T) {
f := Facts{
ArtifactType: TypeAssistantReply, ChatTurnStatus: "", AssistantMessageID: 8,
ExportPolicy: "full", Classification: ClassificationOK, Payload: []byte("hello world from assistant"),
}
if err := EvaluateEligibility(f); err == nil {
t.Fatal("missing completed turn must reject")
}
if err := EvaluateEligibility(Facts{
ArtifactType: TypeAssistantReply, ExplicitPin: true, ChatTurnStatus: "pending",
Classification: ClassificationOK, Payload: []byte("hello world from assistant"),
}); err != ErrExplicitBypass {
t.Fatalf("pin before completed: %v", err)
}
}
func TestEvaluateEligibility_SensitiveRejectedBeforeStaging(t *testing.T) {
now := time.Now().UTC()
f := Facts{
ArtifactType: TypeAssistantReply, ChatTurnStatus: "completed", CompletedAt: &now,
AssistantMessageID: 1, ExportPolicy: "full", Payload: []byte("api_key=sk-secret"),
}
if err := EvaluateEligibility(f); err != ErrSensitiveRejected {
t.Fatalf("secret payload: %v", err)
}
}
func TestEvaluateEligibility_CopyOnlyRequiresExplicitPinAfterCompleted(t *testing.T) {
now := time.Now().UTC()
base := Facts{
ArtifactType: TypeAssistantReply, ChatTurnStatus: "completed", CompletedAt: &now,
AssistantMessageID: 9, ExportPolicy: "copy_only", Classification: ClassificationOK,
Payload: []byte("ok, noted."),
}
if err := EvaluateEligibility(base); err == nil {
t.Fatal("copy_only without pin must reject")
}
base.ExplicitPin = true
if err := EvaluateEligibility(base); err != ErrExplicitBypass {
t.Fatalf("pin without PinAfterCompleted: %v", err)
}
base.PinAfterCompleted = true
if err := EvaluateEligibility(base); err != nil {
t.Fatalf("copy_only + pin after completed should pass: %v", err)
}
}
func TestEvaluateEligibility_UnknownTypeRejected(t *testing.T) {
if err := EvaluateEligibility(Facts{ArtifactType: "workspace_scan", Classification: ClassificationOK, Payload: []byte("x")}); err != ErrUnknownArtifactType {
t.Fatalf("got %v", err)
}
}
func TestIdempotencyKeyStable(t *testing.T) {
req := PublishRequest{
ArtifactType: TypeAgentCommit, Environment: "dev", ProjectExternalKey: "p1",
LogicalResourceID: "doc_a", SourceEventKind: "agent_commit", SourceEventID: "ev1",
}
a := IdempotencyKey(req, "abc")
b := IdempotencyKey(req, "abc")
if a != b || a == "" {
t.Fatalf("key unstable %q %q", a, b)
}
req.SourceEventID = "ev2"
if IdempotencyKey(req, "abc") == a {
t.Fatal("new event must new key")
}
}