1263 lines
36 KiB
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)
|
|
}
|
|
}
|