iop/apps/agent/internal/taskloop/module_test.go

1263 lines
36 KiB
Go

package taskloop
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"iop/apps/agent/internal/projectlog"
"iop/packages/go/agentconfig"
"iop/packages/go/agentpolicy"
clistatus "iop/packages/go/agentprovider/cli/status"
"iop/packages/go/agentruntime"
"iop/packages/go/agentstate"
"iop/packages/go/agenttask"
"iop/packages/go/agentworkspace"
)
func TestRuntimePersistsManualLifecycleAndCompletesIndependentProject(t *testing.T) {
fixture := newRuntimeFixture(t)
ctx := context.Background()
preview, err := fixture.runtime.Preview(ctx, "project-a")
if err != nil {
t.Fatalf("Preview(project-a): %v", err)
}
if preview.NextWork != "1" || len(preview.Blockers) != 0 {
t.Fatalf("initial preview = %#v, want work 1", preview)
}
writeTaskFile(t, filepath.Join(fixture.projectA, "agent-task", "m-m1", "1_first", completeFileName), "complete\n")
preview, err = fixture.runtime.Preview(ctx, "project-a")
if err != nil {
t.Fatalf("Preview(project-a) after predecessor completion: %v", err)
}
if preview.NextWork != "2" || len(preview.Blockers) != 0 {
t.Fatalf("completed-predecessor preview = %#v, want work 2", preview)
}
started, err := fixture.runtime.StartProject(ctx, "project-a")
if err != nil || started.Status != agenttask.ProjectStatusStarted {
t.Fatalf("StartProject(project-a) = %#v, err = %v", started, err)
}
stopped, err := fixture.runtime.StopProject(ctx, "project-a")
if err != nil || stopped.Status != agenttask.ProjectStatusStopped {
t.Fatalf("StopProject(project-a) = %#v, err = %v", stopped, err)
}
resumed, err := fixture.runtime.ResumeProject(ctx, "project-a")
if err != nil || resumed.Status != agenttask.ProjectStatusStarted {
t.Fatalf("ResumeProject(project-a) = %#v, err = %v", resumed, err)
}
restarted, err := New(Config{
Snapshot: fixture.snapshot,
Catalog: fixture.catalog,
OwnerID: "taskloop-test-restart",
Validator: DefaultValidator(),
})
if err != nil {
t.Fatalf("New(restart): %v", err)
}
restartedView, err := restarted.ProjectStatus(ctx, "project-a")
if err != nil || restartedView.Status != agenttask.ProjectStatusStarted {
t.Fatalf("restart status = %#v, err = %v", restartedView, err)
}
if _, err := restarted.StopProject(ctx, "project-a"); err != nil {
t.Fatalf("stop restarted project-a: %v", err)
}
projectB, err := restarted.StartProject(ctx, "project-b")
if err != nil || projectB.Status != agenttask.ProjectStatusStarted {
t.Fatalf("StartProject(project-b) = %#v, err = %v", projectB, err)
}
if err := restarted.Reconcile(ctx); err != nil {
t.Fatalf("Reconcile terminal project-b: %v", err)
}
projectB, err = restarted.ProjectStatus(ctx, "project-b")
if err != nil || projectB.Status != agenttask.ProjectStatusCompleted {
t.Fatalf("terminal project-b status = %#v, err = %v", projectB, err)
}
projectA, err := restarted.ProjectStatus(ctx, "project-a")
if err != nil || projectA.Status != agenttask.ProjectStatusStopped {
t.Fatalf("independent project-a status = %#v, err = %v", projectA, err)
}
}
func TestRuntimeFakeProviderPersistedLifecycleRollbackAndRestart(t *testing.T) {
fixture := newFakeProviderLifecycleFixture(t)
ctx := context.Background()
started, err := fixture.runtime.StartProject(ctx, "project")
if err != nil || started.Status != agenttask.ProjectStatusStarted {
t.Fatalf("StartProject = %#v, err = %v", started, err)
}
if err := fixture.runtime.Reconcile(ctx); err != nil {
t.Fatalf("Reconcile: %v", err)
}
assertFakeProviderLifecycle(t, fixture)
restarted, err := New(Config{
Snapshot: fixture.snapshot,
Catalog: fixture.catalog,
OwnerID: "taskloop-fake-provider-restart",
Provider: fixture.provider,
ReviewExecutor: fixture.reviewer,
Validator: fixture.validator.Validate,
})
if err != nil {
t.Fatalf("New(restart): %v", err)
}
if err := restarted.Reconcile(ctx); err != nil {
t.Fatalf("Reconcile(restart): %v", err)
}
if got := fixture.provider.DispatchCount(); got != 2 {
t.Fatalf("dispatches after restart = %d, want 2", got)
}
if got := fixture.reviewer.Count(); got != 2 {
t.Fatalf("reviews after restart = %d, want 2", got)
}
}
func TestRuntimeRejectsUnknownAndUnselectedProjects(t *testing.T) {
fixture := newRuntimeFixture(t)
ctx := context.Background()
if _, err := fixture.runtime.StartProject(ctx, "unknown"); err == nil {
t.Fatal("unknown project start succeeded")
}
if err := fixture.runtime.selections.Save(ctx, "project-a", ""); err != nil {
t.Fatalf("save corrupt selection precondition: %v", err)
}
if _, err := fixture.runtime.ProjectStatus(ctx, "project-a"); err == nil {
t.Fatal("corrupt selection record was accepted")
}
}
func TestConfigSelectorPersistsOrderedPolicyAndSuppliesQuotaBackedFailover(t *testing.T) {
root := canonicalTempDir(t)
workspace := filepath.Join(root, "workspace")
if err := os.MkdirAll(workspace, 0o700); err != nil {
t.Fatal(err)
}
global := `version: "1"
defaults:
default_profile: p1
selection:
timezone: UTC
default:
provider: provider-a
model: model-a
profile: p1
rules:
- id: quota-failover
match:
failure_codes: [quota_exhausted]
target:
provider: provider-a
model: model-a
profile: p1
- id: high-grade
match:
stages: [worker]
min_grade: 7
max_grade: 10
target:
provider: provider-b
model: model-b
profile: p2
isolation:
default_mode: overlay
`
local := fmt.Sprintf(`version: "1"
device:
state_root: %s
overlay_root: %s
log_root: %s
projects:
project-a:
workspace: %s
enabled: true
`, filepath.Join(root, "state"), filepath.Join(root, "overlays"), filepath.Join(root, "logs"), workspace)
snapshot, err := agentconfig.LoadRuntimeConfigBytes([]byte(global), []byte(local))
if err != nil {
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
}
providerCatalog := orderedTestCatalog()
policySnapshot, err := selectionPolicySnapshot(snapshot, providerCatalog)
if err != nil {
t.Fatalf("selectionPolicySnapshot: %v", err)
}
state, err := agentstate.NewStore(filepath.Join(root, "state", "selector.json"))
if err != nil {
t.Fatalf("NewStore: %v", err)
}
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
selector := &configSelector{
snapshot: snapshot,
policySnapshot: policySnapshot,
catalog: providerCatalog,
state: state,
evaluator: agentpolicy.NewEvaluator(),
now: func() time.Time { return now },
quota: staticQuotaObserver{},
}
project := agenttask.ProjectRecord{
ProjectID: "project-a", WorkspaceID: WorkspaceIdentity(workspace),
}
work := agenttask.WorkRecord{
Unit: agenttask.WorkUnit{
ID: "work-1",
Metadata: map[string]string{
"plan_path": "agent-task/m-m1/1/PLAN-cloud-G10.md",
},
},
AttemptID: "attempt-1",
}
request := agenttask.SelectionRequest{Project: project, Work: work}
selected, err := selector.Select(context.Background(), request)
if err != nil {
t.Fatalf("Select: %v", err)
}
if selected.ProviderID != "provider-b" || selected.ProfileID != "p2" {
t.Fatalf("selected target = %#v", selected)
}
routePayload, _, found, err := state.LoadIntegrationRecord(
context.Background(),
routeDecisionKey(request),
)
if err != nil || !found || len(routePayload) == 0 {
t.Fatalf("persisted route found=%v size=%d err=%v", found, len(routePayload), err)
}
replayed, err := selector.Select(context.Background(), request)
if err != nil || !sameExecutionTarget(replayed, selected) {
t.Fatalf("replayed target = %#v, err = %v", replayed, err)
}
work.Target = &selected
continuation, err := selector.ContinuationPolicy(
context.Background(),
agentruntimeContinuationRequest(t, project, work, selected),
)
if err != nil {
t.Fatalf("ContinuationPolicy: %v", err)
}
if len(continuation.Policy.FailoverCodes) != 1 ||
continuation.Policy.FailoverCodes[0] != "quota_exhausted" {
t.Fatalf("failure policy = %#v", continuation.Policy)
}
var alternate *agenttask.FailureContinuationCandidate
for index := range continuation.Candidates {
if continuation.Candidates[index].Target.ProfileID == "p1" {
alternate = &continuation.Candidates[index]
}
}
if alternate == nil || !alternate.Eligible ||
alternate.Quota.State != agentpolicy.QuotaStateNotApplicable ||
alternate.Quota.Validity != agentpolicy.ObservationValid {
t.Fatalf("alternate candidate = %#v", alternate)
}
currentQuota, err := staticQuotaObserver{}.ObserveQuota(
context.Background(),
selected,
now,
)
if err != nil {
t.Fatal(err)
}
var commonCandidates []agentpolicy.ContinuationCandidate
for _, candidate := range continuation.Candidates {
commonCandidates = append(
commonCandidates,
agentpolicy.ContinuationCandidate{
Target: agentpolicy.TargetIdentity{
ProviderID: candidate.Target.ProviderID,
ModelID: candidate.Target.ModelID,
ProfileID: candidate.Target.ProfileID,
ProfileRevision: candidate.Target.ProfileRevision,
},
Eligible: candidate.Eligible,
Quota: candidate.Quota,
},
)
}
decision, err := agentpolicy.DecideContinuation(agentpolicy.ContinuationRequest{
Policy: continuation.Policy,
Current: agentpolicy.TargetIdentity{
ProviderID: selected.ProviderID,
ModelID: selected.ModelID,
ProfileID: selected.ProfileID,
ProfileRevision: selected.ProfileRevision,
},
Candidates: commonCandidates,
Budget: agentpolicy.FailureBudget{Used: 1, Limit: 10},
Observation: agentpolicy.AttemptObservation{
ObservedAt: now,
Quota: currentQuota,
Failure: agentpolicy.FailureObservation{
Code: agentruntime.FailureCodeQuotaExhausted,
},
},
})
if err != nil || decision.Action != agentpolicy.ContinuationFailover ||
decision.Target.ProfileID != "p1" {
t.Fatalf("common continuation decision = %#v, err = %v", decision, err)
}
}
func TestConfigSelectorContinuationUsesExactFailureRuleContext(t *testing.T) {
root := canonicalTempDir(t)
workspace := filepath.Join(root, "workspace")
if err := os.MkdirAll(workspace, 0o700); err != nil {
t.Fatal(err)
}
global := `version: "1"
defaults:
default_profile: p1
selection:
timezone: UTC
default:
provider: provider-a
model: model-a
profile: p1
rules:
- id: wrong-stage
match:
stages: [review]
failure_codes: [process_exit]
target:
provider: provider-b
model: model-b
profile: p2
- id: wrong-grade
match:
min_grade: 11
failure_codes: [process_exit]
target:
provider: provider-b
model: model-b
profile: p2
- id: wrong-lane
match:
lanes: [local]
failure_codes: [process_exit]
target:
provider: provider-b
model: model-b
profile: p2
- id: wrong-capability
match:
capabilities: [resume]
failure_codes: [process_exit]
target:
provider: provider-b
model: model-b
profile: p2
- id: wrong-quota
match:
quota_states: [available]
failure_codes: [process_exit]
target:
provider: provider-b
model: model-b
profile: p2
- id: quota-retry
match:
quota_states: [exhausted]
stages: [worker]
lanes: [cloud]
min_grade: 7
max_grade: 10
capabilities: [run]
failure_codes: [quota_exhausted]
target:
provider: provider-b
model: model-b
profile: p2
- id: process-failover
match:
quota_states: [exhausted]
stages: [worker]
lanes: [cloud]
min_grade: 7
max_grade: 10
capabilities: [run]
failure_codes: [process_exit]
target:
provider: provider-a
model: model-a
profile: p1
- id: ordinary-high-grade
match:
stages: [worker]
min_grade: 7
max_grade: 10
target:
provider: provider-b
model: model-b
profile: p2
isolation:
default_mode: overlay
`
local := fmt.Sprintf(`version: "1"
device:
state_root: %s
overlay_root: %s
log_root: %s
projects:
project-a:
workspace: %s
enabled: true
`, filepath.Join(root, "state"), filepath.Join(root, "overlays"), filepath.Join(root, "logs"), workspace)
snapshot, err := agentconfig.LoadRuntimeConfigBytes([]byte(global), []byte(local))
if err != nil {
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
}
providerCatalog := orderedTestCatalog()
policySnapshot, err := selectionPolicySnapshot(snapshot, providerCatalog)
if err != nil {
t.Fatalf("selectionPolicySnapshot: %v", err)
}
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
selector := &configSelector{
snapshot: snapshot,
policySnapshot: policySnapshot,
catalog: providerCatalog,
evaluator: agentpolicy.NewEvaluator(),
now: func() time.Time { return now },
quota: staticQuotaObserver{},
}
project := agenttask.ProjectRecord{
ProjectID: "project-a", WorkspaceID: WorkspaceIdentity(workspace),
}
baseWork := agenttask.WorkRecord{
Unit: agenttask.WorkUnit{
ID: "work-1",
Metadata: map[string]string{
"plan_path": "agent-task/m-m1/1/PLAN-cloud-G10.md",
"required_capabilities": "run",
},
},
AttemptID: "attempt-1",
}
current, err := selector.executionTargetRef(agentconfig.TargetRef{
Provider: "provider-b",
Model: "model-b",
Profile: "p2",
})
if err != nil {
t.Fatalf("current target: %v", err)
}
tests := []struct {
name string
work agenttask.WorkRecord
code agentruntime.FailureCode
wantTarget string
wantRetry bool
wantError bool
}{
{
name: "process exit skips all nonmatching predicates",
work: baseWork,
code: agentruntime.FailureCodeProcessExit,
wantTarget: "p1",
},
{
name: "quota failure uses only its matching current-target rule",
work: baseWork,
code: agentruntime.FailureCodeQuotaExhausted,
wantTarget: "p2",
wantRetry: true,
},
{
name: "default target cannot authorize continuation",
work: func() agenttask.WorkRecord {
work := baseWork
work.Unit.Metadata = map[string]string{
"plan_path": "agent-task/m-m1/1/PLAN-cloud-G01.md",
}
return work
}(),
code: agentruntime.FailureCodeInternal,
wantError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
observation := continuationObservation(
t,
current,
test.code,
agentpolicy.QuotaStateExhausted,
test.wantRetry,
now,
)
policy, err := selector.ContinuationPolicy(
context.Background(),
agenttask.FailureContinuationPolicyRequest{
Project: project,
Work: test.work,
CurrentTarget: current,
Observation: observation,
},
)
if test.wantError {
if err == nil {
t.Fatalf("ContinuationPolicy accepted non-failure/default rule: %#v", policy)
}
return
}
if err != nil {
t.Fatalf("ContinuationPolicy: %v", err)
}
if len(policy.Candidates) != 1 ||
policy.Candidates[0].Target.ProfileID != test.wantTarget {
t.Fatalf("continuation candidates = %#v", policy.Candidates)
}
if test.wantRetry {
if len(policy.Policy.RetryableCodes) != 1 ||
policy.Policy.RetryableCodes[0] != test.code ||
len(policy.Policy.FailoverCodes) != 0 ||
policy.Candidates[0].Eligible {
t.Fatalf("retry policy = %#v", policy)
}
} else if len(policy.Policy.FailoverCodes) != 1 ||
policy.Policy.FailoverCodes[0] != test.code ||
len(policy.Policy.RetryableCodes) != 0 ||
!policy.Candidates[0].Eligible {
t.Fatalf("failover policy = %#v", policy)
}
})
}
}
func TestRuntimeArchivesTerminalProjectLogEvidence(t *testing.T) {
fixture := newRuntimeFixture(t)
projectID := agenttask.ProjectID("project-a")
workspaceID := WorkspaceIdentity(fixture.projectA)
workID := agenttask.WorkUnitID("archive-work")
scoped, err := fixture.runtime.projectStores[string(projectID)].ForWorkUnit(workID)
if err != nil {
t.Fatalf("ForWorkUnit: %v", err)
}
if _, err := scoped.AppendRecord(context.Background(), projectlog.ProjectLogRecord{
SchemaVersion: projectlog.RecordSchemaVersion,
RecordID: "terminal-record",
LoopOrdinal: 1,
DispatchOrdinal: 1,
ProjectID: projectID,
WorkspaceID: workspaceID,
WorkUnitID: workID,
AttemptID: "attempt-1",
EventType: agenttask.EventCompleted,
State: agenttask.WorkStateCompleted,
StateRevision: "state-r1",
Timestamp: time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC),
Terminal: true,
}); err != nil {
t.Fatalf("AppendRecord: %v", err)
}
state := agenttask.ManagerState{
SchemaVersion: agenttask.StateSchemaVersion,
Projects: map[agenttask.ProjectID]agenttask.ProjectRecord{
projectID: {
ProjectID: projectID, WorkspaceID: workspaceID,
Works: map[agenttask.WorkUnitID]agenttask.WorkRecord{
workID: {
Unit: agenttask.WorkUnit{ID: workID},
State: agenttask.WorkStateCompleted,
},
},
},
},
}
if err := fixture.runtime.archiveTerminalProjectLogs(
context.Background(),
state,
); err != nil {
t.Fatalf("archiveTerminalProjectLogs: %v", err)
}
var manifests int
if err := filepath.Walk(
fixture.snapshot.Config().Device.LogRoot,
func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !info.IsDir() && strings.HasSuffix(path, ".manifest.json") {
manifests++
}
return nil
},
); err != nil {
t.Fatalf("walk archives: %v", err)
}
if manifests != 1 {
t.Fatalf("terminal archive manifests = %d, want 1", manifests)
}
}
type staticQuotaObserver struct{}
func (staticQuotaObserver) ObserveQuota(
_ context.Context,
target agenttask.ExecutionTarget,
observedAt time.Time,
) (agentpolicy.QuotaObservation, error) {
snapshot := clistatus.NormalizeQuotaSnapshot(
target.ProviderID,
target.ProfileID,
nil,
observedAt,
nil,
nil,
)
return agentpolicy.NormalizeQuotaObservation(snapshot, observedAt, time.Minute), nil
}
func continuationObservation(
t *testing.T,
target agenttask.ExecutionTarget,
code agentruntime.FailureCode,
state agentpolicy.QuotaState,
retryable bool,
observedAt time.Time,
) agentpolicy.AttemptObservation {
t.Helper()
var requiredCapabilities []string
var usage *clistatus.UsageStatus
switch state {
case agentpolicy.QuotaStateAvailable:
requiredCapabilities = []string{"overall"}
usage = &clistatus.UsageStatus{DailyLimit: "50%"}
case agentpolicy.QuotaStateExhausted:
requiredCapabilities = []string{"overall"}
usage = &clistatus.UsageStatus{DailyLimit: "0%"}
case agentpolicy.QuotaStateUnknown:
requiredCapabilities = []string{"overall"}
usage = &clistatus.UsageStatus{DailyLimit: "not-a-percent"}
case agentpolicy.QuotaStateNotApplicable:
default:
t.Fatalf("unsupported quota state %q", state)
}
quota := agentpolicy.NormalizeQuotaObservation(
clistatus.NormalizeQuotaSnapshot(
target.ProviderID,
target.ProfileID,
requiredCapabilities,
observedAt,
usage,
nil,
),
observedAt,
time.Minute,
)
if quota.State != state {
t.Fatalf("normalized quota state = %q, want %q", quota.State, state)
}
return agentpolicy.AttemptObservation{
ObservedAt: observedAt,
Quota: quota,
Failure: agentpolicy.FailureObservation{
Code: code,
Retryable: retryable,
},
}
}
func agentruntimeContinuationRequest(
t *testing.T,
project agenttask.ProjectRecord,
work agenttask.WorkRecord,
target agenttask.ExecutionTarget,
) agenttask.FailureContinuationPolicyRequest {
t.Helper()
return agenttask.FailureContinuationPolicyRequest{
Project: project,
Work: work,
CurrentTarget: target,
Observation: continuationObservation(
t,
target,
agentruntime.FailureCodeQuotaExhausted,
agentpolicy.QuotaStateExhausted,
false,
time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC),
),
}
}
func orderedTestCatalog() agentconfig.Catalog {
capabilities := []string{
"approval_bypass", "run", "unattended", "writable_root_confinement",
}
return agentconfig.Catalog{
Version: agentconfig.SchemaVersion,
Providers: []agentconfig.Provider{
{
ID: "provider-a", Command: "provider-a-must-not-run",
VersionProbe: agentconfig.CommandProbe{Args: []string{"--version"}},
Authentication: agentconfig.AuthenticationProbe{
Args: []string{"auth", "status"},
},
Capabilities: capabilities,
},
{
ID: "provider-b", Command: "provider-b-must-not-run",
VersionProbe: agentconfig.CommandProbe{Args: []string{"--version"}},
Authentication: agentconfig.AuthenticationProbe{
Args: []string{"auth", "status"},
},
Capabilities: capabilities,
},
},
Models: []agentconfig.Model{
{ID: "model-a", Provider: "provider-a", Target: "model-a"},
{ID: "model-b", Provider: "provider-b", Target: "model-b"},
},
Profiles: []agentconfig.Profile{
{
ID: "p1", Provider: "provider-a", Model: "model-a",
Args: []string{"--model", "{{model}}"}, Capabilities: capabilities,
},
{
ID: "p2", Provider: "provider-b", Model: "model-b",
Args: []string{"--model", "{{model}}"}, Capabilities: capabilities,
},
},
}
}
type runtimeFixture struct {
runtime *Runtime
snapshot agentconfig.RuntimeSnapshot
catalog agentconfig.Catalog
projectA string
projectB string
}
type fakeProviderLifecycleFixture struct {
runtime *Runtime
snapshot agentconfig.RuntimeSnapshot
catalog agentconfig.Catalog
provider *fakeLifecycleProvider
reviewer *fakeLifecycleReviewer
validator *fakeLifecycleValidator
workspace string
logRoot string
}
func newFakeProviderLifecycleFixture(t *testing.T) fakeProviderLifecycleFixture {
t.Helper()
root := canonicalTempDir(t)
workspace := filepath.Join(root, "workspace")
stateRoot := filepath.Join(root, "state")
logRoot := filepath.Join(root, "logs")
for _, directory := range []string{workspace, stateRoot, logRoot} {
if err := os.MkdirAll(directory, 0o700); err != nil {
t.Fatal(err)
}
}
writeTaskFile(t, filepath.Join(workspace, "rejected.txt"), "base rejected\n")
writeTaskFile(t, filepath.Join(workspace, "sibling.txt"), "base sibling\n")
createLifecycleTaskPair(t, workspace, "1_rejected", "rejected.txt")
createLifecycleTaskPair(t, workspace, "2_sibling", "sibling.txt")
runIntegrationGit(t, workspace, "init", "-q")
runIntegrationGit(t, workspace, "config", "user.email", "taskloop@example.invalid")
runIntegrationGit(t, workspace, "config", "user.name", "Taskloop Fixture")
runIntegrationGit(t, workspace, "add", "-A")
runIntegrationGit(t, workspace, "commit", "-q", "-m", "base")
global := `version: "1"
defaults:
default_profile: p1
selection:
timezone: UTC
default:
provider: test-provider
model: test-model
profile: p1
isolation:
default_mode: overlay
retention:
completed_days: 14
blocked_days: 14
`
local := fmt.Sprintf(`version: "1"
device:
state_root: %s
overlay_root: %s
log_root: %s
projects:
project:
workspace: %s
enabled: true
selected_milestone: m1
`, stateRoot, filepath.Join(root, "overlays"), logRoot, workspace)
snapshot, err := agentconfig.LoadRuntimeConfigBytes([]byte(global), []byte(local))
if err != nil {
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
}
provider := &fakeLifecycleProvider{}
reviewer := &fakeLifecycleReviewer{}
validator := &fakeLifecycleValidator{}
providerCatalog := testCatalog()
runtime, err := New(Config{
Snapshot: snapshot,
Catalog: providerCatalog,
OwnerID: "taskloop-fake-provider",
Provider: provider,
ReviewExecutor: reviewer,
Validator: validator.Validate,
})
if err != nil {
t.Fatalf("New: %v", err)
}
return fakeProviderLifecycleFixture{
runtime: runtime, snapshot: snapshot, catalog: providerCatalog,
provider: provider, reviewer: reviewer, validator: validator,
workspace: workspace, logRoot: logRoot,
}
}
func createLifecycleTaskPair(
t *testing.T,
workspace, taskDirectory, target string,
) {
t.Helper()
taskRoot := filepath.Join(workspace, "agent-task", "m-m1", taskDirectory)
if err := os.MkdirAll(taskRoot, 0o700); err != nil {
t.Fatal(err)
}
reviewRelative := filepath.ToSlash(
filepath.Join("agent-task", "m-m1", taskDirectory, "CODE_REVIEW-test.md"),
)
writeTaskFile(t, filepath.Join(taskRoot, "PLAN-test.md"), fmt.Sprintf(`# Plan
## Modified Files Summary
| File | Purpose |
|------|---------|
| %s | Exercise provider output. |
| %s | Record implementation and canonical review. |
`, "`"+target+"`", "`"+reviewRelative+"`"))
writeTaskFile(t, filepath.Join(taskRoot, "CODE_REVIEW-test.md"), `# Code Review Reference
## Implementation Notes
Deterministic fake-provider implementation evidence is complete.
`)
}
func assertFakeProviderLifecycle(
t *testing.T,
fixture fakeProviderLifecycleFixture,
) {
t.Helper()
if got := fixture.provider.DispatchCount(); got != 2 {
t.Fatalf("dispatches = %d, want 2", got)
}
if got := fixture.reviewer.Count(); got != 2 {
t.Fatalf("reviews = %d, want 2", got)
}
if got := fixture.validator.Count(); got != 2 {
t.Fatalf("validator calls = %d, want 2", got)
}
state, _, err := fixture.runtime.StateStore().Load(context.Background())
if err != nil {
t.Fatal(err)
}
project := state.Projects["project"]
rejected := project.Works["1"]
if rejected.State != agenttask.WorkStateTerminalDeferred ||
rejected.Integration == nil ||
rejected.Integration.Outcome != agenttask.IntegrationOutcomeTerminalDeferred ||
rejected.Blocker == nil ||
!strings.Contains(rejected.Blocker.Message, "validation") ||
rejected.ChangeSet == nil {
t.Fatalf("rejected work = %#v", rejected)
}
sibling := project.Works["2"]
if sibling.State != agenttask.WorkStateCompleted ||
sibling.Integration == nil ||
sibling.Integration.Outcome != agenttask.IntegrationOutcomeIntegrated ||
!sibling.CompletionVerified {
t.Fatalf("sibling work = %#v", sibling)
}
assertIntegrationFile(t, filepath.Join(fixture.workspace, "rejected.txt"), "base rejected\n")
assertIntegrationFile(t, filepath.Join(fixture.workspace, "sibling.txt"), "integrated 2\n")
var archiveManifests int
terminalTimelines := make(map[agenttask.WorkUnitID]bool)
if err := filepath.Walk(
fixture.logRoot,
func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !info.IsDir() && strings.HasSuffix(path, ".manifest.json") {
archiveManifests++
}
if !info.IsDir() && strings.HasSuffix(path, ".timeline.jsonl") {
content, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
var prior uint64
var last projectlog.ProjectLogRecord
for index, line := range lines {
var record projectlog.ProjectLogRecord
if err := json.Unmarshal([]byte(line), &record); err != nil {
return err
}
if index > 0 && record.Sequence != prior+1 {
return fmt.Errorf("project log sequence %d does not follow %d", record.Sequence, prior)
}
prior = record.Sequence
last = record
}
if !last.Terminal {
return errors.New("project log timeline is not terminal")
}
terminalTimelines[last.WorkUnitID] = true
}
return nil
},
); err != nil {
t.Fatal(err)
}
if archiveManifests != 2 {
t.Fatalf("terminal archive manifests = %d, want 2", archiveManifests)
}
if !terminalTimelines["1"] || !terminalTimelines["2"] {
t.Fatalf("terminal timeline work units = %#v", terminalTimelines)
}
}
type fakeLifecycleProvider struct {
mu sync.Mutex
requests []agenttask.DispatchRequest
}
func (provider *fakeLifecycleProvider) Prepare(
_ context.Context,
request agenttask.DispatchRequest,
) (agenttask.ProviderLaunch, error) {
if request.Confinement == nil || request.Permit == nil {
return nil, errors.New("fake provider requires admitted confinement")
}
binding := request.Confinement.Binding()
if err := request.Confinement.Validate(binding); err != nil {
return nil, err
}
provider.mu.Lock()
provider.requests = append(provider.requests, request)
ordinal := len(provider.requests)
provider.mu.Unlock()
return &fakeLifecycleLaunch{
request: request,
root: binding.WorkingDir,
ordinal: ordinal,
}, nil
}
func (provider *fakeLifecycleProvider) DispatchCount() int {
provider.mu.Lock()
defer provider.mu.Unlock()
return len(provider.requests)
}
type fakeLifecycleLaunch struct {
request agenttask.DispatchRequest
root string
ordinal int
}
func (launch *fakeLifecycleLaunch) Command() agenttask.ConfinementCommand {
return agenttask.ConfinementCommand{Name: "true"}
}
func (launch *fakeLifecycleLaunch) BindStarted(
started agenttask.StartedConfinement,
) (agenttask.ProviderInvocation, error) {
if started == nil || started.Child() == nil || started.Child().Process == nil {
return nil, errors.New("fake provider child is incomplete")
}
opaque := fmt.Sprintf("fake-process-%d", started.Child().Process.Pid)
return &fakeLifecycleInvocation{
request: launch.request,
root: launch.root,
started: started,
locators: []agenttask.LocatorRecord{{
Kind: agenttask.LocatorProcess,
Opaque: opaque,
Revision: digestStrings("fake-process", opaque),
ProjectID: launch.request.Project.ProjectID,
WorkspaceID: launch.request.Project.WorkspaceID,
WorkUnitID: launch.request.Work.Unit.ID,
AttemptID: launch.request.Work.AttemptID,
}},
}, nil
}
type fakeLifecycleInvocation struct {
request agenttask.DispatchRequest
root string
started agenttask.StartedConfinement
locators []agenttask.LocatorRecord
}
func (invocation *fakeLifecycleInvocation) Locators() []agenttask.LocatorRecord {
return append([]agenttask.LocatorRecord(nil), invocation.locators...)
}
func (invocation *fakeLifecycleInvocation) Wait(
context.Context,
) (agenttask.Submission, error) {
if err := waitFakeLifecycleChild(invocation.started); err != nil {
return agenttask.Submission{}, err
}
target := ""
for _, candidate := range invocation.request.Work.Unit.DeclaredWriteSet {
if !strings.HasPrefix(candidate, "agent-task/") {
target = candidate
break
}
}
if target == "" {
return agenttask.Submission{}, errors.New("fake provider target is missing")
}
content := "integrated " + string(invocation.request.Work.Unit.ID) + "\n"
if err := os.WriteFile(
filepath.Join(invocation.root, filepath.FromSlash(target)),
[]byte(content),
0o600,
); err != nil {
return agenttask.Submission{}, err
}
return agenttask.Submission{
ProjectID: invocation.request.Project.ProjectID,
WorkUnitID: invocation.request.Work.Unit.ID,
AttemptID: invocation.request.Work.AttemptID,
ArtifactID: artifactIdentity(invocation.request.Work),
Ready: true,
Locators: invocation.Locators(),
}, nil
}
func (invocation *fakeLifecycleInvocation) Cancel(context.Context) error {
return invocation.started.Abort()
}
func waitFakeLifecycleChild(started agenttask.StartedConfinement) error {
if stdin := started.Stdin(); stdin != nil {
_ = stdin.Close()
}
if stdout := started.Stdout(); stdout != nil {
_, _ = io.Copy(io.Discard, stdout)
_ = stdout.Close()
}
if stderr := started.Stderr(); stderr != nil {
_, _ = io.Copy(io.Discard, stderr)
_ = stderr.Close()
}
return started.Child().Wait()
}
type fakeLifecycleReviewer struct {
mu sync.Mutex
count int
}
func (reviewer *fakeLifecycleReviewer) ExecuteReview(
_ context.Context,
_ agenttask.ReviewRequest,
root, _, reviewRelative string,
) error {
reviewer.mu.Lock()
reviewer.count++
reviewer.mu.Unlock()
path := filepath.Join(root, filepath.FromSlash(reviewRelative))
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(
file,
"\n## Code Review Result\n\nOverall Verdict: PASS\n",
)
return err
}
func (reviewer *fakeLifecycleReviewer) Count() int {
reviewer.mu.Lock()
defer reviewer.mu.Unlock()
return reviewer.count
}
type fakeLifecycleValidator struct {
mu sync.Mutex
count int
}
func (validator *fakeLifecycleValidator) Validate(
_ context.Context,
request agentworkspace.ValidationRequest,
) error {
validator.mu.Lock()
validator.count++
validator.mu.Unlock()
content, err := os.ReadFile(filepath.Join(request.ValidationRoot, "rejected.txt"))
if err != nil {
return err
}
if string(content) != "base rejected\n" {
return errors.New("fake validator rejected project-1 candidate")
}
return nil
}
func (validator *fakeLifecycleValidator) Count() int {
validator.mu.Lock()
defer validator.mu.Unlock()
return validator.count
}
type staticArtifactRoot string
func (root staticArtifactRoot) ArtifactRoot(agenttask.WorkRecord) (string, error) {
return string(root), nil
}
func newRuntimeFixture(t *testing.T) runtimeFixture {
t.Helper()
root := canonicalTempDir(t)
stateRoot := filepath.Join(root, "state")
projectA := filepath.Join(root, "project-a")
projectB := filepath.Join(root, "project-b")
if err := os.MkdirAll(stateRoot, 0700); err != nil {
t.Fatalf("create state root: %v", err)
}
createTaskPair(t, projectA, "m1", "1_first", false)
createTaskPair(t, projectA, "m1", "2+1_second", false)
createTaskPair(t, projectB, "m1", "1_terminal", true)
global := `version: "1"
defaults:
default_profile: p1
auto_resume_interrupted: true
selection:
timezone: UTC
default:
provider: test-provider
model: test-model
profile: p1
isolation:
default_mode: overlay
retention:
completed_days: 14
`
local := fmt.Sprintf(`version: "1"
device:
state_root: %s
overlay_root: %s
log_root: %s
projects:
project-a:
workspace: %s
enabled: true
selected_milestone: m1
project-b:
workspace: %s
enabled: true
selected_milestone: m1
`, stateRoot, filepath.Join(root, "overlays"), filepath.Join(root, "logs"), projectA, projectB)
snapshot, err := agentconfig.LoadRuntimeConfigBytes([]byte(global), []byte(local))
if err != nil {
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
}
catalog := testCatalog()
runtime, err := New(Config{
Snapshot: snapshot,
Catalog: catalog,
OwnerID: "taskloop-test",
Validator: DefaultValidator(),
})
if err != nil {
t.Fatalf("New task runtime: %v", err)
}
return runtimeFixture{
runtime: runtime, snapshot: snapshot, catalog: catalog,
projectA: projectA, projectB: projectB,
}
}
func canonicalTempDir(t *testing.T) string {
t.Helper()
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatalf("resolve canonical temporary directory: %v", err)
}
return root
}
func testCatalog() agentconfig.Catalog {
capabilities := []string{
"approval_bypass", "run", "unattended", "writable_root_confinement",
}
return agentconfig.Catalog{
Version: agentconfig.SchemaVersion,
Providers: []agentconfig.Provider{{
ID: "test-provider", Command: "provider-must-not-run-in-tests",
VersionProbe: agentconfig.CommandProbe{Args: []string{"--version"}},
Authentication: agentconfig.AuthenticationProbe{Args: []string{"auth", "status"}},
Capabilities: capabilities,
}},
Models: []agentconfig.Model{{
ID: "test-model", Provider: "test-provider", Target: "test-model",
}},
Profiles: []agentconfig.Profile{{
ID: "p1", Provider: "test-provider", Model: "test-model",
Args: []string{"--model", "{{model}}"}, Capabilities: capabilities,
}},
}
}
func createTaskPair(t *testing.T, workspace, milestone, task string, complete bool) {
t.Helper()
taskRoot := filepath.Join(workspace, "agent-task", "m-"+milestone, task)
if err := os.MkdirAll(taskRoot, 0700); err != nil {
t.Fatalf("create task %s: %v", task, err)
}
writeTaskFile(t, filepath.Join(taskRoot, "PLAN-test.md"), `# Plan
## Modified Files Summary
| File | Purpose |
|------|---------|
| `+"`README.md`"+` | Exercise the task runtime. |
`)
writeTaskFile(t, filepath.Join(taskRoot, "CODE_REVIEW-test.md"), `# Code Review Reference
## Implementation Notes
Implementation evidence is complete.
`)
if complete {
writeTaskFile(t, filepath.Join(taskRoot, completeFileName), "complete\n")
}
}
func writeTaskFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}