Files
tutti/services/tuttid/service/workspace/issue_sequential_dispatch_test.go
T
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

1883 lines
76 KiB
Go

package workspace
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
canonical "github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
agenttargetbiz "github.com/tutti-os/tutti/services/tuttid/biz/agenttarget"
executionbiz "github.com/tutti-os/tutti/services/tuttid/biz/tuttimodeexecution"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
tuttimodeexecutionservice "github.com/tutti-os/tutti/services/tuttid/service/tuttimodeexecution"
)
type sequentialSessionCreatorRecorder struct {
inputs []agentservice.CreateSessionInput
launches []IssueRunLaunch
}
func (r *sequentialSessionCreatorRecorder) Launch(_ context.Context, launch IssueRunLaunch) error {
input := createSessionInputFromLaunch(launch)
r.launches = append(r.launches, launch)
r.inputs = append(r.inputs, input)
return nil
}
type staticIssueSourceSessionContextResolver struct {
context IssueSourceSessionContext
ok bool
}
func (r staticIssueSourceSessionContextResolver) ResolveSourceSessionContext(
_ string,
_ string,
) (IssueSourceSessionContext, bool) {
return r.context, r.ok
}
type countingIssueSourceSessionContextResolver struct {
context IssueSourceSessionContext
calls int
}
func (r *countingIssueSourceSessionContextResolver) ResolveSourceSessionContext(
_ string,
_ string,
) (IssueSourceSessionContext, bool) {
r.calls++
return r.context, r.calls == 1
}
func availableIssueSourceSessionContextResolver() IssueSourceSessionContextResolver {
return staticIssueSourceSessionContextResolver{
context: IssueSourceSessionContext{
RailPlacement: &IssueRunRailPlacement{
Kind: "conversations",
SectionKey: "conversations",
},
},
ok: true,
}
}
func createSessionInputFromLaunch(launch IssueRunLaunch) agentservice.CreateSessionInput {
reasoningIntensity := launch.ReasoningIntensity
return agentservice.CreateSessionInput{
AgentSessionID: launch.AgentSessionID,
AgentTargetID: launch.AgentTargetID,
ReasoningIntensity: &reasoningIntensity,
InitialContent: []agentservice.PromptContentBlock{{Type: "text", Text: launch.Prompt}},
ClientSubmitID: launch.ClientSubmitID,
Title: optionalTrimmedTestString(launch.Title),
Cwd: optionalTrimmedTestString(launch.ExecutionDirectory),
Model: optionalTrimmedTestString(launch.Model),
ModelPlanID: optionalTrimmedTestString(launch.ModelPlanID),
ReasoningEffort: optionalTrimmedTestString(launch.ReasoningEffort),
PermissionModeID: optionalTrimmedTestString(launch.PermissionModeID),
StrictPermissionMode: strings.TrimSpace(launch.PermissionModeID) != "",
}
}
func optionalTrimmedTestString(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func TestIssueSequentialExecutionDispatchesSuccessorOnlyAfterUserAcceptance(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-sequential", Name: "Sequential"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-sequential", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-sequential",
TopicID: workspaceissues.DefaultTopicID,
Title: "Sequential issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex},
{TaskID: "task-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 1 || creator.inputs[0].AgentSessionID == "" {
t.Fatalf("initial dispatches = %#v, want one durable session launch", creator.inputs)
}
if creator.inputs[0].ReasoningIntensity == nil || *creator.inputs[0].ReasoningIntensity != workspaceissues.DefaultReasoningIntensity {
t.Fatalf("initial reasoning intensity = %#v, want Issue default", creator.inputs[0].ReasoningIntensity)
}
first := detail.Tasks[0]
if first.Status != workspaceissues.StatusRunning || first.LatestRunID == "" {
t.Fatalf("first task = %#v, want running with run", first)
}
if _, err := service.CompleteRun(ctx, "workspace-sequential", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches before acceptance = %d, want 1", len(creator.inputs))
}
if _, err := service.UpdateTask(ctx, "workspace-sequential", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusCompleted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(accept) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after acceptance = %d, want 2", len(creator.inputs))
}
updated, err := service.GetIssueDetail(ctx, "workspace-sequential", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if updated.Tasks[1].Status != workspaceissues.StatusRunning {
t.Fatalf("second task status = %q, want running", updated.Tasks[1].Status)
}
}
func TestIssueParallelExecutionDispatchesIndependentRootsAndWaitsForAcceptedDependencies(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-parallel", Name: "Parallel"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-parallel", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-parallel",
TopicID: workspaceissues.DefaultTopicID,
Title: "Parallel issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
ParallelExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "First root", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: "/worktrees/task-1"},
{TaskID: "task-2", Title: "Second root", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: "/worktrees/task-2"},
{TaskID: "task-3", Title: "Dependent", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: "/worktrees/task-3", DependencyTaskIDs: []string{"task-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("initial parallel dispatches = %d, want 2", len(creator.inputs))
}
if detail.Tasks[0].Status != workspaceissues.StatusRunning || detail.Tasks[1].Status != workspaceissues.StatusRunning || detail.Tasks[2].Status != workspaceissues.StatusNotStarted {
t.Fatalf("initial task states = %#v", detail.Tasks)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-parallel", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches before acceptance = %d, want 2", len(creator.inputs))
}
if _, err := service.UpdateTask(ctx, "workspace-parallel", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusCompleted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(accept) error = %v", err)
}
if len(creator.inputs) != 3 {
t.Fatalf("dispatches after acceptance = %d, want 3", len(creator.inputs))
}
updated, err := service.GetIssueDetail(ctx, "workspace-parallel", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if updated.Tasks[2].Status != workspaceissues.StatusRunning {
t.Fatalf("dependent task status = %q, want running", updated.Tasks[2].Status)
}
}
func TestIssueParallelExecutionHonorsWorkspaceConcurrencyAndRefillsSlots(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-parallel-limit", Name: "Parallel limit"}); err != nil {
t.Fatal(err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatal(err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{RunLauncher: creator, Store: store, AgentTargetReader: store}
tasks := make([]CreateIssueManagerTaskItemInput, 0, maxWorkspaceParallelIssueRuns+1)
for index := 0; index < maxWorkspaceParallelIssueRuns+1; index++ {
tasks = append(tasks, CreateIssueManagerTaskItemInput{
TaskID: fmt.Sprintf("task-limit-%d", index),
Title: fmt.Sprintf("Parallel %d", index),
AgentTargetID: agenttargetbiz.IDLocalCodex,
ExecutionDirectory: fmt.Sprintf("/worktrees/limit-%d", index),
})
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-parallel-limit", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-parallel-limit", TopicID: workspaceissues.DefaultTopicID, Title: "Bounded parallel",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan), ParallelExecution: true,
},
Tasks: tasks,
})
if err != nil {
t.Fatal(err)
}
if len(creator.inputs) != maxWorkspaceParallelIssueRuns {
t.Fatalf("initial parallel dispatches = %d, want %d", len(creator.inputs), maxWorkspaceParallelIssueRuns)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-parallel-limit", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{Status: string(workspaceissues.StatusCompleted)}); err != nil {
t.Fatal(err)
}
if len(creator.inputs) != maxWorkspaceParallelIssueRuns+1 {
t.Fatalf("dispatches after slot refill = %d, want %d", len(creator.inputs), maxWorkspaceParallelIssueRuns+1)
}
}
func TestIssueParallelExecutionRejectsSharedExecutionDirectory(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-parallel-unsafe", Name: "Parallel unsafe"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
service := IssueManagerService{Store: store}
_, err := service.CreateIssueFromPlan(ctx, "workspace-parallel-unsafe", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
TopicID: workspaceissues.DefaultTopicID,
Title: "Unsafe parallel issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
ParallelExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{Title: "One", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: "/shared"},
{Title: "Two", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: "/shared"},
},
})
if !errors.Is(err, workspaceissues.ErrInvalidArgument) {
t.Fatalf("CreateIssueFromPlan() error = %v, want ErrInvalidArgument", err)
}
}
func TestIssueAgentSessionSettlementCompletesRunWithUsageAndAgentClaim(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-settlement", Name: "Settlement"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-settlement", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-settlement",
TopicID: workspaceissues.DefaultTopicID,
Title: "Settle from Agent state",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-settlement",
Title: "Execute",
AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
outcome := "completed"
turnID := "turn-settlement"
reader := issueRunSettlementReaderStub{settlementByRunID: map[string]IssueRunSettlement{
detail.Tasks[0].LatestRunID: {TurnID: turnID, Status: workspaceissues.StatusCompleted},
}}
coordinator := IssueExecutionCoordinator{Issues: &service, SettlementReader: reader}
coordinator.ObserveAgentSessionState(ctx, canonical.ReportSessionStateInput{
WorkspaceID: "workspace-settlement",
AgentSessionID: creator.inputs[0].AgentSessionID,
State: canonical.WorkspaceAgentSessionStateUpdate{
TurnLifecycle: &canonical.WorkspaceAgentTurnLifecycle{Phase: "settled", Outcome: &outcome},
Turn: &canonical.WorkspaceAgentTurnStateUpdate{TurnID: turnID, Phase: "settled", Outcome: outcome},
RuntimeContext: map[string]any{
"usage": map[string]any{
"inputTokens": int64(100),
"outputTokens": int64(20),
"cacheReadTokens": int64(30),
"cacheWriteTokens": int64(40),
"quotas": []map[string]any{{
"quotaType": "weekly",
"percentRemaining": float64(25),
}},
},
},
},
}, canonical.ReportSessionStateReply{})
settled, err := service.GetIssueDetail(ctx, "workspace-settlement", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
task := settled.Tasks[0]
if task.Status != workspaceissues.StatusPendingAcceptance || task.AcceptanceState != workspaceissues.AcceptanceAgentClaimed {
t.Fatalf("settled task = %#v, want pending acceptance with Agent claim", task)
}
if settled.LatestRun == nil || settled.LatestRun.Status != workspaceissues.StatusCompleted || settled.LatestRun.Usage.Total() != 190 {
t.Fatalf("latest run = %#v, want completed with all usage categories", settled.LatestRun)
}
if settled.Issue.Budget.ConsumedTokens != 190 || !settled.Issue.Budget.HasRemainingQuota || settled.Issue.Budget.RemainingQuotaPercent != 25 {
t.Fatalf("settled Issue budget = %#v", settled.Issue.Budget)
}
if err := service.RecordAutomationReviewOutcome(ctx, "workspace-settlement", creator.inputs[0].AgentSessionID, "All checks passed.\nVERDICT: PASS", true, true); err != nil {
t.Fatalf("RecordAutomationReviewOutcome() error = %v", err)
}
reviewed, err := service.GetIssueDetail(ctx, "workspace-settlement", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail(reviewed) error = %v", err)
}
if reviewed.Tasks[0].AcceptanceState != workspaceissues.AcceptanceAutoChecked || !strings.Contains(reviewed.Tasks[0].AcceptanceSummary, "VERDICT: PASS") {
t.Fatalf("reviewed task = %#v, want auto_checked with review evidence", reviewed.Tasks[0])
}
}
func TestIssueBudgetRecoveryUpdateResumesEligibleDispatch(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-budget-recovery", Name: "Budget recovery"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-budget-recovery", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-budget-recovery",
TopicID: workspaceissues.DefaultTopicID,
Title: "Recover budget",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
HasBudget: true,
Budget: workspaceissues.Budget{
Mode: workspaceissues.BudgetModeFixed,
TokenLimit: 60_000,
QuotaWaterlinePercent: workspaceissues.DefaultQuotaWaterlinePercent,
},
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-budget-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex},
{TaskID: "task-budget-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-budget-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-budget-recovery", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
Usage: workspaceissues.TokenUsage{InputTokens: 30_000},
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if _, err := service.UpdateTask(ctx, "workspace-budget-recovery", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusCompleted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(accept) error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches while soft limited = %d, want 1", len(creator.inputs))
}
if _, err := service.UpdateIssue(ctx, "workspace-budget-recovery", detail.Issue.IssueID, UpdateIssueManagerIssueInput{
HasBudget: true,
Budget: workspaceissues.Budget{
Mode: workspaceissues.BudgetModeFixed,
TokenLimit: 100_000,
QuotaWaterlinePercent: workspaceissues.DefaultQuotaWaterlinePercent,
},
}); err != nil {
t.Fatalf("UpdateIssue(add budget) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after budget recovery = %d, want 2", len(creator.inputs))
}
}
func TestIssueLowerIntensityRecoveryReleasesPreDispatchBudgetGate(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-intensity-recovery", Name: "Intensity recovery"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-intensity-recovery", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-intensity-recovery",
TopicID: workspaceissues.DefaultTopicID,
Title: "Lower intensity",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
HasBudget: true,
Budget: workspaceissues.Budget{
Mode: workspaceissues.BudgetModeFixed,
TokenLimit: 60_000,
QuotaWaterlinePercent: workspaceissues.DefaultQuotaWaterlinePercent,
},
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-intensity-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex},
{TaskID: "task-intensity-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-intensity-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-intensity-recovery", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
Usage: workspaceissues.TokenUsage{InputTokens: 30_000},
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if _, err := service.UpdateTask(ctx, "workspace-intensity-recovery", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusCompleted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(accept) error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches before intensity recovery = %d, want 1", len(creator.inputs))
}
softLimited, err := service.GetIssueDetail(ctx, "workspace-intensity-recovery", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if softLimited.Issue.Budget.Status != workspaceissues.BudgetStatusSoftLimited {
t.Fatalf("budget status = %q, want soft_limited", softLimited.Issue.Budget.Status)
}
if _, err := service.UpdateIssue(ctx, "workspace-intensity-recovery", detail.Issue.IssueID, UpdateIssueManagerIssueInput{
ExecutionProfile: workspaceissues.ExecutionProfile{},
HasExecutionProfile: true,
HasBudget: true,
Budget: workspaceissues.Budget{
Mode: workspaceissues.BudgetModeFixed,
TokenLimit: 60_000,
QuotaWaterlinePercent: workspaceissues.DefaultQuotaWaterlinePercent,
},
}); err != nil {
t.Fatalf("UpdateIssue(lower intensity) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after intensity recovery = %d, want 2", len(creator.inputs))
}
}
func TestIssueExplicitPauseBlocksOnlyFutureDispatchAndResumeContinues(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-dispatch-pause", Name: "Dispatch pause"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-dispatch-pause", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-dispatch-pause",
TopicID: workspaceissues.DefaultTopicID,
Title: "Pause dispatch",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-pause-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex},
{TaskID: "task-pause-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-pause-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("initial dispatches = %d, want 1", len(creator.inputs))
}
paused, err := service.UpdateIssue(ctx, "workspace-dispatch-pause", detail.Issue.IssueID, UpdateIssueManagerIssueInput{
DispatchPaused: true,
HasDispatchPaused: true,
})
if err != nil {
t.Fatalf("UpdateIssue(pause) error = %v", err)
}
if !paused.DispatchPaused {
t.Fatalf("DispatchPaused = false after durable pause")
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-dispatch-pause", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{Status: string(workspaceissues.StatusCompleted)}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if _, err := service.UpdateTask(ctx, "workspace-dispatch-pause", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{Status: string(workspaceissues.StatusCompleted), HasStatus: true}); err != nil {
t.Fatalf("UpdateTask(accept) error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches while explicitly paused = %d, want 1", len(creator.inputs))
}
resumed, err := service.UpdateIssue(ctx, "workspace-dispatch-pause", detail.Issue.IssueID, UpdateIssueManagerIssueInput{
DispatchPaused: false,
HasDispatchPaused: true,
})
if err != nil {
t.Fatalf("UpdateIssue(resume) error = %v", err)
}
if resumed.DispatchPaused {
t.Fatalf("DispatchPaused = true after resume")
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after resume = %d, want 2", len(creator.inputs))
}
}
type strictPermissionSessionCreator struct {
inputs []agentservice.CreateSessionInput
supportedModes map[string]struct{}
}
func (r *strictPermissionSessionCreator) Launch(_ context.Context, launch IssueRunLaunch) error {
input := createSessionInputFromLaunch(launch)
r.inputs = append(r.inputs, input)
if input.StrictPermissionMode && input.PermissionModeID != nil {
if _, ok := r.supportedModes[*input.PermissionModeID]; !ok {
return fmt.Errorf("unsupported permission mode %q", *input.PermissionModeID)
}
}
return nil
}
func TestIssueTaskLaunchAppliesTaskLevelOverridesWithStrictPermissionMode(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-strict-overrides", Name: "Strict overrides"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &strictPermissionSessionCreator{supportedModes: map[string]struct{}{"acceptEdits": {}}}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
if _, err := service.CreateIssueFromPlan(ctx, "workspace-strict-overrides", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-with-overrides",
TopicID: workspaceissues.DefaultTopicID,
Title: "Task-level overrides",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-with-overrides",
Title: "Overridden launch",
AgentTargetID: agenttargetbiz.IDLocalCodex,
PermissionModeID: "acceptEdits",
ReasoningEffort: "high",
}},
}); err != nil {
t.Fatalf("CreateIssueFromPlan(overrides) error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches = %d, want 1", len(creator.inputs))
}
launch := creator.inputs[0]
if launch.PermissionModeID == nil || *launch.PermissionModeID != "acceptEdits" || !launch.StrictPermissionMode {
t.Fatalf("launch permission mode = %#v strict=%v, want strict explicit acceptEdits", launch.PermissionModeID, launch.StrictPermissionMode)
}
if launch.ReasoningEffort == nil || *launch.ReasoningEffort != "high" {
t.Fatalf("launch reasoning effort = %#v, want explicit high", launch.ReasoningEffort)
}
// A task without an explicit permission mode keeps the provider-default
// resolution and must not opt into strict rejection.
if _, err := service.CreateIssueFromPlan(ctx, "workspace-strict-overrides", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-without-overrides",
TopicID: workspaceissues.DefaultTopicID,
Title: "Default launch",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-without-overrides",
Title: "Default launch",
AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
}); err != nil {
t.Fatalf("CreateIssueFromPlan(defaults) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches = %d, want 2", len(creator.inputs))
}
if creator.inputs[1].PermissionModeID != nil || creator.inputs[1].StrictPermissionMode {
t.Fatalf("default launch = %#v strict=%v, want no explicit mode and no strict flag", creator.inputs[1].PermissionModeID, creator.inputs[1].StrictPermissionMode)
}
}
func TestIssueTaskLaunchFailsClosedOnUnsupportedPermissionMode(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-strict-reject", Name: "Strict reject"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &strictPermissionSessionCreator{supportedModes: map[string]struct{}{"acceptEdits": {}}}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-strict-reject", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-strict-reject",
TopicID: workspaceissues.DefaultTopicID,
Title: "Stale permission mode",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-strict-reject",
Title: "Launch with stale mode",
AgentTargetID: agenttargetbiz.IDLocalCodex,
PermissionModeID: "bogus-mode",
}},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 1 || !creator.inputs[0].StrictPermissionMode {
t.Fatalf("launch inputs = %#v, want one strict launch attempt", creator.inputs)
}
settled, err := service.GetIssueDetail(ctx, "workspace-strict-reject", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
task := settled.Tasks[0]
if task.Status == workspaceissues.StatusRunning {
t.Fatalf("task status = %q, want fail-closed launch instead of a silently downgraded running session", task.Status)
}
if settled.LatestRun == nil || settled.LatestRun.Status != workspaceissues.StatusFailed ||
!strings.Contains(settled.LatestRun.ErrorMessage, "unsupported permission mode") {
t.Fatalf("latest run = %#v, want failed run carrying the strict rejection", settled.LatestRun)
}
}
func initIssueTaskGitRepo(t *testing.T) string {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git is unavailable in this environment")
}
dir := t.TempDir()
for _, args := range [][]string{
{"init", "--initial-branch=main"},
{"config", "user.email", "test@example.com"},
{"config", "user.name", "Test"},
{"commit", "--allow-empty", "-m", "init"},
} {
command := exec.Command("git", append([]string{"-C", dir}, args...)...)
// The push hook exports GIT_DIR to the real repository; without the
// scrub these commands would commit onto the branch being pushed.
command.Env = gitEnvironmentWithoutRepoOverrides()
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("git %v error = %v: %s", args, err, output)
}
}
return dir
}
func newParallelizableDispatchService(t *testing.T, workspaceID string) (IssueManagerService, *sequentialSessionCreatorRecorder, string) {
t.Helper()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Workspace"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
worktreeRoot := t.TempDir()
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
TaskWorktreeRoot: worktreeRoot,
}
return service, creator, worktreeRoot
}
func acceptIssueTask(t *testing.T, service IssueManagerService, workspaceID string, issueID string, taskID string) {
t.Helper()
ctx := context.Background()
detail, err := service.GetIssueDetail(ctx, workspaceID, issueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
for _, task := range detail.Tasks {
if task.TaskID != taskID {
continue
}
if _, err := service.CompleteRun(ctx, workspaceID, issueID, taskID, task.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
}); err != nil {
t.Fatalf("CompleteRun(%q) error = %v", taskID, err)
}
if _, err := service.UpdateTask(ctx, workspaceID, issueID, taskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusCompleted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(accept %q) error = %v", taskID, err)
}
return
}
t.Fatalf("task %q not found", taskID)
}
func TestSequentialIssueRunsParallelizableTasksInIsolatedWorktrees(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := initIssueTaskGitRepo(t)
service, creator, worktreeRoot := newParallelizableDispatchService(t, "ws-par-worktree")
if _, err := service.CreateIssueFromPlan(ctx, "ws-par-worktree", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-worktree",
TopicID: workspaceissues.DefaultTopicID,
Title: "Worktree issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
{TaskID: "p2", Title: "Second parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
},
}); err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches = %d, want both parallelizable tasks launched together", len(creator.inputs))
}
cwds := map[string]bool{}
for _, input := range creator.inputs {
if input.Cwd == nil || *input.Cwd == "" {
t.Fatalf("launch cwd missing: %#v", input)
}
cwd := *input.Cwd
if cwd == repo {
t.Fatalf("launch reused the shared checkout %q instead of a worktree", cwd)
}
if !strings.HasPrefix(cwd, worktreeRoot) {
t.Fatalf("worktree %q is outside the configured root %q", cwd, worktreeRoot)
}
if _, err := os.Stat(filepath.Join(cwd, ".git")); err != nil {
t.Fatalf("worktree %q is not a linked checkout: %v", cwd, err)
}
cwds[cwd] = true
prompt := creator.inputs[0].InitialContent[0].Text
if !strings.Contains(prompt, "dedicated git worktree") || !strings.Contains(prompt, "Do not push") {
t.Fatalf("prompt lacks worktree isolation contract: %q", prompt)
}
}
if len(cwds) != 2 {
t.Fatalf("worktree cwds = %v, want two distinct directories", cwds)
}
}
func TestSequentialIssueWorktreeDelegatesKeepSourceProjectRailPlacement(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := initIssueTaskGitRepo(t)
service, creator, worktreeRoot := newParallelizableDispatchService(t, "ws-par-source-placement")
sectionKey := "project:" + repo
sourcePlacement := &IssueRunRailPlacement{
Kind: "project",
ProjectPath: repo,
SectionKey: sectionKey,
}
resolver := &countingIssueSourceSessionContextResolver{
context: IssueSourceSessionContext{
WorkingDirectory: repo,
RailPlacement: sourcePlacement,
},
}
service.SourceSessionContextResolver = resolver
if _, err := service.CreateIssueFromPlan(ctx, "ws-par-source-placement", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-source-placement",
TopicID: workspaceissues.DefaultTopicID,
Title: "Source placement issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, Parallelizable: true},
{TaskID: "p2", Title: "Second parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, Parallelizable: true},
},
}); err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.launches) != 2 {
t.Fatalf("launches = %d, want both parallelizable tasks", len(creator.launches))
}
for _, launch := range creator.launches {
if launch.ExecutionDirectory == repo || !strings.HasPrefix(launch.ExecutionDirectory, worktreeRoot) {
t.Fatalf(
"launch execution directory = %q, want isolated worktree under %q",
launch.ExecutionDirectory,
worktreeRoot,
)
}
if launch.RailPlacement == nil {
t.Fatalf("launch rail placement is nil")
}
if *launch.RailPlacement != *sourcePlacement {
t.Fatalf("launch rail placement = %#v, want %#v", launch.RailPlacement, sourcePlacement)
}
}
if resolver.calls != 1 {
t.Fatalf("source context resolver calls = %d, want one immutable dispatch snapshot", resolver.calls)
}
}
func TestIssueWithMissingSourceSessionDoesNotClaimOrLaunchRun(t *testing.T) {
t.Parallel()
ctx := context.Background()
service, creator, _ := newParallelizableDispatchService(t, "ws-missing-source")
service.SourceSessionContextResolver = staticIssueSourceSessionContextResolver{}
detail, err := service.CreateIssueFromPlan(ctx, "ws-missing-source", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-missing-source",
TopicID: workspaceissues.DefaultTopicID,
Title: "Missing source",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "deleted-planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "Must wait", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.launches) != 0 {
t.Fatalf("launches = %#v, want none while source Session is unavailable", creator.launches)
}
if detail.Tasks[0].Status != workspaceissues.StatusNotStarted || detail.Tasks[0].LatestRunID != "" {
t.Fatalf("task = %#v, want unclaimed not_started task", detail.Tasks[0])
}
}
func TestSequentialIssueDegradesSharedNonGitParallelizableTasksToExclusive(t *testing.T) {
t.Parallel()
ctx := context.Background()
shared := t.TempDir()
service, creator, _ := newParallelizableDispatchService(t, "ws-par-degrade")
if _, err := service.CreateIssueFromPlan(ctx, "ws-par-degrade", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-degrade",
TopicID: workspaceissues.DefaultTopicID,
Title: "Degrade issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: shared, Parallelizable: true},
{TaskID: "p2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: shared, Parallelizable: true},
},
}); err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches = %d, want exclusive degradation for a shared non-git directory", len(creator.inputs))
}
if creator.inputs[0].Cwd == nil || *creator.inputs[0].Cwd != shared {
t.Fatalf("exclusive launch cwd = %#v, want the shared directory", creator.inputs[0].Cwd)
}
acceptIssueTask(t, service, "ws-par-degrade", "issue-degrade", "p1")
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after acceptance = %d, want the successor", len(creator.inputs))
}
}
func TestSequentialIssueExclusiveTaskWaitsForParallelizableBatch(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := initIssueTaskGitRepo(t)
service, creator, _ := newParallelizableDispatchService(t, "ws-par-barrier")
if _, err := service.CreateIssueFromPlan(ctx, "ws-par-barrier", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-barrier",
TopicID: workspaceissues.DefaultTopicID,
Title: "Barrier issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
{TaskID: "p2", Title: "Second parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
{TaskID: "s3", Title: "Exclusive", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo},
},
}); err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("initial dispatches = %d, want only the parallelizable batch", len(creator.inputs))
}
acceptIssueTask(t, service, "ws-par-barrier", "issue-barrier", "p1")
if len(creator.inputs) != 2 {
t.Fatalf("dispatches with p2 still live = %d, exclusive task must wait", len(creator.inputs))
}
acceptIssueTask(t, service, "ws-par-barrier", "issue-barrier", "p2")
if len(creator.inputs) != 3 {
t.Fatalf("dispatches after batch drained = %d, want the exclusive task", len(creator.inputs))
}
exclusive := creator.inputs[2]
if exclusive.Cwd == nil || *exclusive.Cwd != repo {
t.Fatalf("exclusive launch cwd = %#v, want the base checkout", exclusive.Cwd)
}
}
func TestAutoAcceptTaskCompletionAdvancesDispatchWithoutHumanGate(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-auto-accept", Name: "Auto accept"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-auto-accept", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-auto-accept",
TopicID: workspaceissues.DefaultTopicID,
Title: "Auto accept issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex, AutoAccept: true},
{TaskID: "task-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-auto-accept", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after auto-accepted completion = %d, want 2", len(creator.inputs))
}
updated, err := service.GetIssueDetail(ctx, "workspace-auto-accept", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if updated.Tasks[0].Status != workspaceissues.StatusCompleted ||
updated.Tasks[0].AcceptanceState != workspaceissues.AcceptanceUserAccepted {
t.Fatalf("auto-accepted task = %#v, want completed and user_accepted", updated.Tasks[0])
}
if updated.Tasks[1].Status != workspaceissues.StatusRunning {
t.Fatalf("second task status = %q, want running", updated.Tasks[1].Status)
}
}
func TestReworkFromPendingAcceptanceRedispatchesSequentialHead(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-rework", Name: "Rework"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-rework", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-rework",
TopicID: workspaceissues.DefaultTopicID,
Title: "Rework issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "Only", AgentTargetID: agenttargetbiz.IDLocalCodex},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-rework", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
}); err != nil {
t.Fatalf("CompleteRun() error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches while pending acceptance = %d, want 1", len(creator.inputs))
}
if _, err := service.UpdateTask(ctx, "workspace-rework", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusNotStarted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(rework) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after rework = %d, want 2", len(creator.inputs))
}
updated, err := service.GetIssueDetail(ctx, "workspace-rework", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if updated.Tasks[0].Status != workspaceissues.StatusRunning {
t.Fatalf("reworked task status = %q, want running", updated.Tasks[0].Status)
}
}
func TestTuttiModePlanInitialScheduleMaterializationIsInert(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-inert-tutti", Name: "Inert Tutti"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
now := time.UnixMilli(1_700_000_000_000).UTC()
createIssueManagerTuttiWorkflowFixture(t, store, "ws-inert-tutti", "inert-tutti", "planning-session", now)
creator := &sequentialSessionCreatorRecorder{}
executions := &tuttimodeexecutionservice.Service{
Store: store,
Clock: func() time.Time { return now },
}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
TuttiModeExecutions: executions,
}
detail, err := service.CreateIssueFromPlan(ctx, "ws-inert-tutti", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "tutti-mode-plan-inert-tutti",
TopicID: workspaceissues.DefaultTopicID,
Title: "Inert Tutti issue",
PlanningSource: string(workspaceissues.PlanningSourceTuttiModePlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
TuttiModeWorkflowOwned: true,
TuttiModeWorkflowID: "inert-tutti",
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex, AutoAccept: true},
{TaskID: "task-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 0 {
t.Fatalf("initial dispatches = %d, want 0", len(creator.inputs))
}
for _, task := range detail.Tasks {
if task.Status != workspaceissues.StatusNotStarted || task.LatestRunID != "" {
t.Fatalf("materialized task = %#v, want inert not_started task", task)
}
}
runs, err := service.ListRuns(ctx, "ws-inert-tutti", detail.Issue.IssueID, "")
if err != nil || len(runs) != 0 {
t.Fatalf("materialized runs = %#v error=%v, want none", runs, err)
}
aggregate, err := executions.GetByIssue(ctx, "ws-inert-tutti", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetByIssue() error = %v", err)
}
if aggregate.Execution.Status != executionbiz.StatusAwaitingSchedule ||
aggregate.Execution.GraphRevision != 1 ||
len(aggregate.Checkpoints) != 1 ||
aggregate.Checkpoints[0].Kind != executionbiz.CheckpointKindInitialSchedule ||
aggregate.Checkpoints[0].Status != executionbiz.CheckpointStatusActive {
t.Fatalf("initial execution aggregate = %#v", aggregate)
}
}
type runSessionCancellerRecorder struct {
canceledSessionIDs []string
}
func (r *runSessionCancellerRecorder) RequestRunCancellation(_ context.Context, request IssueRunCancellationRequest) (IssueRunCancelResult, error) {
r.canceledSessionIDs = append(r.canceledSessionIDs, request.AgentSessionID)
return IssueRunCancelResult{
State: IssueRunCancelCanceled,
Settlement: &IssueRunSettlement{
WorkspaceID: request.WorkspaceID,
AgentSessionID: request.AgentSessionID,
Status: workspaceissues.StatusCanceled,
},
}, nil
}
type failingRunSessionCanceller struct{}
func (failingRunSessionCanceller) RequestRunCancellation(context.Context, IssueRunCancellationRequest) (IssueRunCancelResult, error) {
return IssueRunCancelResult{}, errors.New("cancel unavailable")
}
type reentrantRunSessionCanceller struct {
coordinator *IssueExecutionCoordinator
turnID string
}
func (r *reentrantRunSessionCanceller) RequestRunCancellation(ctx context.Context, request IssueRunCancellationRequest) (IssueRunCancelResult, error) {
outcome := "canceled"
r.coordinator.ObserveAgentSessionState(ctx, canonical.ReportSessionStateInput{
WorkspaceID: request.WorkspaceID,
AgentSessionID: request.AgentSessionID,
State: canonical.WorkspaceAgentSessionStateUpdate{
TurnLifecycle: &canonical.WorkspaceAgentTurnLifecycle{Phase: "settled", Outcome: &outcome},
Turn: &canonical.WorkspaceAgentTurnStateUpdate{TurnID: r.turnID, Phase: "settled", Outcome: outcome},
},
}, canonical.ReportSessionStateReply{})
return IssueRunCancelResult{State: IssueRunCancelAccepted}, nil
}
func TestCancelIssueExecutionCancelsAllRunningTraditionalPlanRuns(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-stop-cascade", Name: "Stop cascade"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
canceller := &runSessionCancellerRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
MutationLocks: NewIssueMutationLocks(),
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
}
coordinator := IssueExecutionCoordinator{Issues: &service, RunSessionCanceller: canceller}
detail, err := service.CreateIssueFromPlan(ctx, "ws-stop-cascade", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "traditional-plan-stop-cascade",
TopicID: workspaceissues.DefaultTopicID,
Title: "Stop cascade issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex},
{TaskID: "task-2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, DependencyTaskIDs: []string{"task-1"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 1 {
t.Fatalf("initial dispatches = %d, want 1", len(creator.inputs))
}
// Explicit Issue cancellation settles the live run and durably pauses
// traditional Plan dispatch.
if _, err := coordinator.CancelIssueExecution(ctx, "ws-stop-cascade", detail.Issue.IssueID); err != nil {
t.Fatalf("CancelIssueExecution() error = %v", err)
}
if len(canceller.canceledSessionIDs) != 1 {
t.Fatalf("canceled sessions = %v, want the running run's session", canceller.canceledSessionIDs)
}
updated, err := service.GetIssueDetail(ctx, "ws-stop-cascade", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if !updated.Issue.DispatchPaused {
t.Fatalf("issue dispatchPaused = false, want true after stop")
}
if updated.Tasks[0].Status != workspaceissues.StatusCanceled {
t.Fatalf("running task status = %q, want canceled", updated.Tasks[0].Status)
}
if updated.Tasks[1].Status != workspaceissues.StatusNotStarted {
t.Fatalf("successor status = %q, want untouched not_started", updated.Tasks[1].Status)
}
if len(creator.inputs) != 1 {
t.Fatalf("dispatches after stop = %d, want no new launches", len(creator.inputs))
}
// Stop is idempotent.
if _, err := coordinator.CancelIssueExecution(ctx, "ws-stop-cascade", detail.Issue.IssueID); err != nil {
t.Fatalf("CancelIssueExecution() repeat error = %v", err)
}
}
func TestCancelIssueExecutionAllowsSynchronousAgentSettlementReentry(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-stop-reentry", Name: "Stop reentry"}); err != nil {
t.Fatal(err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatal(err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
MutationLocks: NewIssueMutationLocks(),
RunLaunchGate: NewIssueRunLaunchGate(),
}
detail, err := service.CreateIssueFromPlan(ctx, "ws-stop-reentry", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-stop-reentry",
TopicID: workspaceissues.DefaultTopicID,
Title: "Stop with synchronous settlement",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
if err != nil {
t.Fatal(err)
}
runID := detail.Tasks[0].LatestRunID
turnID := "turn-stop-reentry"
reader := issueRunSettlementReaderStub{settlementByRunID: map[string]IssueRunSettlement{
runID: {TurnID: turnID, Status: workspaceissues.StatusCanceled},
}}
coordinator := &IssueExecutionCoordinator{Issues: &service, SettlementReader: reader}
coordinator.RunSessionCanceller = &reentrantRunSessionCanceller{coordinator: coordinator, turnID: turnID}
result := make(chan error, 1)
go func() {
_, cancelErr := coordinator.CancelIssueExecution(ctx, "ws-stop-reentry", detail.Issue.IssueID)
result <- cancelErr
}()
select {
case cancelErr := <-result:
if cancelErr != nil {
t.Fatalf("CancelIssueExecution() error = %v", cancelErr)
}
case <-time.After(time.Second):
t.Fatal("CancelIssueExecution() deadlocked on synchronous Agent settlement")
}
updated, err := service.GetIssueDetail(ctx, "ws-stop-reentry", detail.Issue.IssueID)
if err != nil {
t.Fatal(err)
}
if !updated.Issue.DispatchPaused || updated.Tasks[0].Status != workspaceissues.StatusCanceled {
t.Fatalf("stopped Issue = %#v tasks=%#v, want paused with canceled task", updated.Issue, updated.Tasks)
}
}
func TestCancelIssueExecutionDoesNotForgeCanceledOutcomeOnAgentFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-stop-failure", Name: "Stop failure"}); err != nil {
t.Fatal(err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatal(err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
MutationLocks: NewIssueMutationLocks(),
RunLaunchGate: NewIssueRunLaunchGate(),
}
detail, err := service.CreateIssueFromPlan(ctx, "ws-stop-failure", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-stop-failure",
TopicID: workspaceissues.DefaultTopicID,
Title: "Stop failure",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
if err != nil {
t.Fatal(err)
}
coordinator := IssueExecutionCoordinator{Issues: &service, RunSessionCanceller: failingRunSessionCanceller{}}
if _, err := coordinator.CancelIssueExecution(ctx, "ws-stop-failure", detail.Issue.IssueID); err == nil {
t.Fatal("CancelIssueExecution() error = nil, want Agent cancellation failure")
}
updated, err := service.GetIssueDetail(ctx, "ws-stop-failure", detail.Issue.IssueID)
if err != nil {
t.Fatal(err)
}
if !updated.Issue.DispatchPaused {
t.Fatal("Issue dispatch resumed after failed cancellation")
}
if updated.Tasks[0].Status != workspaceissues.StatusRunning {
t.Fatalf("task status = %q, want running until canonical Agent settlement", updated.Tasks[0].Status)
}
}
type reentrantIssueRunLauncher struct {
issues *IssueManagerService
}
func (l reentrantIssueRunLauncher) Launch(ctx context.Context, launch IssueRunLaunch) error {
_, err := l.issues.CompleteRun(ctx, launch.WorkspaceID, launch.IssueID, launch.TaskID, launch.RunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusCompleted),
})
return err
}
func TestIssueDispatchAllowsSynchronousLaunchSettlementReentry(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-launch-reentry", Name: "Launch reentry"}); err != nil {
t.Fatal(err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatal(err)
}
}
service := IssueManagerService{
Store: store,
AgentTargetReader: store,
MutationLocks: NewIssueMutationLocks(),
RunLaunchGate: NewIssueRunLaunchGate(),
}
service.RunLauncher = reentrantIssueRunLauncher{issues: &service}
result := make(chan error, 1)
go func() {
_, createErr := service.CreateIssueFromPlan(ctx, "ws-launch-reentry", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-launch-reentry",
TopicID: workspaceissues.DefaultTopicID,
Title: "Launch with synchronous settlement",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
result <- createErr
}()
select {
case createErr := <-result:
if createErr != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", createErr)
}
case <-time.After(time.Second):
t.Fatal("CreateIssueFromPlan() deadlocked on synchronous launch settlement")
}
updated, err := service.GetIssueDetail(ctx, "ws-launch-reentry", "issue-launch-reentry")
if err != nil {
t.Fatal(err)
}
if updated.Tasks[0].Status != workspaceissues.StatusPendingAcceptance {
t.Fatalf("task status = %q, want pending_acceptance", updated.Tasks[0].Status)
}
}
type blockingIssueRunLauncher struct {
started chan struct{}
release chan struct{}
}
func (l blockingIssueRunLauncher) Launch(context.Context, IssueRunLaunch) error {
close(l.started)
<-l.release
return nil
}
func TestIssueStopFencesInFlightLaunchBeforeCancelingSession(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "ws-launch-stop-race", Name: "Launch stop race"}); err != nil {
t.Fatal(err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatal(err)
}
}
launcher := blockingIssueRunLauncher{started: make(chan struct{}), release: make(chan struct{})}
canceller := &runSessionCancellerRecorder{}
service := IssueManagerService{
RunLauncher: launcher,
RunCancellationRequester: canceller,
Store: store,
AgentTargetReader: store,
MutationLocks: NewIssueMutationLocks(),
RunLaunchGate: NewIssueRunLaunchGate(),
}
coordinator := IssueExecutionCoordinator{Issues: &service, RunSessionCanceller: canceller}
createDone := make(chan error, 1)
go func() {
_, createErr := service.CreateIssueFromPlan(ctx, "ws-launch-stop-race", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-launch-stop-race",
TopicID: workspaceissues.DefaultTopicID,
Title: "Launch stop race",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
createDone <- createErr
}()
<-launcher.started
cancelDone := make(chan error, 1)
go func() {
_, cancelErr := coordinator.CancelIssueExecution(ctx, "ws-launch-stop-race", "issue-launch-stop-race")
cancelDone <- cancelErr
}()
deadline := time.Now().Add(time.Second)
for {
current, readErr := service.GetIssueDetail(ctx, "ws-launch-stop-race", "issue-launch-stop-race")
if readErr == nil && current.Issue.DispatchPaused {
break
}
if time.Now().After(deadline) {
t.Fatal("stop did not persist dispatch pause while launch was in flight")
}
time.Sleep(time.Millisecond)
}
select {
case err := <-cancelDone:
if err != nil {
t.Fatalf("CancelIssueExecution() error = %v", err)
}
case <-time.After(time.Second):
t.Fatal("CancelIssueExecution() waited for the external launch")
}
close(launcher.release)
if err := <-createDone; err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(canceller.canceledSessionIDs) != 2 {
t.Fatalf("canceled sessions = %v, want stop request plus post-launch compensation", canceller.canceledSessionIDs)
}
updated, err := service.GetIssueDetail(ctx, "ws-launch-stop-race", "issue-launch-stop-race")
if err != nil {
t.Fatal(err)
}
if !updated.Issue.DispatchPaused || updated.Tasks[0].Status != workspaceissues.StatusCanceled {
t.Fatalf("stopped Issue = %#v tasks=%#v, want paused with canceled task", updated.Issue, updated.Tasks)
}
}
func TestTraditionalPlanFailedRunCanBeReworkedAndRedispatched(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := openIssueServiceStore(t)
if err := store.Create(ctx, workspacebiz.Summary{ID: "workspace-fail-notify", Name: "Fail notify"}); err != nil {
t.Fatalf("Create() workspace error = %v", err)
}
for _, target := range agenttargetbiz.DefaultSystemTargets(time.Now().UnixMilli()) {
if _, err := store.PutAgentTarget(ctx, target); err != nil {
t.Fatalf("PutAgentTarget(%q) error = %v", target.ID, err)
}
}
creator := &sequentialSessionCreatorRecorder{}
service := IssueManagerService{
RunLauncher: creator,
Store: store,
AgentTargetReader: store,
SourceSessionContextResolver: availableIssueSourceSessionContextResolver(),
}
detail, err := service.CreateIssueFromPlan(ctx, "workspace-fail-notify", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "traditional-plan-fail-rework",
TopicID: workspaceissues.DefaultTopicID,
Title: "Fail rework issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SourceSessionID: "planning-session",
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "Only", AgentTargetID: agenttargetbiz.IDLocalCodex},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
if _, err := service.CompleteRun(ctx, "workspace-fail-notify", detail.Issue.IssueID, first.TaskID, first.LatestRunID, CompleteIssueManagerRunInput{
Status: string(workspaceissues.StatusFailed),
ErrorMessage: "Agent session ended without reporting run completion.",
}); err != nil {
t.Fatalf("CompleteRun(failed) error = %v", err)
}
if _, err := service.UpdateTask(ctx, "workspace-fail-notify", detail.Issue.IssueID, first.TaskID, UpdateIssueManagerTaskInput{
Status: string(workspaceissues.StatusNotStarted),
HasStatus: true,
}); err != nil {
t.Fatalf("UpdateTask(rework failed task) error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("dispatches after rework of failed task = %d, want 2", len(creator.inputs))
}
updated, err := service.GetIssueDetail(ctx, "workspace-fail-notify", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if updated.Tasks[0].Status != workspaceissues.StatusRunning {
t.Fatalf("reworked task status = %q, want running", updated.Tasks[0].Status)
}
}
func TestChainedParallelizableFlagsNormalizeToDependencyOrder(t *testing.T) {
t.Parallel()
ctx := context.Background()
service, _, _ := newParallelizableDispatchService(t, "ws-par-normalize")
detail, err := service.CreateIssueFromPlan(ctx, "ws-par-normalize", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-normalize",
TopicID: workspaceissues.DefaultTopicID,
Title: "Normalize issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
// A chained "parallel" group: p2 depends on its neighbor p1, so its
// flag is a lie the dispatcher would ignore; p3 depends on p2 (now
// exclusive) and may honestly stay parallelizable for future siblings.
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First", AgentTargetID: agenttargetbiz.IDLocalCodex, Parallelizable: true},
{TaskID: "p2", Title: "Second", AgentTargetID: agenttargetbiz.IDLocalCodex, Parallelizable: true, DependencyTaskIDs: []string{"p1"}},
{TaskID: "p3", Title: "Third", AgentTargetID: agenttargetbiz.IDLocalCodex, Parallelizable: true, DependencyTaskIDs: []string{"p2"}},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
flags := map[string]bool{}
for _, task := range detail.Tasks {
flags[task.TaskID] = task.Parallelizable
}
if !flags["p1"] || flags["p2"] || !flags["p3"] {
t.Fatalf("normalized parallelizable flags = %v, want p1=true p2=false p3=true", flags)
}
}
func TestIntegrationTaskPromptCarriesDependencyWorktreeBranches(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := initIssueTaskGitRepo(t)
service, creator, _ := newParallelizableDispatchService(t, "ws-par-integrate")
if _, err := service.CreateIssueFromPlan(ctx, "ws-par-integrate", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-integrate",
TopicID: workspaceissues.DefaultTopicID,
Title: "Integrate issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "p1", Title: "First parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
{TaskID: "p2", Title: "Second parallel", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, Parallelizable: true},
{TaskID: "integrate", Title: "Integrate", AgentTargetID: agenttargetbiz.IDLocalCodex, ExecutionDirectory: repo, DependencyTaskIDs: []string{"p1", "p2"}},
},
}); err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
if len(creator.inputs) != 2 {
t.Fatalf("initial dispatches = %d, want the parallel pair", len(creator.inputs))
}
acceptIssueTask(t, service, "ws-par-integrate", "issue-integrate", "p1")
acceptIssueTask(t, service, "ws-par-integrate", "issue-integrate", "p2")
if len(creator.inputs) != 3 {
t.Fatalf("dispatches after acceptance = %d, want the integration task", len(creator.inputs))
}
prompt := creator.inputs[2].InitialContent[0].Text
if !strings.Contains(prompt, "Dependency outputs") ||
!strings.Contains(prompt, "branch tutti/task/p1-") ||
!strings.Contains(prompt, "branch tutti/task/p2-") ||
!strings.Contains(prompt, "git merge") {
t.Fatalf("integration prompt lacks dependency branch pointers: %q", prompt)
}
}
func TestTuttiModeTaskPromptScalesValidationWithEffect(t *testing.T) {
t.Parallel()
task := workspaceissues.Task{Title: "Implement preferences", Content: "Wire both values"}
for _, testCase := range []struct {
effect int
want string
}{
{effect: 20, want: "at least one focused check"},
{effect: 50, want: "relevant tests plus an integration check"},
{effect: 90, want: "edge or variant case"},
} {
issue := workspaceissues.Issue{
Title: "Tutti preferences",
Content: "Implement the plan",
PlanningSource: workspaceissues.PlanningSourceTuttiModePlan,
ExecutionProfile: workspaceissues.ExecutionProfile{
ReasoningIntensity: testCase.effect,
},
}
prompt := issueTaskPrompt(issue, task, ".", "", "", nil)
if !strings.Contains(prompt, fmt.Sprintf("Tutti effect preference: %d/100", testCase.effect)) ||
!strings.Contains(prompt, testCase.want) {
t.Fatalf("effect %d prompt = %q, want validation guidance containing %q", testCase.effect, prompt, testCase.want)
}
}
}
func TestAgentSettlementCompletesOnlyTheInitiatingTurnRun(t *testing.T) {
t.Parallel()
ctx := context.Background()
service, creator, _ := newParallelizableDispatchService(t, "ws-turn-match")
reader := issueRunSettlementReaderStub{settlementByRunID: map[string]IssueRunSettlement{}}
coordinator := IssueExecutionCoordinator{Issues: &service, SettlementReader: reader}
detail, err := service.CreateIssueFromPlan(ctx, "ws-turn-match", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-turn-match",
TopicID: workspaceissues.DefaultTopicID,
Title: "Turn match issue",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{
{TaskID: "task-1", Title: "Only", AgentTargetID: agenttargetbiz.IDLocalCodex},
},
})
if err != nil {
t.Fatalf("CreateIssueFromPlan() error = %v", err)
}
first := detail.Tasks[0]
reader.settlementByRunID[first.LatestRunID] = IssueRunSettlement{
TurnID: "turn-brief",
Status: workspaceissues.StatusCompleted,
}
sessionID := creator.inputs[0].AgentSessionID
outcome := "completed"
settledState := func(turnID string) canonical.ReportSessionStateInput {
return canonical.ReportSessionStateInput{
WorkspaceID: "ws-turn-match",
AgentSessionID: sessionID,
State: canonical.WorkspaceAgentSessionStateUpdate{
TurnLifecycle: &canonical.WorkspaceAgentTurnLifecycle{Phase: "settled", Outcome: &outcome},
Turn: &canonical.WorkspaceAgentTurnStateUpdate{
TurnID: turnID,
Phase: "settled",
Outcome: outcome,
},
},
}
}
// A different turn settling in the delegate conversation (e.g. a human
// interjection) must not complete the run.
coordinator.ObserveAgentSessionState(ctx, settledState("turn-interjection"), canonical.ReportSessionStateReply{})
after, err := service.GetIssueDetail(ctx, "ws-turn-match", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if after.Tasks[0].Status != workspaceissues.StatusRunning {
t.Fatalf("task status after unrelated settle = %q, want running", after.Tasks[0].Status)
}
// The run's own initiating turn settling completes it.
coordinator.ObserveAgentSessionState(ctx, settledState("turn-brief"), canonical.ReportSessionStateReply{})
settled, err := service.GetIssueDetail(ctx, "ws-turn-match", detail.Issue.IssueID)
if err != nil {
t.Fatalf("GetIssueDetail() error = %v", err)
}
if settled.Tasks[0].Status != workspaceissues.StatusPendingAcceptance {
t.Fatalf("task status after initiating settle = %q, want pending_acceptance", settled.Tasks[0].Status)
}
}
func TestIssueRunReconcilerRecoversExactCanonicalSettlement(t *testing.T) {
t.Parallel()
ctx := context.Background()
service, _, _ := newParallelizableDispatchService(t, "ws-settlement-recovery")
detail, err := service.CreateIssueFromPlan(ctx, "ws-settlement-recovery", CreateIssueManagerIssueFromPlanInput{
Issue: CreateIssueManagerIssueInput{
IssueID: "issue-settlement-recovery",
TopicID: workspaceissues.DefaultTopicID,
Title: "Settlement recovery",
PlanningSource: string(workspaceissues.PlanningSourceTraditionalPlan),
SequentialExecution: true,
},
Tasks: []CreateIssueManagerTaskItemInput{{
TaskID: "task-1", Title: "Only", AgentTargetID: agenttargetbiz.IDLocalCodex,
}},
})
if err != nil {
t.Fatal(err)
}
runID := detail.Tasks[0].LatestRunID
coordinator := IssueExecutionCoordinator{
Issues: &service,
SettlementReader: issueRunSettlementReaderStub{settlementByRunID: map[string]IssueRunSettlement{
runID: {
TurnID: "turn-recovered",
Status: workspaceissues.StatusCompleted,
},
}},
}
result, err := coordinator.ReconcileRunningRuns(ctx, "ws-settlement-recovery")
if err != nil {
t.Fatalf("ReconcileRunningRuns() error = %v", err)
}
if result.CompletedCount != 1 {
t.Fatalf("completed count = %d, want 1", result.CompletedCount)
}
updated, err := service.GetIssueDetail(ctx, "ws-settlement-recovery", detail.Issue.IssueID)
if err != nil {
t.Fatal(err)
}
if updated.Tasks[0].Status != workspaceissues.StatusPendingAcceptance {
t.Fatalf("task status = %q, want pending_acceptance", updated.Tasks[0].Status)
}
}