iop/apps/agent/internal/projectlog/record_test.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: &quota,
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")
}
})
}
})
}