package taskloop import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "iop/packages/go/agentconfig" "iop/packages/go/agentguard" "iop/packages/go/agentstate" "iop/packages/go/agenttask" "iop/packages/go/agentworkspace" ) func TestIntegrationDelegatesCleanConflictRetentionAndQueueContinuation(t *testing.T) { fixture := newLoopIntegrationFixture(t) writeIntegrationFile(t, filepath.Join(fixture.baseRoot, "shared.txt"), "base\n") writeIntegrationFile(t, filepath.Join(fixture.baseRoot, "other.txt"), "other base\n") fixture.commit("base") first := fixture.prepare("first", 1) conflicting := fixture.prepare("conflicting", 2) independent := fixture.prepare("independent", 3) writeIntegrationFile(t, filepath.Join(first.view, "shared.txt"), "first\n") writeIntegrationFile(t, filepath.Join(conflicting.view, "shared.txt"), "second\n") writeIntegrationFile(t, filepath.Join(independent.view, "other.txt"), "other integrated\n") firstSet := first.freeze() conflictingSet := conflicting.freeze() independentSet := independent.freeze() integration, err := NewIntegration(fixture.backend, fixture.store, DefaultValidator()) if err != nil { t.Fatalf("NewIntegration: %v", err) } firstResult := fixture.integrate(integration, first, firstSet) if firstResult.Outcome != agenttask.IntegrationOutcomeIntegrated { t.Fatalf("first result = %#v", firstResult) } conflictResult := fixture.integrate(integration, conflicting, conflictingSet) if conflictResult.Outcome != agenttask.IntegrationOutcomeTerminalDeferred || !conflictResult.Retained || conflictResult.Blocker == nil || !strings.Contains(conflictResult.Blocker.Message, "conflict") { t.Fatalf("conflict result = %#v", conflictResult) } independentResult := fixture.integrate(integration, independent, independentSet) if independentResult.Outcome != agenttask.IntegrationOutcomeIntegrated { t.Fatalf("independent result = %#v", independentResult) } assertIntegrationFile(t, filepath.Join(fixture.baseRoot, "shared.txt"), "first\n") assertIntegrationFile(t, filepath.Join(fixture.baseRoot, "other.txt"), "other integrated\n") if _, err := os.Stat(conflictingSet.Locator.Record); err != nil { t.Fatalf("retained conflict record: %v", err) } } func TestIntegrationRejectsUnavailableSharedOwner(t *testing.T) { var integration *Integration _, err := integration.Integrate(context.Background(), agenttask.IntegrationRequest{}) if err == nil || !strings.Contains(err.Error(), "shared integrator is unavailable") { t.Fatalf("Integrate error = %v", err) } } func TestIntegrationRequiresPostApplyValidator(t *testing.T) { fixture := newLoopIntegrationFixture(t) if _, err := NewIntegration(fixture.backend, fixture.store, nil); err == nil || !strings.Contains(err.Error(), "validator is required") { t.Fatalf("NewIntegration(nil validator) error = %v", err) } } func TestIntegrationValidationFailureRollsBackAndAllowsIndependentQueue(t *testing.T) { fixture := newLoopIntegrationFixture(t) writeIntegrationFile(t, filepath.Join(fixture.baseRoot, "a.txt"), "a base\n") writeIntegrationFile(t, filepath.Join(fixture.baseRoot, "b.txt"), "b base\n") fixture.commit("base") rejected := fixture.prepare("rejected", 1) independent := fixture.prepare("independent-after-rejection", 2) writeIntegrationFile(t, filepath.Join(rejected.view, "a.txt"), "invalid whitespace \n") writeIntegrationFile(t, filepath.Join(independent.view, "b.txt"), "b integrated\n") rejectedSet := rejected.freeze() independentSet := independent.freeze() integration, err := NewIntegration( fixture.backend, fixture.store, DefaultValidator(), ) if err != nil { t.Fatalf("NewIntegration: %v", err) } rejectedResult := fixture.integrate(integration, rejected, rejectedSet) if rejectedResult.Outcome != agenttask.IntegrationOutcomeTerminalDeferred || !rejectedResult.Retained || rejectedResult.Blocker == nil || !strings.Contains(rejectedResult.Blocker.Message, "validation") { t.Fatalf("rejected result = %#v", rejectedResult) } assertIntegrationFile(t, filepath.Join(fixture.baseRoot, "a.txt"), "a base\n") independentResult := fixture.integrate( integration, independent, independentSet, ) if independentResult.Outcome != agenttask.IntegrationOutcomeIntegrated { t.Fatalf("independent result = %#v", independentResult) } assertIntegrationFile( t, filepath.Join(fixture.baseRoot, "b.txt"), "b integrated\n", ) } type loopIntegrationFixture struct { t *testing.T baseRoot string backend *agentworkspace.Backend store *agentstate.Store } type loopPreparedTask struct { fixture *loopIntegrationFixture request agenttask.IsolationRequest prepared agenttask.PreparedIsolation view string artifact agenttask.ArtifactID ordinal agenttask.DispatchOrdinal } func newLoopIntegrationFixture(t *testing.T) *loopIntegrationFixture { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git is required for integration adapter tests") } root, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatal(err) } baseRoot := filepath.Join(root, "workspace") localRoot := filepath.Join(root, "runtime") for _, directory := range []string{baseRoot, localRoot} { if err := os.MkdirAll(directory, 0700); err != nil { t.Fatal(err) } } runIntegrationGit(t, baseRoot, "init", "-q") runIntegrationGit(t, baseRoot, "config", "user.email", "taskloop@example.invalid") runIntegrationGit(t, baseRoot, "config", "user.name", "Taskloop Fixture") resolver := agentworkspace.InputResolverFunc(func( _ context.Context, request agenttask.IsolationRequest, ) (agentworkspace.ResolvedInputs, error) { return agentworkspace.ResolvedInputs{ Grant: agentguard.WorkspaceGrant{ ProjectID: string(request.Project.ProjectID), WorkspaceID: string(request.Project.WorkspaceID), Root: baseRoot, Revision: string(request.Project.Intent.GrantRevision), }, Profile: agentguard.ProviderProfile{ ProviderID: request.Target.ProviderID, ModelID: request.Target.ModelID, ProfileID: request.Target.ProfileID, Revision: request.Target.ProfileRevision, Unattended: true, ApprovalBypass: true, WritableRootConfinement: true, }, }, nil }) backend, err := agentworkspace.NewBackend(agentworkspace.BackendConfig{ LocalRoot: localRoot, Retention: agentconfig.RetentionPolicy{CompletedDays: 7, BlockedDays: 14}, }, resolver) if err != nil { t.Fatalf("NewBackend: %v", err) } store, err := agentstate.NewStore(filepath.Join(localRoot, "state", "manager.json")) if err != nil { t.Fatalf("NewStore: %v", err) } return &loopIntegrationFixture{ t: t, baseRoot: baseRoot, backend: backend, store: store, } } func (fixture *loopIntegrationFixture) commit(message string) { fixture.t.Helper() runIntegrationGit(fixture.t, fixture.baseRoot, "add", "-A") runIntegrationGit(fixture.t, fixture.baseRoot, "commit", "-q", "-m", message) } func (fixture *loopIntegrationFixture) prepare( workID string, ordinal agenttask.DispatchOrdinal, ) loopPreparedTask { fixture.t.Helper() projectID := agenttask.ProjectID("project") workspaceID := agenttask.WorkspaceID("workspace") request := agenttask.IsolationRequest{ Project: agenttask.ProjectRecord{ ProjectID: projectID, WorkspaceID: workspaceID, Intent: &agenttask.StartIntent{ ProjectID: projectID, WorkspaceID: workspaceID, ConfigRevision: "config-r1", GrantRevision: "grant-r1", }, }, Work: agenttask.WorkRecord{ Unit: agenttask.WorkUnit{ ID: agenttask.WorkUnitID(workID), IsolationMode: agentguard.IsolationModeOverlay, }, AttemptID: agenttask.AttemptID(workID + "#1"), DispatchOrdinal: ordinal, }, Target: agenttask.ExecutionTarget{ ProviderID: "provider", ModelID: "model", ProfileID: "profile", ProfileRevision: "profile-r1", ConfigRevision: "config-r1", Capacity: 3, }, IdempotencyKey: "dispatch/" + workID + "/1/isolation", } prepared, err := fixture.backend.Prepare(context.Background(), request) if err != nil { fixture.t.Fatalf("Prepare(%s): %v", workID, err) } return loopPreparedTask{ fixture: fixture, request: request, prepared: prepared, view: prepared.Descriptor.WorkingDir, artifact: agenttask.ArtifactID("artifact-" + workID), ordinal: ordinal, } } func (task loopPreparedTask) freeze() agentworkspace.ChangeSet { task.fixture.t.Helper() changeSet, err := task.fixture.backend.Freeze(context.Background(), agentworkspace.FreezeRequest{ Descriptor: *task.prepared.Descriptor, ArtifactID: task.artifact, ValidationEvidence: []agentworkspace.ValidationEvidence{{ Name: "official-review", Result: "pass", Digest: "sha256:fixture", }}, }) if err != nil { task.fixture.t.Fatalf("Freeze(%s): %v", task.request.Work.Unit.ID, err) } return changeSet } func (fixture *loopIntegrationFixture) integrate( integration *Integration, task loopPreparedTask, changeSet agentworkspace.ChangeSet, ) agenttask.IntegrationResult { fixture.t.Helper() work := task.request.Work identity := changeSet.Identity() work.ChangeSet = &identity work.Isolation = &agenttask.IsolationIdentity{ ID: task.prepared.Descriptor.ID, Revision: task.prepared.Descriptor.Revision, Mode: task.prepared.Descriptor.Mode, PinnedBaseRevision: task.prepared.Descriptor.PinnedBaseRevision, TaskRoot: task.prepared.Descriptor.TaskRoot, } result, err := integration.Integrate(context.Background(), agenttask.IntegrationRequest{ Project: task.request.Project, Work: work, ChangeSet: identity, Ordinal: task.ordinal, Attempt: 1, IdempotencyKey: fmt.Sprintf( "integrate/%s/%s/1", task.request.Work.Unit.ID, changeSet.Revision, ), }) if err != nil { fixture.t.Fatalf("Integrate(%s): %v", task.request.Work.Unit.ID, err) } return result } func runIntegrationGit(t *testing.T, root string, arguments ...string) { t.Helper() command := exec.Command("git", arguments...) command.Dir = root if output, err := command.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", arguments, err, output) } } func writeIntegrationFile(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) } } func assertIntegrationFile(t *testing.T, path, expected string) { t.Helper() content, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } if string(content) != expected { t.Fatalf("%s = %q, want %q", path, content, expected) } }