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

444 lines
13 KiB
Go

package taskloop
import (
"context"
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"iop/packages/go/agentconfig"
"iop/packages/go/agentguard"
"iop/packages/go/agentpolicy"
clistatus "iop/packages/go/agentprovider/cli/status"
"iop/packages/go/agentruntime"
"iop/packages/go/agenttask"
)
func TestProviderEnvironmentConfinesCodexRuntimeState(t *testing.T) {
sourceHome := canonicalTempDir(t)
for _, name := range []string{"auth.json", "config.toml"} {
if err := os.WriteFile(
filepath.Join(sourceHome, name),
[]byte(name+"\n"),
0o600,
); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
t.Setenv("CODEX_HOME", sourceHome)
taskRoot := canonicalTempDir(t)
binding := agenttask.ConfinementBinding{
TaskRoot: taskRoot,
WorkingDir: filepath.Join(taskRoot, "view"),
WritableRoots: []string{
filepath.Join(taskRoot, "view"),
filepath.Join(taskRoot, "tmp"),
filepath.Join(taskRoot, "cache"),
},
}
for _, root := range binding.WritableRoots {
if err := os.MkdirAll(root, 0o700); err != nil {
t.Fatalf("create writable root: %v", err)
}
}
resolved := agentconfig.ResolvedProfile{
Provider: agentconfig.Provider{ID: "codex"},
Profile: agentconfig.Profile{Env: []string{"FIXTURE=present"}},
}
environment, err := providerEnvironment(
resolved,
binding,
binding.WorkingDir,
)
if err != nil {
t.Fatalf("providerEnvironment: %v", err)
}
values := make(map[string]string)
for _, entry := range environment {
key, value, found := strings.Cut(entry, "=")
if found {
values[key] = value
}
}
codexHome := filepath.Join(binding.WritableRoots[2], "codex-home")
if values["CODEX_HOME"] != codexHome ||
values["TMPDIR"] != binding.WritableRoots[1] ||
values["TMP"] != binding.WritableRoots[1] ||
values["TEMP"] != binding.WritableRoots[1] ||
values["PWD"] != binding.WorkingDir ||
values["IOP_AGENT_TASK_ROOT"] != binding.TaskRoot ||
values["FIXTURE"] != "present" {
t.Fatalf("provider environment is not confined: %#v", values)
}
for _, name := range []string{"auth.json", "config.toml"} {
target, err := os.Readlink(filepath.Join(codexHome, name))
if err != nil {
t.Fatalf("read runtime link %s: %v", name, err)
}
if target != filepath.Join(sourceHome, name) {
t.Fatalf("runtime link %s = %q", name, target)
}
}
if _, err := providerEnvironment(resolved, binding, binding.WorkingDir); err != nil {
t.Fatalf("idempotent providerEnvironment: %v", err)
}
}
func TestProviderPreparesConfinedCommandAndOwnsExactStartedChild(t *testing.T) {
provider, err := NewProvider(testCatalog())
if err != nil {
t.Fatalf("NewProvider: %v", err)
}
work := agenttask.WorkRecord{
Unit: agenttask.WorkUnit{
ID: "1",
Metadata: map[string]string{
"plan_path": "agent-task/m-m1/1_fixture/PLAN-test.md",
"review_path": "agent-task/m-m1/1_fixture/CODE_REVIEW-test.md",
},
},
AttemptID: "attempt-1",
}
project := agenttask.ProjectRecord{
ProjectID: "project-a", WorkspaceID: "workspace-a",
}
target := agenttask.ExecutionTarget{
ProviderID: "test-provider", ModelID: "test-model", ProfileID: "p1",
}
resolved, ok := testCatalog().ResolveProfile(target.ProfileID)
if !ok {
t.Fatal("test profile is missing")
}
target.ProfileRevision = guardProfile(resolved).Revision
binding := agenttask.ConfinementBinding{
Revision: "confinement-r1",
ProfileRevision: target.ProfileRevision,
TaskRoot: "/tmp/task",
}
launch, err := provider.Prepare(context.Background(), agenttask.DispatchRequest{
Project: project, Work: work, Target: target,
Permit: &agentguard.Permit{},
Confinement: fakeConfinement{binding: binding},
Workspace: agentguard.CanonicalWorkspace{
WorkingDir: "/tmp/task/view", WritableRoots: []string{"/tmp/task/view"},
},
})
if err != nil {
t.Fatalf("Prepare: %v", err)
}
command := launch.Command()
if command.Name != "provider-must-not-run-in-tests" ||
!strings.Contains(command.Args[len(command.Args)-1], "PLAN-test.md") ||
!strings.Contains(command.Args[len(command.Args)-1], "CODE_REVIEW-test.md") {
t.Fatalf("prepared command = %#v", command)
}
if _, err := launch.BindStarted(nil); err == nil {
t.Fatal("BindStarted accepted a missing confinement child")
}
child := exec.Command("/bin/sh", "-c", "cat >/dev/null")
stdin, err := child.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdout, err := child.StdoutPipe()
if err != nil {
t.Fatal(err)
}
stderr, err := child.StderrPipe()
if err != nil {
t.Fatal(err)
}
if err := child.Start(); err != nil {
t.Fatalf("start deterministic fixture child: %v", err)
}
started := &fakeStartedConfinement{
child: child, stdin: stdin, stdout: stdout, stderr: stderr,
}
invocation, err := launch.BindStarted(started)
if err != nil {
_ = child.Process.Kill()
t.Fatalf("BindStarted: %v", err)
}
locators := invocation.Locators()
if len(locators) != 1 ||
locators[0].Kind != agenttask.LocatorProcess ||
locators[0].ProjectID != project.ProjectID ||
locators[0].WorkUnitID != work.Unit.ID {
t.Fatalf("locators = %#v", locators)
}
submission, err := invocation.Wait(context.Background())
if err != nil {
t.Fatalf("Wait: %v", err)
}
if !submission.Ready || submission.ArtifactID != artifactIdentity(work) {
t.Fatalf("submission = %#v", submission)
}
}
func TestProviderPersistsPiNativeSessionBeforeWait(t *testing.T) {
providerCatalog := piTestCatalog()
provider, err := NewProvider(providerCatalog)
if err != nil {
t.Fatalf("NewProvider: %v", err)
}
resolved, ok := providerCatalog.ResolveProfile("pi-headless")
if !ok {
t.Fatal("Pi test profile is missing")
}
target := agenttask.ExecutionTarget{
ProviderID: resolved.Provider.ID,
ModelID: resolved.Model.ID,
ProfileID: resolved.Profile.ID,
ProfileRevision: guardProfile(resolved).Revision,
ConfigRevision: "config-r1",
Capacity: 1,
}
root := canonicalTempDir(t)
binding := agenttask.ConfinementBinding{
Revision: "confinement-r1",
IsolationID: "overlay:pi",
IsolationRevision: "overlay-r1",
PinnedBaseRevision: "snapshot-r1",
ConfigRevision: "config-r1",
GrantRevision: "grant-r1",
ProfileRevision: target.ProfileRevision,
BaseRoot: filepath.Join(root, "base"),
RuntimeRoot: root,
SnapshotRoot: filepath.Join(root, "snapshot"),
TaskRoot: filepath.Join(root, "task"),
WorkingDir: filepath.Join(root, "task", "view"),
WritableRoots: []string{
filepath.Join(root, "task", "view"),
filepath.Join(root, "task", "tmp"),
filepath.Join(root, "task", "cache"),
},
}
project := agenttask.ProjectRecord{ProjectID: "p", WorkspaceID: "w"}
work := agenttask.WorkRecord{
Unit: agenttask.WorkUnit{
ID: "1",
Metadata: map[string]string{
"plan_path": "agent-task/m-m/1/PLAN-cloud-G10.md",
"review_path": "agent-task/m-m/1/CODE_REVIEW-cloud-G10.md",
},
},
AttemptID: "attempt-1",
}
launch, err := provider.Prepare(context.Background(), agenttask.DispatchRequest{
Project: project,
Work: work,
Target: target,
Permit: &agentguard.Permit{},
Confinement: fakeConfinement{
binding: binding,
},
Workspace: agentguard.CanonicalWorkspace{
WorkingDir: binding.WorkingDir,
WritableRoots: append(
[]string(nil),
binding.WritableRoots...,
),
},
})
if err != nil {
t.Fatalf("Prepare: %v", err)
}
command := launch.Command()
sessionIndex := -1
for index, argument := range command.Args {
if argument == "--session-id" {
sessionIndex = index
break
}
}
if sessionIndex < 0 || sessionIndex+3 >= len(command.Args) ||
command.Args[sessionIndex+2] != "--session-dir" {
t.Fatalf("Pi command has no durable session arguments: %#v", command.Args)
}
started, err := startFixtureCommand(context.Background(), "cat >/dev/null")
if err != nil {
t.Fatalf("start fixture: %v", err)
}
invocation, err := launch.BindStarted(started)
if err != nil {
_ = started.Abort()
t.Fatalf("BindStarted: %v", err)
}
locators := invocation.Locators()
if len(locators) != 2 || locators[1].Kind != agenttask.LocatorSession {
t.Fatalf("Pi locators = %#v", locators)
}
var reference nativeSessionReference
if err := json.Unmarshal([]byte(locators[1].Opaque), &reference); err != nil {
t.Fatalf("decode native session locator: %v", err)
}
if reference.SessionID != command.Args[sessionIndex+1] ||
reference.SessionDirectory != command.Args[sessionIndex+3] ||
locators[1].Revision != digestStrings(
"native-session-locator",
locators[1].Opaque,
) {
t.Fatalf("native session locator/command mismatch: %#v / %#v", reference, command.Args)
}
if _, err := invocation.Wait(context.Background()); err != nil {
t.Fatalf("Wait: %v", err)
}
}
func TestWorkerPromptPreservesActiveArtifactsForOfficialReview(t *testing.T) {
plan := "agent-task/m-m/1/PLAN-cloud-G10.md"
review := "agent-task/m-m/1/CODE_REVIEW-cloud-G10.md"
prompt := workerPrompt(plan, review)
for _, required := range []string{
plan,
review,
"direct implementing-worker assignment",
"do not invoke a task-loop dispatcher",
"Do not append a review verdict",
"archive, move, rename, or delete either active artifact",
} {
if !strings.Contains(prompt, required) {
t.Fatalf("worker prompt does not contain %q: %q", required, prompt)
}
}
}
func TestProviderFailureObservationFeedsCommonContinuationIdentity(t *testing.T) {
providerCatalog := testCatalog()
observer := matchingQuotaObserver{}
provider, err := NewProvider(providerCatalog, observer)
if err != nil {
t.Fatalf("NewProvider: %v", err)
}
resolved, ok := providerCatalog.ResolveProfile("p1")
if !ok {
t.Fatal("test profile is missing")
}
target := agenttask.ExecutionTarget{
ProviderID: resolved.Provider.ID,
ModelID: resolved.Model.ID,
ProfileID: resolved.Profile.ID,
ProfileRevision: guardProfile(resolved).Revision,
ConfigRevision: "config-r1",
Capacity: 1,
}
binding := agenttask.ConfinementBinding{
Revision: "confinement-r1",
ProfileRevision: target.ProfileRevision,
TaskRoot: "/tmp/task",
}
work := agenttask.WorkRecord{
Unit: agenttask.WorkUnit{
ID: "work",
Metadata: map[string]string{
"plan_path": "agent-task/m/PLAN-local-G01.md",
"review_path": "agent-task/m/CODE_REVIEW-local-G01.md",
},
},
AttemptID: "attempt-1",
}
launch, err := provider.Prepare(context.Background(), agenttask.DispatchRequest{
Project: agenttask.ProjectRecord{ProjectID: "p", WorkspaceID: "w"},
Work: work,
Target: target,
Permit: &agentguard.Permit{},
Confinement: fakeConfinement{
binding: binding,
},
Workspace: agentguard.CanonicalWorkspace{WorkingDir: "/tmp/task"},
})
if err != nil {
t.Fatalf("Prepare: %v", err)
}
started, err := startFixtureCommand(context.Background(), "exit 3")
if err != nil {
t.Fatalf("start fixture: %v", err)
}
invocation, err := launch.BindStarted(started)
if err != nil {
_ = started.Abort()
t.Fatalf("BindStarted: %v", err)
}
if _, err := invocation.Wait(context.Background()); err == nil {
t.Fatal("failed provider fixture returned success")
}
observed, ok := invocation.(agentruntimeFailureObservedInvocation)
if !ok {
t.Fatal("provider invocation does not expose a normalized failure observation")
}
observation := observed.FailureObservation()
if observation.Failure.Code != agentruntime.FailureCodeProcessExit ||
!observation.Failure.Retryable ||
observation.Quota.Adapter != target.ProviderID ||
observation.Quota.Target != target.ProfileID ||
observation.Quota.Validity != agentpolicy.ObservationValid {
t.Fatalf("failure observation = %#v", observation)
}
}
type agentruntimeFailureObservedInvocation interface {
FailureObservation() agentpolicy.AttemptObservation
}
type matchingQuotaObserver struct{}
func (matchingQuotaObserver) 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
}
type fakeConfinement struct {
binding agenttask.ConfinementBinding
}
func (confinement fakeConfinement) Revision() string {
return confinement.binding.Revision
}
func (confinement fakeConfinement) Binding() agenttask.ConfinementBinding {
return confinement.binding
}
func (confinement fakeConfinement) Validate(agenttask.ConfinementBinding) error {
return nil
}
func (confinement fakeConfinement) Start(
context.Context,
agenttask.ConfinementCommand,
) (agenttask.StartedConfinement, error) {
panic("fixture confinement must not launch the declared provider command")
}
type fakeStartedConfinement struct {
child *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
}
func (started *fakeStartedConfinement) Child() *exec.Cmd { return started.child }
func (started *fakeStartedConfinement) Stdin() io.WriteCloser { return started.stdin }
func (started *fakeStartedConfinement) Stdout() io.ReadCloser { return started.stdout }
func (started *fakeStartedConfinement) Stderr() io.ReadCloser { return started.stderr }
func (started *fakeStartedConfinement) Abort() error {
if started.child == nil || started.child.Process == nil {
return nil
}
_ = started.stdin.Close()
return started.child.Process.Kill()
}