563 lines
19 KiB
Go
563 lines
19 KiB
Go
package projectlog
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"iop/packages/go/agentpolicy"
|
|
"iop/packages/go/agentprovider/cli/status"
|
|
"iop/packages/go/agenttask"
|
|
)
|
|
|
|
var recordTestNow = time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
|
|
|
|
func validQuotaObservation() agentpolicy.QuotaObservation {
|
|
snapshot := status.NormalizeQuotaSnapshot(
|
|
"prov-1",
|
|
"prof-1",
|
|
nil,
|
|
recordTestNow,
|
|
nil,
|
|
nil,
|
|
)
|
|
return agentpolicy.NormalizeQuotaObservation(snapshot, recordTestNow, time.Minute)
|
|
}
|
|
|
|
func validRecord() ProjectLogRecord {
|
|
quota := validQuotaObservation()
|
|
return ProjectLogRecord{
|
|
SchemaVersion: RecordSchemaVersion,
|
|
RecordID: "rec-001",
|
|
Sequence: 1,
|
|
LoopOrdinal: 1,
|
|
DispatchOrdinal: 1,
|
|
ProjectID: agenttask.ProjectID("proj-001"),
|
|
WorkspaceID: agenttask.WorkspaceID("ws-001"),
|
|
WorkUnitID: agenttask.WorkUnitID("work-001"),
|
|
AttemptID: agenttask.AttemptID("att-001"),
|
|
CommandID: agenttask.CommandID("cmd-001"),
|
|
EventType: agenttask.EventManualStart,
|
|
State: agenttask.WorkStateDispatching,
|
|
StateRevision: agenttask.StateRevision("rev-1"),
|
|
RouteStatus: &RouteStatus{
|
|
ProviderID: "prov-1",
|
|
ProfileID: "prof-1",
|
|
ModelID: "model-1",
|
|
RuleID: "rule-1",
|
|
ProfileRevision: "profile-rev-1",
|
|
ConfigRevision: "config-rev-1",
|
|
SelectionRevision: "selection-rev-1",
|
|
ReasonCode: "matched",
|
|
},
|
|
QuotaObservation: "a,
|
|
Locators: []agenttask.LocatorRecord{
|
|
{
|
|
Kind: agenttask.LocatorSession,
|
|
Opaque: "sess-12345",
|
|
Revision: "locator-rev-1",
|
|
ProjectID: agenttask.ProjectID("proj-001"),
|
|
WorkspaceID: agenttask.WorkspaceID("ws-001"),
|
|
WorkUnitID: agenttask.WorkUnitID("work-001"),
|
|
AttemptID: agenttask.AttemptID("att-001"),
|
|
},
|
|
},
|
|
BlockerCode: "",
|
|
Message: "Task started successfully",
|
|
Metadata: map[string]string{
|
|
"step": "initialization",
|
|
},
|
|
Timestamp: recordTestNow,
|
|
Terminal: false,
|
|
}
|
|
}
|
|
|
|
func TestRecordValidationMatrix(t *testing.T) {
|
|
t.Run("valid record passes", func(t *testing.T) {
|
|
rec := validRecord()
|
|
if err := rec.Validate(); err != nil {
|
|
t.Fatalf("expected valid record to pass, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid schema version", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.SchemaVersion = 99
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidSchemaVersion) {
|
|
t.Fatalf("expected ErrInvalidSchemaVersion, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("required record identity", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.RecordID = ""
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity for empty RecordID, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("known event state and blocker enums", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*ProjectLogRecord)
|
|
}{
|
|
{"empty event", func(rec *ProjectLogRecord) { rec.EventType = "" }},
|
|
{"unknown event", func(rec *ProjectLogRecord) { rec.EventType = "future_event" }},
|
|
{"unknown state", func(rec *ProjectLogRecord) { rec.State = "future_state" }},
|
|
{"revision without state", func(rec *ProjectLogRecord) { rec.State = "" }},
|
|
{"unknown blocker", func(rec *ProjectLogRecord) { rec.BlockerCode = "future_blocker" }},
|
|
{"terminal mismatch", func(rec *ProjectLogRecord) { rec.Terminal = true }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rec := validRecord()
|
|
test.mutate(&rec)
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("state revision is optional when exact evidence is unavailable", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.StateRevision = ""
|
|
if err := rec.Validate(); err != nil {
|
|
t.Fatalf("record with state and no revision rejected: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("route identities and reason code are strict", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*RouteStatus)
|
|
}{
|
|
{"provider required", func(route *RouteStatus) { route.ProviderID = "" }},
|
|
{"model bounded", func(route *RouteStatus) { route.ModelID = strings.Repeat("m", MaxIDLength+1) }},
|
|
{"profile revision required", func(route *RouteStatus) { route.ProfileRevision = "" }},
|
|
{"config revision required", func(route *RouteStatus) { route.ConfigRevision = "" }},
|
|
{"reason lowercase", func(route *RouteStatus) { route.ReasonCode = "Matched Route" }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rec := validRecord()
|
|
test.mutate(rec.RouteStatus)
|
|
if err := rec.Validate(); err == nil {
|
|
t.Fatal("expected route validation error")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("quota observation accepts sealed and canonical corrupt values", func(t *testing.T) {
|
|
rec := validRecord()
|
|
corrupt := agentpolicy.CorruptQuotaObservation()
|
|
rec.QuotaObservation = &corrupt
|
|
if err := rec.Validate(); err != nil {
|
|
t.Fatalf("canonical corrupt quota observation rejected: %v", err)
|
|
}
|
|
|
|
rec = validRecord()
|
|
invalid := agentpolicy.QuotaObservation{Validity: agentpolicy.ObservationValid}
|
|
rec.QuotaObservation = &invalid
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected invalid unsealed quota rejection, got: %v", err)
|
|
}
|
|
|
|
rec = validRecord()
|
|
oversizedSnapshot := status.NormalizeQuotaSnapshot(
|
|
strings.Repeat("a", MaxIDLength+1),
|
|
"prof-1",
|
|
nil,
|
|
recordTestNow,
|
|
nil,
|
|
nil,
|
|
)
|
|
oversizedQuota := agentpolicy.NormalizeQuotaObservation(oversizedSnapshot, recordTestNow, time.Minute)
|
|
rec.QuotaObservation = &oversizedQuota
|
|
if err := rec.Validate(); !errors.Is(err, ErrUnboundedField) {
|
|
t.Fatalf("expected ErrUnboundedField for sealed oversized adapter, got: %v", err)
|
|
}
|
|
|
|
rec = validRecord()
|
|
sensitiveSnapshot := status.NormalizeQuotaSnapshot(
|
|
"prov-1",
|
|
"Bearer provider-secret",
|
|
nil,
|
|
recordTestNow,
|
|
nil,
|
|
nil,
|
|
)
|
|
sensitiveQuota := agentpolicy.NormalizeQuotaObservation(sensitiveSnapshot, recordTestNow, time.Minute)
|
|
rec.QuotaObservation = &sensitiveQuota
|
|
if err := rec.Validate(); !errors.Is(err, ErrSensitiveContent) {
|
|
t.Fatalf("expected ErrSensitiveContent for sealed sensitive target, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("locator identity matrix", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*agenttask.LocatorRecord)
|
|
}{
|
|
{"unknown kind", func(locator *agenttask.LocatorRecord) { locator.Kind = "future_locator" }},
|
|
{"empty opaque", func(locator *agenttask.LocatorRecord) { locator.Opaque = "" }},
|
|
{"missing revision", func(locator *agenttask.LocatorRecord) { locator.Revision = "" }},
|
|
{"project drift", func(locator *agenttask.LocatorRecord) { locator.ProjectID = "other-project" }},
|
|
{"workspace drift", func(locator *agenttask.LocatorRecord) { locator.WorkspaceID = "other-workspace" }},
|
|
{"work drift", func(locator *agenttask.LocatorRecord) { locator.WorkUnitID = "other-work" }},
|
|
{"attempt drift", func(locator *agenttask.LocatorRecord) { locator.AttemptID = "other-attempt" }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rec := validRecord()
|
|
test.mutate(&rec.Locators[0])
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("empty or whitespace project identity", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.ProjectID = ""
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity for empty ProjectID, got: %v", err)
|
|
}
|
|
|
|
rec = validRecord()
|
|
rec.WorkspaceID = " ws-001 "
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity for padded WorkspaceID, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("zero timestamp", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Timestamp = time.Time{}
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidTimestamp) {
|
|
t.Fatalf("expected ErrInvalidTimestamp, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("archive manifest validation and creation", func(t *testing.T) {
|
|
now := recordTestNow
|
|
rec1 := validRecord()
|
|
rec1.Sequence = 10
|
|
rec2 := validRecord()
|
|
rec2.RecordID = "rec-002"
|
|
rec2.Sequence = 11
|
|
rec2.EventType = agenttask.EventCompleted
|
|
rec2.State = agenttask.WorkStateCompleted
|
|
rec2.StateRevision = "rev-2"
|
|
rec2.Terminal = true
|
|
|
|
manifest, err := NewArchiveManifest("proj-001", "ws-001", 1, []ProjectLogRecord{rec1, rec2}, now, true)
|
|
if err != nil {
|
|
t.Fatalf("NewArchiveManifest failed: %v", err)
|
|
}
|
|
|
|
if manifest.RecordCount != 2 {
|
|
t.Errorf("expected RecordCount 2, got %d", manifest.RecordCount)
|
|
}
|
|
if manifest.FirstSequence != 10 || manifest.LastSequence != 11 {
|
|
t.Errorf("expected sequence 10-11, got %d-%d", manifest.FirstSequence, manifest.LastSequence)
|
|
}
|
|
if !strings.HasPrefix(manifest.Checksum, "sha256:") {
|
|
t.Errorf("expected sha256 checksum prefix, got %q", manifest.Checksum)
|
|
}
|
|
if !manifest.Terminal {
|
|
t.Error("manifest terminal = false, want derived true")
|
|
}
|
|
|
|
manifest.SchemaVersion = 999
|
|
if err := manifest.Validate(); !errors.Is(err, ErrInvalidSchemaVersion) {
|
|
t.Fatalf("expected ErrInvalidSchemaVersion for manifest, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRecordRejectsSensitiveOrUnboundedFields(t *testing.T) {
|
|
t.Run("oversized message rejected", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Message = strings.Repeat("a", MaxMessageLength+1)
|
|
if err := rec.Validate(); !errors.Is(err, ErrUnboundedField) {
|
|
t.Fatalf("expected ErrUnboundedField for oversized message, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("oversized metadata keys count rejected", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Metadata = make(map[string]string)
|
|
for i := 0; i < MaxMetadataKeys+1; i++ {
|
|
rec.Metadata[string(rune('a'+i))] = "val"
|
|
}
|
|
if err := rec.Validate(); !errors.Is(err, ErrUnboundedField) {
|
|
t.Fatalf("expected ErrUnboundedField for oversized metadata keys, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("oversized locators count rejected", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Locators = make([]agenttask.LocatorRecord, MaxLocatorsCount+1)
|
|
for i := range rec.Locators {
|
|
rec.Locators[i] = agenttask.LocatorRecord{
|
|
Kind: agenttask.LocatorProcess,
|
|
Opaque: "proc",
|
|
Revision: "locator-rev",
|
|
ProjectID: rec.ProjectID,
|
|
WorkspaceID: rec.WorkspaceID,
|
|
WorkUnitID: rec.WorkUnitID,
|
|
AttemptID: rec.AttemptID,
|
|
}
|
|
}
|
|
if err := rec.Validate(); !errors.Is(err, ErrUnboundedField) {
|
|
t.Fatalf("expected ErrUnboundedField for oversized locators count, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("secret tokens in message rejected", func(t *testing.T) {
|
|
secrets := []string{
|
|
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
"API token is sk-proj-1234567890abcdef",
|
|
"GitHub token ghp_123456789012345678901234567890123456",
|
|
"AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
"-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3...",
|
|
}
|
|
for _, sec := range secrets {
|
|
rec := validRecord()
|
|
rec.Message = sec
|
|
if err := rec.Validate(); !errors.Is(err, ErrSensitiveContent) {
|
|
t.Errorf("expected ErrSensitiveContent for message %q, got: %v", sec, err)
|
|
}
|
|
}
|
|
|
|
rec := validRecord()
|
|
rec.RouteStatus.SelectionRevision = ""
|
|
rec.RouteStatus.ReasonCode = ""
|
|
if err := rec.Validate(); err != nil {
|
|
t.Fatalf("optional selection evidence rejected: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("change-set and integration evidence are exact when present", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.ChangeSetID = "change-1"
|
|
rec.ChangeSetRevision = "change-rev-1"
|
|
rec.IntegrationAttempt = 2
|
|
rec.IntegrationOutcome = agenttask.IntegrationOutcomeIntegrated
|
|
if err := rec.Validate(); err != nil {
|
|
t.Fatalf("exact integration evidence rejected: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*ProjectLogRecord)
|
|
}{
|
|
{"revision without change set", func(rec *ProjectLogRecord) {
|
|
rec.ChangeSetRevision = "change-rev-1"
|
|
}},
|
|
{"change set without revision", func(rec *ProjectLogRecord) {
|
|
rec.ChangeSetID = "change-1"
|
|
}},
|
|
{"outcome without attempt", func(rec *ProjectLogRecord) {
|
|
rec.ChangeSetID = "change-1"
|
|
rec.ChangeSetRevision = "change-rev-1"
|
|
rec.IntegrationOutcome = agenttask.IntegrationOutcomeIntegrated
|
|
}},
|
|
{"unknown outcome", func(rec *ProjectLogRecord) {
|
|
rec.ChangeSetID = "change-1"
|
|
rec.ChangeSetRevision = "change-rev-1"
|
|
rec.IntegrationAttempt = 1
|
|
rec.IntegrationOutcome = "future"
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.ChangeSetID = ""
|
|
rec.ChangeSetRevision = ""
|
|
rec.IntegrationAttempt = 0
|
|
rec.IntegrationOutcome = ""
|
|
test.mutate(&rec)
|
|
if err := rec.Validate(); !errors.Is(err, ErrInvalidIdentity) {
|
|
t.Fatalf("expected ErrInvalidIdentity, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("raw environment dump in message rejected", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Message = "Environment PATH=/usr/local/bin:/usr/bin:/bin"
|
|
if err := rec.Validate(); !errors.Is(err, ErrSensitiveContent) {
|
|
t.Fatalf("expected ErrSensitiveContent for raw env in message, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("sensitive metadata keys rejected", func(t *testing.T) {
|
|
sensitiveKeys := []string{"api_key", "my_password", "auth_token", "env", "stderr_output"}
|
|
for _, k := range sensitiveKeys {
|
|
rec := validRecord()
|
|
rec.Metadata = map[string]string{k: "value"}
|
|
if err := rec.Validate(); !errors.Is(err, ErrSensitiveContent) {
|
|
t.Errorf("expected ErrSensitiveContent for metadata key %q, got: %v", k, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("sensitive locator opaque rejected", func(t *testing.T) {
|
|
rec := validRecord()
|
|
rec.Locators = []agenttask.LocatorRecord{
|
|
{
|
|
Kind: agenttask.LocatorProcess,
|
|
Opaque: "proc-with-sk-secret-token",
|
|
Revision: "locator-rev",
|
|
ProjectID: rec.ProjectID,
|
|
WorkspaceID: rec.WorkspaceID,
|
|
WorkUnitID: rec.WorkUnitID,
|
|
AttemptID: rec.AttemptID,
|
|
},
|
|
}
|
|
if err := rec.Validate(); !errors.Is(err, ErrSensitiveContent) {
|
|
t.Fatalf("expected ErrSensitiveContent for sensitive locator opaque, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func terminalArchiveRecords() []ProjectLogRecord {
|
|
first := validRecord()
|
|
first.RecordID = "rec-010"
|
|
first.Sequence = 10
|
|
|
|
last := validRecord()
|
|
last.RecordID = "rec-011"
|
|
last.Sequence = 11
|
|
last.EventType = agenttask.EventCompleted
|
|
last.State = agenttask.WorkStateCompleted
|
|
last.StateRevision = "rev-2"
|
|
last.Terminal = true
|
|
return []ProjectLogRecord{first, last}
|
|
}
|
|
|
|
func TestArchiveManifestValidationMatrix(t *testing.T) {
|
|
t.Run("valid archive is deterministic", func(t *testing.T) {
|
|
records := terminalArchiveRecords()
|
|
first, err := NewArchiveManifest("proj-001", "ws-001", 1, records, recordTestNow, true)
|
|
if err != nil {
|
|
t.Fatalf("first NewArchiveManifest() error = %v", err)
|
|
}
|
|
second, err := NewArchiveManifest("proj-001", "ws-001", 1, records, recordTestNow, true)
|
|
if err != nil {
|
|
t.Fatalf("second NewArchiveManifest() error = %v", err)
|
|
}
|
|
firstJSON, _ := json.Marshal(first)
|
|
secondJSON, _ := json.Marshal(second)
|
|
if string(firstJSON) != string(secondJSON) {
|
|
t.Fatalf("manifest is not deterministic:\n%s\n%s", firstJSON, secondJSON)
|
|
}
|
|
})
|
|
|
|
t.Run("constructor rejects membership order and terminal drift", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ordinal uint64
|
|
terminal bool
|
|
mutate func([]ProjectLogRecord) []ProjectLogRecord
|
|
}{
|
|
{"empty archive", 1, true, func([]ProjectLogRecord) []ProjectLogRecord { return nil }},
|
|
{"zero ordinal", 0, true, func(records []ProjectLogRecord) []ProjectLogRecord { return records }},
|
|
{"project drift", 1, true, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[0].ProjectID = "other-project"
|
|
records[0].Locators[0].ProjectID = "other-project"
|
|
return records
|
|
}},
|
|
{"workspace drift", 1, true, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[0].WorkspaceID = "other-workspace"
|
|
records[0].Locators[0].WorkspaceID = "other-workspace"
|
|
return records
|
|
}},
|
|
{"duplicate sequence", 1, true, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[1].Sequence = records[0].Sequence
|
|
return records
|
|
}},
|
|
{"descending sequence", 1, true, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[1].Sequence = records[0].Sequence - 1
|
|
return records
|
|
}},
|
|
{"early terminal", 1, true, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[0].EventType = agenttask.EventCompleted
|
|
records[0].State = agenttask.WorkStateCompleted
|
|
records[0].Terminal = true
|
|
return records
|
|
}},
|
|
{"missing final terminal", 1, false, func(records []ProjectLogRecord) []ProjectLogRecord {
|
|
records[1].EventType = agenttask.EventDispatchStarted
|
|
records[1].State = agenttask.WorkStateDispatching
|
|
records[1].Terminal = false
|
|
return records
|
|
}},
|
|
{"caller terminal mismatch", 1, false, func(records []ProjectLogRecord) []ProjectLogRecord { return records }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
records := test.mutate(terminalArchiveRecords())
|
|
if _, err := NewArchiveManifest(
|
|
"proj-001",
|
|
"ws-001",
|
|
test.ordinal,
|
|
records,
|
|
recordTestNow,
|
|
test.terminal,
|
|
); err == nil {
|
|
t.Fatal("expected archive construction error")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("decoded manifest shape is strict", func(t *testing.T) {
|
|
base, err := NewArchiveManifest(
|
|
"proj-001",
|
|
"ws-001",
|
|
1,
|
|
terminalArchiveRecords(),
|
|
recordTestNow,
|
|
true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewArchiveManifest() error = %v", err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*ArchiveManifest)
|
|
}{
|
|
{"zero ordinal", func(manifest *ArchiveManifest) { manifest.ArchiveOrdinal = 0 }},
|
|
{"empty record count", func(manifest *ArchiveManifest) { manifest.RecordCount = 0 }},
|
|
{"inverted endpoints", func(manifest *ArchiveManifest) { manifest.LastSequence = manifest.FirstSequence - 1 }},
|
|
{"uppercase checksum", func(manifest *ArchiveManifest) { manifest.Checksum = strings.ToUpper(manifest.Checksum) }},
|
|
{"short checksum", func(manifest *ArchiveManifest) { manifest.Checksum = "sha256:abcd" }},
|
|
{"non-terminal", func(manifest *ArchiveManifest) { manifest.Terminal = false }},
|
|
{"empty work unit", func(manifest *ArchiveManifest) { manifest.WorkUnitIDs[0] = "" }},
|
|
{"oversized work unit", func(manifest *ArchiveManifest) {
|
|
manifest.WorkUnitIDs[0] = agenttask.WorkUnitID(strings.Repeat("w", MaxIDLength+1))
|
|
}},
|
|
{"duplicate work unit", func(manifest *ArchiveManifest) {
|
|
manifest.WorkUnitIDs = append(manifest.WorkUnitIDs, manifest.WorkUnitIDs[0])
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
manifest := base
|
|
manifest.WorkUnitIDs = append([]agenttask.WorkUnitID(nil), base.WorkUnitIDs...)
|
|
test.mutate(&manifest)
|
|
if err := manifest.Validate(); err == nil {
|
|
t.Fatal("expected manifest validation error")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|