798 lines
25 KiB
Go
798 lines
25 KiB
Go
package projectlog
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"iop/packages/go/agentstate"
|
|
"iop/packages/go/agenttask"
|
|
)
|
|
|
|
type sinkTestHarness struct {
|
|
manager *agentstate.Store
|
|
store *Store
|
|
sink *Sink
|
|
project agenttask.ProjectRecord
|
|
}
|
|
|
|
type replayFailingEvidenceResolver struct {
|
|
evidence EventEvidence
|
|
calls int
|
|
}
|
|
|
|
func (r *replayFailingEvidenceResolver) ResolveEventEvidence(
|
|
_ context.Context,
|
|
_ agenttask.Event,
|
|
) (EventEvidence, error) {
|
|
r.calls++
|
|
if r.calls > 1 {
|
|
return EventEvidence{}, errors.New("resolver must not be called for stable replay")
|
|
}
|
|
return r.evidence, nil
|
|
}
|
|
|
|
func newSinkTestHarness(
|
|
t *testing.T,
|
|
projectID agenttask.ProjectID,
|
|
workspaceID agenttask.WorkspaceID,
|
|
) *sinkTestHarness {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
manager, err := agentstate.NewStore(filepath.Join(root, "manager-state.json"))
|
|
if err != nil {
|
|
t.Fatalf("agentstate.NewStore manager: %v", err)
|
|
}
|
|
journalState, err := agentstate.NewStore(filepath.Join(root, "projectlog-state.json"))
|
|
if err != nil {
|
|
t.Fatalf("agentstate.NewStore projectlog: %v", err)
|
|
}
|
|
store, err := NewStore(journalState, filepath.Join(root, "archives"), projectID, workspaceID)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
resolver, err := NewStateStoreEvidenceResolver(manager)
|
|
if err != nil {
|
|
t.Fatalf("NewStateStoreEvidenceResolver: %v", err)
|
|
}
|
|
sink, err := NewSink(store, resolver)
|
|
if err != nil {
|
|
t.Fatalf("NewSink: %v", err)
|
|
}
|
|
return &sinkTestHarness{
|
|
manager: manager,
|
|
store: store,
|
|
sink: sink,
|
|
project: agenttask.ProjectRecord{
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
Status: agenttask.ProjectStatusRunning,
|
|
Intent: &agenttask.StartIntent{
|
|
CommandID: "cmd-001",
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
MilestoneID: "milestone-001",
|
|
WorkflowRevision: "workflow-rev-1",
|
|
ConfigRevision: "config-rev-1",
|
|
GrantRevision: "grant-rev-1",
|
|
StartedAt: time.Date(2026, 7, 30, 9, 0, 0, 0, time.UTC),
|
|
},
|
|
Works: make(map[agenttask.WorkUnitID]agenttask.WorkRecord),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (h *sinkTestHarness) putWork(t *testing.T, work agenttask.WorkRecord) agenttask.StateRevision {
|
|
t.Helper()
|
|
h.project.Works[work.Unit.ID] = work
|
|
state, revision, err := h.manager.Load(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("manager Load: %v", err)
|
|
}
|
|
if state.Projects == nil {
|
|
state.Projects = make(map[agenttask.ProjectID]agenttask.ProjectRecord)
|
|
}
|
|
state.Projects[h.project.ProjectID] = h.project
|
|
next, err := h.manager.CompareAndSwap(context.Background(), revision, state)
|
|
if err != nil {
|
|
t.Fatalf("manager CompareAndSwap: %v", err)
|
|
}
|
|
return next
|
|
}
|
|
|
|
func sinkTestWork(
|
|
projectID agenttask.ProjectID,
|
|
workspaceID agenttask.WorkspaceID,
|
|
workUnitID agenttask.WorkUnitID,
|
|
attempt uint32,
|
|
state agenttask.WorkState,
|
|
) agenttask.WorkRecord {
|
|
attemptID := agenttask.AttemptID("attempt-" + string(rune('0'+attempt)))
|
|
return agenttask.WorkRecord{
|
|
Unit: agenttask.WorkUnit{
|
|
ID: workUnitID,
|
|
MilestoneID: "milestone-001",
|
|
WriteSetKind: agenttask.WriteSetDisjoint,
|
|
IsolationMode: "overlay",
|
|
},
|
|
State: state,
|
|
Attempt: attempt,
|
|
AttemptID: attemptID,
|
|
DispatchOrdinal: 17,
|
|
Target: &agenttask.ExecutionTarget{
|
|
ProviderID: "provider-exact",
|
|
ModelID: "model-exact",
|
|
ProfileID: "profile-exact",
|
|
ProfileRevision: "profile-rev-exact",
|
|
ConfigRevision: "config-rev-exact",
|
|
Capacity: 2,
|
|
},
|
|
Locators: map[agenttask.LocatorKind]agenttask.LocatorRecord{
|
|
agenttask.LocatorSession: {
|
|
Kind: agenttask.LocatorSession,
|
|
Opaque: "session-exact",
|
|
Revision: "session-rev-exact",
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
WorkUnitID: workUnitID,
|
|
AttemptID: attemptID,
|
|
},
|
|
agenttask.LocatorProcess: {
|
|
Kind: agenttask.LocatorProcess,
|
|
Opaque: "process-exact",
|
|
Revision: "process-rev-exact",
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
WorkUnitID: workUnitID,
|
|
AttemptID: attemptID,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestSinkMapsEveryEventType(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-sink-1", "ws-sink-1")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-001",
|
|
1,
|
|
agenttask.WorkStateReviewing,
|
|
)
|
|
harness.putWork(t, work)
|
|
|
|
eventTypes := []agenttask.EventType{
|
|
agenttask.EventObserved,
|
|
agenttask.EventManualStart,
|
|
agenttask.EventAutoResume,
|
|
agenttask.EventStopped,
|
|
agenttask.EventDependencyReady,
|
|
agenttask.EventDispatchStarted,
|
|
agenttask.EventSubmissionAccepted,
|
|
agenttask.EventReviewResult,
|
|
agenttask.EventFollowup,
|
|
agenttask.EventIntegrationResult,
|
|
agenttask.EventBlocked,
|
|
agenttask.EventCompleted,
|
|
}
|
|
now := time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC)
|
|
for index, eventType := range eventTypes {
|
|
event := agenttask.Event{
|
|
EventID: "event-" + string(rune('a'+index)),
|
|
Type: eventType,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
CommandID: harness.project.Intent.CommandID,
|
|
Timestamp: now.Add(time.Duration(index) * time.Minute),
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("Emit %s: %v", eventType, err)
|
|
}
|
|
}
|
|
|
|
scoped, err := harness.store.ForWorkUnit(work.Unit.ID)
|
|
if err != nil {
|
|
t.Fatalf("ForWorkUnit: %v", err)
|
|
}
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ReplayRecords: %v", err)
|
|
}
|
|
if len(records) != len(eventTypes) {
|
|
t.Fatalf("record count = %d, want %d", len(records), len(eventTypes))
|
|
}
|
|
for index, record := range records {
|
|
if record.EventType != eventTypes[index] {
|
|
t.Fatalf("record %d type = %s, want %s", index, record.EventType, eventTypes[index])
|
|
}
|
|
if record.State != agenttask.WorkStateReviewing {
|
|
t.Fatalf("record %d inferred state %q", index, record.State)
|
|
}
|
|
if record.DispatchOrdinal != work.DispatchOrdinal || record.LoopOrdinal != uint64(work.Attempt) {
|
|
t.Fatalf("record %d lost exact ordinal evidence: %+v", index, record)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSinkResolvesExactDurableEvidence(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-exact", "ws-exact")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-exact",
|
|
3,
|
|
agenttask.WorkStateTerminalDeferred,
|
|
)
|
|
work.ChangeSet = &agenttask.ChangeSetIdentity{
|
|
ID: "change-exact",
|
|
Revision: "change-rev-exact",
|
|
ArtifactID: "artifact-exact",
|
|
}
|
|
work.IntegrationAttempt = 4
|
|
work.Integration = &agenttask.IntegrationResult{
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkUnitID: work.Unit.ID,
|
|
ChangeSet: *work.ChangeSet,
|
|
Ordinal: work.DispatchOrdinal,
|
|
Attempt: 4,
|
|
Outcome: agenttask.IntegrationOutcomeTerminalDeferred,
|
|
Retained: true,
|
|
BeforeRevision: "base-before",
|
|
Blocker: &agenttask.Blocker{
|
|
Code: agenttask.BlockerIntegrationFailed,
|
|
Message: "safe integration failure",
|
|
},
|
|
}
|
|
work.Blocker = work.Integration.Blocker
|
|
wantRevision := harness.putWork(t, work)
|
|
|
|
event := agenttask.Event{
|
|
EventID: "manager-event-exact",
|
|
Type: agenttask.EventIntegrationResult,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
ChangeSetID: work.ChangeSet.ID,
|
|
ChangeSetRevision: work.ChangeSet.Revision,
|
|
IntegrationAttempt: work.IntegrationAttempt,
|
|
State: work.State,
|
|
ProviderID: work.Target.ProviderID,
|
|
ProfileID: work.Target.ProfileID,
|
|
Detail: "terminal integration evidence",
|
|
Timestamp: time.Date(2026, 7, 30, 11, 0, 0, 0, time.UTC),
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("Emit: %v", err)
|
|
}
|
|
scoped, _ := harness.store.ForWorkUnit(work.Unit.ID)
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil || len(records) != 1 {
|
|
t.Fatalf("ReplayRecords = %d, %v", len(records), err)
|
|
}
|
|
record := records[0]
|
|
if record.StateRevision != wantRevision ||
|
|
record.LoopOrdinal != uint64(work.Attempt) ||
|
|
record.AttemptID != work.AttemptID ||
|
|
record.DispatchOrdinal != work.DispatchOrdinal {
|
|
t.Fatalf("exact state/attempt evidence lost: %+v", record)
|
|
}
|
|
if record.RouteStatus == nil ||
|
|
record.RouteStatus.ProviderID != work.Target.ProviderID ||
|
|
record.RouteStatus.ModelID != work.Target.ModelID ||
|
|
record.RouteStatus.SelectionRevision != "" ||
|
|
record.RouteStatus.ReasonCode != "" {
|
|
t.Fatalf("route evidence was fabricated or lost: %+v", record.RouteStatus)
|
|
}
|
|
if record.ChangeSetID != work.ChangeSet.ID ||
|
|
record.ChangeSetRevision != work.ChangeSet.Revision ||
|
|
record.IntegrationAttempt != work.IntegrationAttempt ||
|
|
record.IntegrationOutcome != work.Integration.Outcome ||
|
|
record.BlockerCode != work.Blocker.Code {
|
|
t.Fatalf("change-set/integration evidence lost: %+v", record)
|
|
}
|
|
if len(record.Locators) != 2 ||
|
|
record.Locators[0].Kind != agenttask.LocatorProcess ||
|
|
record.Locators[1].Kind != agenttask.LocatorSession {
|
|
t.Fatalf("locators are not exact and sorted: %+v", record.Locators)
|
|
}
|
|
}
|
|
|
|
func TestSinkAllowsProjectEventWithoutWorkEvidence(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-project-event", "ws-project-event")
|
|
state, revision, err := harness.manager.Load(ctx)
|
|
if err != nil {
|
|
t.Fatalf("manager Load: %v", err)
|
|
}
|
|
state.Projects = map[agenttask.ProjectID]agenttask.ProjectRecord{
|
|
harness.project.ProjectID: harness.project,
|
|
}
|
|
if _, err := harness.manager.CompareAndSwap(ctx, revision, state); err != nil {
|
|
t.Fatalf("manager CompareAndSwap: %v", err)
|
|
}
|
|
event := agenttask.Event{
|
|
EventID: "project-event-manual-start",
|
|
Type: agenttask.EventManualStart,
|
|
ProjectID: harness.project.ProjectID,
|
|
CommandID: harness.project.Intent.CommandID,
|
|
Detail: "manual project start",
|
|
Timestamp: time.Date(2026, 7, 30, 11, 30, 0, 0, time.UTC),
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("Emit project event: %v", err)
|
|
}
|
|
records, err := harness.store.ReplayRecords(ctx)
|
|
if err != nil || len(records) != 1 {
|
|
t.Fatalf("ReplayRecords = %d, %v", len(records), err)
|
|
}
|
|
record := records[0]
|
|
if record.WorkUnitID != "" ||
|
|
record.AttemptID != "" ||
|
|
record.State != "" ||
|
|
record.StateRevision != "" ||
|
|
record.RouteStatus != nil {
|
|
t.Fatalf("project event fabricated work evidence: %+v", record)
|
|
}
|
|
}
|
|
|
|
func TestSinkRejectsEvidenceIdentityDrift(t *testing.T) {
|
|
harness := newSinkTestHarness(t, "proj-drift", "ws-drift")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-drift",
|
|
2,
|
|
agenttask.WorkStateDispatching,
|
|
)
|
|
work.ChangeSet = &agenttask.ChangeSetIdentity{ID: "change-1", Revision: "change-rev-1"}
|
|
harness.putWork(t, work)
|
|
base := agenttask.Event{
|
|
EventID: "event-drift",
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
State: work.State,
|
|
ProviderID: work.Target.ProviderID,
|
|
ProfileID: work.Target.ProfileID,
|
|
Timestamp: time.Date(2026, 7, 30, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*agenttask.Event)
|
|
}{
|
|
{"workspace", func(event *agenttask.Event) { event.WorkspaceID = "ws-other" }},
|
|
{"attempt", func(event *agenttask.Event) { event.AttemptID = "attempt-other" }},
|
|
{"dispatch", func(event *agenttask.Event) { event.Ordinal++ }},
|
|
{"state", func(event *agenttask.Event) { event.State = agenttask.WorkStateReviewing }},
|
|
{"provider", func(event *agenttask.Event) { event.ProviderID = "provider-other" }},
|
|
{"change set", func(event *agenttask.Event) {
|
|
event.ChangeSetID = "change-other"
|
|
event.ChangeSetRevision = "change-rev-other"
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
event := base
|
|
test.mutate(&event)
|
|
err := harness.sink.Emit(context.Background(), event)
|
|
if !errors.Is(err, ErrEvidenceIdentityDrift) {
|
|
t.Fatalf("Emit error = %v, want ErrEvidenceIdentityDrift", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSinkPendingDeliveryUsesExactCommittedEvidence(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-pending", "ws-pending")
|
|
pendingWork := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-pending",
|
|
1,
|
|
agenttask.WorkStateReviewing,
|
|
)
|
|
pendingWork.Review = &agenttask.ReviewResult{
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkUnitID: pendingWork.Unit.ID,
|
|
AttemptID: pendingWork.AttemptID,
|
|
ArtifactID: "artifact-pending",
|
|
Verdict: agenttask.ReviewVerdictFail,
|
|
Message: "committed review",
|
|
Rework: true,
|
|
}
|
|
pendingProject := harness.project
|
|
pendingProject.Works = map[agenttask.WorkUnitID]agenttask.WorkRecord{
|
|
pendingWork.Unit.ID: pendingWork,
|
|
}
|
|
event := agenttask.Event{
|
|
EventID: "event-pending-review",
|
|
Type: agenttask.EventReviewResult,
|
|
ProjectID: pendingProject.ProjectID,
|
|
WorkspaceID: pendingProject.WorkspaceID,
|
|
WorkUnitID: pendingWork.Unit.ID,
|
|
CommandID: pendingProject.Intent.CommandID,
|
|
AttemptID: pendingWork.AttemptID,
|
|
Ordinal: pendingWork.DispatchOrdinal,
|
|
Detail: string(agenttask.ReviewVerdictFail),
|
|
Timestamp: time.Date(2026, 7, 30, 12, 30, 0, 0, time.UTC),
|
|
}
|
|
|
|
currentWork := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
pendingWork.Unit.ID,
|
|
2,
|
|
agenttask.WorkStateReady,
|
|
)
|
|
currentProject := harness.project
|
|
currentProject.Works = map[agenttask.WorkUnitID]agenttask.WorkRecord{
|
|
currentWork.Unit.ID: currentWork,
|
|
}
|
|
state, revision, err := harness.manager.Load(ctx)
|
|
if err != nil {
|
|
t.Fatalf("manager Load: %v", err)
|
|
}
|
|
state.Projects = map[agenttask.ProjectID]agenttask.ProjectRecord{
|
|
currentProject.ProjectID: currentProject,
|
|
}
|
|
state.PendingEvents = map[string]agenttask.EventDelivery{
|
|
event.EventID: {
|
|
Event: event,
|
|
EvidenceRevision: "pending-state-revision-17",
|
|
Project: &pendingProject,
|
|
Work: &pendingWork,
|
|
},
|
|
}
|
|
if _, err := harness.manager.CompareAndSwap(ctx, revision, state); err != nil {
|
|
t.Fatalf("manager CompareAndSwap: %v", err)
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("Emit pending event: %v", err)
|
|
}
|
|
scoped, err := harness.store.ForWorkUnit(pendingWork.Unit.ID)
|
|
if err != nil {
|
|
t.Fatalf("ForWorkUnit: %v", err)
|
|
}
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil || len(records) != 1 {
|
|
t.Fatalf("ReplayRecords = %d, %v", len(records), err)
|
|
}
|
|
record := records[0]
|
|
if record.StateRevision != "pending-state-revision-17" ||
|
|
record.LoopOrdinal != 1 ||
|
|
record.AttemptID != pendingWork.AttemptID ||
|
|
record.State != agenttask.WorkStateReviewing ||
|
|
record.Result != string(agenttask.ReviewVerdictFail) {
|
|
t.Fatalf("record did not use pending delivery evidence: %+v", record)
|
|
}
|
|
if len(record.Locators) != len(pendingWork.Locators) ||
|
|
record.Locators[0].AttemptID != pendingWork.AttemptID {
|
|
t.Fatalf("record locators came from advanced work: %+v", record.Locators)
|
|
}
|
|
}
|
|
|
|
func TestSinkReplayShortCircuitsEvidenceAfterClockAndStateAdvance(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
journalState, err := agentstate.NewStore(filepath.Join(root, "projectlog-state.json"))
|
|
if err != nil {
|
|
t.Fatalf("agentstate.NewStore: %v", err)
|
|
}
|
|
store, err := NewStore(
|
|
journalState,
|
|
filepath.Join(root, "archives"),
|
|
"proj-replay",
|
|
"ws-replay",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
work := sinkTestWork("proj-replay", "ws-replay", "work-replay", 1, agenttask.WorkStateDispatching)
|
|
project := agenttask.ProjectRecord{
|
|
ProjectID: "proj-replay",
|
|
WorkspaceID: "ws-replay",
|
|
Status: agenttask.ProjectStatusRunning,
|
|
Intent: &agenttask.StartIntent{
|
|
CommandID: "cmd-replay",
|
|
ProjectID: "proj-replay",
|
|
WorkspaceID: "ws-replay",
|
|
MilestoneID: "milestone-replay",
|
|
WorkflowRevision: "workflow-replay",
|
|
ConfigRevision: "config-replay",
|
|
GrantRevision: "grant-replay",
|
|
},
|
|
Works: map[agenttask.WorkUnitID]agenttask.WorkRecord{
|
|
work.Unit.ID: work,
|
|
},
|
|
}
|
|
resolver := &replayFailingEvidenceResolver{
|
|
evidence: EventEvidence{
|
|
StateRevision: "state-revision-1",
|
|
Project: project,
|
|
Work: &work,
|
|
},
|
|
}
|
|
sink, err := NewSink(store, resolver)
|
|
if err != nil {
|
|
t.Fatalf("NewSink: %v", err)
|
|
}
|
|
event := agenttask.Event{
|
|
EventID: "stable-replay-event",
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: project.ProjectID,
|
|
WorkspaceID: project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
CommandID: project.Intent.CommandID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
State: work.State,
|
|
ProviderID: work.Target.ProviderID,
|
|
ProfileID: work.Target.ProfileID,
|
|
Detail: "stable logical event",
|
|
Timestamp: time.Date(2026, 7, 30, 13, 0, 0, 0, time.UTC),
|
|
}
|
|
if err := sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("first Emit: %v", err)
|
|
}
|
|
event.Timestamp = event.Timestamp.Add(6 * time.Hour)
|
|
resolver.evidence.StateRevision = "state-revision-999"
|
|
if err := sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("stable replay Emit: %v", err)
|
|
}
|
|
if resolver.calls != 1 {
|
|
t.Fatalf("evidence resolver calls = %d, want 1", resolver.calls)
|
|
}
|
|
scoped, _ := store.ForWorkUnit(work.Unit.ID)
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil || len(records) != 1 {
|
|
t.Fatalf("ReplayRecords = %d, %v", len(records), err)
|
|
}
|
|
}
|
|
|
|
func TestSinkRejectsLogicalEventIDReuseAcrossScopesBeforeEvidenceResolution(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
journalState, err := agentstate.NewStore(filepath.Join(root, "projectlog-state.json"))
|
|
if err != nil {
|
|
t.Fatalf("agentstate.NewStore: %v", err)
|
|
}
|
|
store, err := NewStore(
|
|
journalState,
|
|
filepath.Join(root, "archives"),
|
|
"proj-cross-scope",
|
|
"ws-cross-scope",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
work := sinkTestWork(
|
|
"proj-cross-scope",
|
|
"ws-cross-scope",
|
|
"work-A",
|
|
1,
|
|
agenttask.WorkStateDispatching,
|
|
)
|
|
project := agenttask.ProjectRecord{
|
|
ProjectID: "proj-cross-scope",
|
|
WorkspaceID: "ws-cross-scope",
|
|
Status: agenttask.ProjectStatusRunning,
|
|
Intent: &agenttask.StartIntent{
|
|
CommandID: "cmd-cross-scope",
|
|
ProjectID: "proj-cross-scope",
|
|
WorkspaceID: "ws-cross-scope",
|
|
MilestoneID: "milestone-cross-scope",
|
|
WorkflowRevision: "workflow-cross-scope",
|
|
ConfigRevision: "config-cross-scope",
|
|
GrantRevision: "grant-cross-scope",
|
|
},
|
|
Works: map[agenttask.WorkUnitID]agenttask.WorkRecord{
|
|
work.Unit.ID: work,
|
|
},
|
|
}
|
|
resolver := &replayFailingEvidenceResolver{
|
|
evidence: EventEvidence{
|
|
StateRevision: "state-revision-cross-scope",
|
|
Project: project,
|
|
Work: &work,
|
|
},
|
|
}
|
|
sink, err := NewSink(store, resolver)
|
|
if err != nil {
|
|
t.Fatalf("NewSink: %v", err)
|
|
}
|
|
event := agenttask.Event{
|
|
EventID: "event-cross-scope-conflict",
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: project.ProjectID,
|
|
WorkspaceID: project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
CommandID: project.Intent.CommandID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
State: work.State,
|
|
ProviderID: work.Target.ProviderID,
|
|
ProfileID: work.Target.ProfileID,
|
|
Detail: "original work A event",
|
|
Timestamp: time.Date(2026, 7, 30, 13, 30, 0, 0, time.UTC),
|
|
}
|
|
if err := sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("first Emit: %v", err)
|
|
}
|
|
conflicting := event
|
|
conflicting.WorkUnitID = "work-B"
|
|
conflicting.AttemptID = "attempt-B"
|
|
conflicting.Detail = "changed work B event"
|
|
if err := sink.Emit(ctx, conflicting); !errors.Is(err, ErrRecordReplayConflict) {
|
|
t.Fatalf("cross-scope Emit error = %v", err)
|
|
}
|
|
if resolver.calls != 1 {
|
|
t.Fatalf("evidence resolver calls = %d, want 1", resolver.calls)
|
|
}
|
|
workA, _ := store.ForWorkUnit("work-A")
|
|
workB, _ := store.ForWorkUnit("work-B")
|
|
recordsA, err := workA.ReplayRecords(ctx)
|
|
if err != nil || len(recordsA) != 1 {
|
|
t.Fatalf("work A records = %d, %v", len(recordsA), err)
|
|
}
|
|
recordsB, err := workB.ReplayRecords(ctx)
|
|
if err != nil || len(recordsB) != 0 {
|
|
t.Fatalf("work B records = %d, %v", len(recordsB), err)
|
|
}
|
|
}
|
|
|
|
func TestSinkUsesOpaqueStableRecordID(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-opaque", "ws-opaque")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-opaque",
|
|
1,
|
|
agenttask.WorkStateDispatching,
|
|
)
|
|
harness.putWork(t, work)
|
|
event := agenttask.Event{
|
|
EventID: "raw-manager-event-with-secret-detail",
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
Detail: "Authorization: Bearer secret-value",
|
|
Timestamp: time.Date(2026, 7, 30, 13, 0, 0, 0, time.UTC),
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("first Emit: %v", err)
|
|
}
|
|
if err := harness.sink.Emit(ctx, event); err != nil {
|
|
t.Fatalf("replayed Emit: %v", err)
|
|
}
|
|
scoped, _ := harness.store.ForWorkUnit(work.Unit.ID)
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil || len(records) != 1 {
|
|
t.Fatalf("ReplayRecords = %d, %v", len(records), err)
|
|
}
|
|
record := records[0]
|
|
if !strings.HasPrefix(record.RecordID, "evt-sha256-") || len(record.RecordID) != len("evt-sha256-")+64 {
|
|
t.Fatalf("RecordID is not a bounded SHA-256 identity: %q", record.RecordID)
|
|
}
|
|
if strings.Contains(record.RecordID, event.EventID) ||
|
|
strings.Contains(record.RecordID, "secret") ||
|
|
record.Message != "[REDACTED]" {
|
|
t.Fatalf("raw event evidence leaked: %+v", record)
|
|
}
|
|
}
|
|
|
|
func TestSinkRejectsMissingEventID(t *testing.T) {
|
|
harness := newSinkTestHarness(t, "proj-missing", "ws-missing")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-missing",
|
|
1,
|
|
agenttask.WorkStateDispatching,
|
|
)
|
|
harness.putWork(t, work)
|
|
err := harness.sink.Emit(context.Background(), agenttask.Event{
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
Timestamp: time.Date(2026, 7, 30, 14, 0, 0, 0, time.UTC),
|
|
})
|
|
if !errors.Is(err, ErrMissingEventID) {
|
|
t.Fatalf("Emit error = %v, want ErrMissingEventID", err)
|
|
}
|
|
}
|
|
|
|
func TestTimelineProjectionIsStableAndRedacted(t *testing.T) {
|
|
ctx := context.Background()
|
|
harness := newSinkTestHarness(t, "proj-timeline", "ws-timeline")
|
|
work := sinkTestWork(
|
|
harness.project.ProjectID,
|
|
harness.project.WorkspaceID,
|
|
"work-timeline",
|
|
1,
|
|
agenttask.WorkStateDispatching,
|
|
)
|
|
harness.putWork(t, work)
|
|
now := time.Date(2026, 7, 30, 15, 0, 0, 0, time.UTC)
|
|
sensitive := agenttask.Event{
|
|
EventID: "timeline-sensitive",
|
|
Type: agenttask.EventDispatchStarted,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
Detail: "Dispatching with token sk-secret12345678",
|
|
Timestamp: now,
|
|
}
|
|
if err := harness.sink.Emit(ctx, sensitive); err != nil {
|
|
t.Fatalf("Emit sensitive event: %v", err)
|
|
}
|
|
|
|
work.State = agenttask.WorkStateCompleted
|
|
work.Integration = &agenttask.IntegrationResult{
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkUnitID: work.Unit.ID,
|
|
ChangeSet: agenttask.ChangeSetIdentity{
|
|
ID: "change-timeline",
|
|
Revision: "change-timeline-rev",
|
|
},
|
|
Ordinal: work.DispatchOrdinal,
|
|
Attempt: 1,
|
|
Outcome: agenttask.IntegrationOutcomeIntegrated,
|
|
}
|
|
work.ChangeSet = &work.Integration.ChangeSet
|
|
work.IntegrationAttempt = 1
|
|
harness.putWork(t, work)
|
|
completed := agenttask.Event{
|
|
EventID: "timeline-completed",
|
|
Type: agenttask.EventCompleted,
|
|
ProjectID: harness.project.ProjectID,
|
|
WorkspaceID: harness.project.WorkspaceID,
|
|
WorkUnitID: work.Unit.ID,
|
|
AttemptID: work.AttemptID,
|
|
Ordinal: work.DispatchOrdinal,
|
|
State: work.State,
|
|
Detail: "Task completed successfully",
|
|
Timestamp: now.Add(time.Minute),
|
|
}
|
|
if err := harness.sink.Emit(ctx, completed); err != nil {
|
|
t.Fatalf("Emit completed event: %v", err)
|
|
}
|
|
|
|
scoped, _ := harness.store.ForWorkUnit(work.Unit.ID)
|
|
records, err := scoped.ReplayRecords(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ReplayRecords: %v", err)
|
|
}
|
|
projection := ProjectWorkLog(records)
|
|
if len(projection.Entries) != 2 {
|
|
t.Fatalf("projection entry count = %d, want 2", len(projection.Entries))
|
|
}
|
|
if projection.Entries[0].Message != "[REDACTED]" ||
|
|
projection.Entries[0].RoleStage != "dispatch" ||
|
|
len(projection.Entries[0].Locators) != 2 {
|
|
t.Fatalf("first projection entry = %+v", projection.Entries[0])
|
|
}
|
|
if projection.Entries[1].Message != "Task completed successfully" ||
|
|
!projection.Entries[1].Terminal ||
|
|
projection.Entries[1].Result != string(agenttask.IntegrationOutcomeIntegrated) {
|
|
t.Fatalf("terminal projection entry = %+v", projection.Entries[1])
|
|
}
|
|
}
|