20308a2d16
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
197 lines
7.2 KiB
Go
197 lines
7.2 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func eligibleSignals() RunnerMutabilitySignals {
|
|
return RunnerMutabilitySignals{RepositoryCount: 1}
|
|
}
|
|
|
|
func TestEvaluateRunnerMutability_Eligible(t *testing.T) {
|
|
verdict := EvaluateRunnerMutability(eligibleSignals())
|
|
if !verdict.Editable || verdict.Reason != RunnerReasonEligible {
|
|
t.Fatalf("verdict = %+v, want editable=true reason=%s", verdict, RunnerReasonEligible)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateRunnerMutability_ConditionOrder(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*RunnerMutabilitySignals)
|
|
wantErr string
|
|
}{
|
|
{"archived", func(s *RunnerMutabilitySignals) { s.Archived = true }, RunnerReasonTaskArchived},
|
|
{"no repository", func(s *RunnerMutabilitySignals) { s.RepositoryCount = 0 }, RunnerReasonNoRepository},
|
|
{"multiple repositories", func(s *RunnerMutabilitySignals) { s.RepositoryCount = 2 }, RunnerReasonMultipleRepositories},
|
|
{"session exists", func(s *RunnerMutabilitySignals) { s.HasSession = true }, RunnerReasonSessionExists},
|
|
{"environment exists", func(s *RunnerMutabilitySignals) { s.HasEnvironment = true }, RunnerReasonEnvironmentExists},
|
|
{"executor running", func(s *RunnerMutabilitySignals) { s.HasExecutorRunning = true }, RunnerReasonExecutorRunning},
|
|
{"workspace folder attached", func(s *RunnerMutabilitySignals) { s.HasWorkspaceFolder = true }, RunnerReasonWorkspaceFolderAttached},
|
|
{"workspace path set", func(s *RunnerMutabilitySignals) { s.WorkspacePath = "/host/path" }, RunnerReasonWorkspacePathSet},
|
|
{"workspace group member", func(s *RunnerMutabilitySignals) { s.HasActiveGroupMembership = true }, RunnerReasonWorkspaceGroupMember},
|
|
{"binding not independent (shared group, no parent)", func(s *RunnerMutabilitySignals) { s.WorkspaceMode = "shared_group" }, RunnerReasonWorkspaceBindingNotIndependent},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
signals := eligibleSignals()
|
|
tt.mutate(&signals)
|
|
verdict := EvaluateRunnerMutability(signals)
|
|
if verdict.Editable {
|
|
t.Fatalf("Editable = true, want false for %s", tt.name)
|
|
}
|
|
if verdict.Reason != tt.wantErr {
|
|
t.Fatalf("Reason = %s, want %s", verdict.Reason, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEvaluateRunnerMutability_FirstFailingConditionWins proves the ten
|
|
// conditions are evaluated in the fixed AC-TASKS-RUNNER-SWITCH-001.3 order:
|
|
// a task tripping several conditions always reports the earliest one.
|
|
func TestEvaluateRunnerMutability_FirstFailingConditionWins(t *testing.T) {
|
|
signals := RunnerMutabilitySignals{
|
|
Archived: true,
|
|
RepositoryCount: 0,
|
|
HasSession: true,
|
|
HasEnvironment: true,
|
|
HasExecutorRunning: true,
|
|
HasWorkspaceFolder: true,
|
|
WorkspacePath: "/host/path",
|
|
HasActiveGroupMembership: true,
|
|
WorkspaceMode: "shared_group",
|
|
}
|
|
verdict := EvaluateRunnerMutability(signals)
|
|
if verdict.Reason != RunnerReasonTaskArchived {
|
|
t.Fatalf("Reason = %s, want %s (archived must win over every later condition)", verdict.Reason, RunnerReasonTaskArchived)
|
|
}
|
|
|
|
signals.Archived = false
|
|
verdict = EvaluateRunnerMutability(signals)
|
|
if verdict.Reason != RunnerReasonNoRepository {
|
|
t.Fatalf("Reason = %s, want %s", verdict.Reason, RunnerReasonNoRepository)
|
|
}
|
|
}
|
|
|
|
// TestEvaluateRunnerMutability_WorkspaceBindingIndependence covers every
|
|
// combination of HasParent x WorkspaceMode against AC-TASKS-RUNNER-SWITCH-001.3
|
|
// condition 10: a task with no parent is independent unless it declares
|
|
// shared-group mode; a task with a parent is independent only when it
|
|
// declares new_workspace explicitly.
|
|
func TestEvaluateRunnerMutability_WorkspaceBindingIndependence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hasParent bool
|
|
mode string
|
|
wantIndependent bool
|
|
}{
|
|
{"no parent, no mode", false, "", true},
|
|
{"no parent, inherit_parent", false, "inherit_parent", true},
|
|
{"no parent, new_workspace", false, "new_workspace", true},
|
|
{"no parent, shared_group", false, "shared_group", false},
|
|
{"parent, no mode", true, "", false},
|
|
{"parent, inherit_parent", true, "inherit_parent", false},
|
|
{"parent, shared_group", true, "shared_group", false},
|
|
{"parent, new_workspace", true, "new_workspace", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
signals := eligibleSignals()
|
|
signals.HasParent = tt.hasParent
|
|
signals.WorkspaceMode = tt.mode
|
|
verdict := EvaluateRunnerMutability(signals)
|
|
gotIndependent := verdict.Editable
|
|
if gotIndependent != tt.wantIndependent {
|
|
t.Fatalf("Editable = %v, want %v (independent=%v)", verdict.Editable, tt.wantIndependent, tt.wantIndependent)
|
|
}
|
|
if !tt.wantIndependent && verdict.Reason != RunnerReasonWorkspaceBindingNotIndependent {
|
|
t.Fatalf("Reason = %s, want %s", verdict.Reason, RunnerReasonWorkspaceBindingNotIndependent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEvaluateRunnerMutability_WorkspacePathBoundary pins the Build-time
|
|
// decision for F23 (spec left this boundary open): a stored workspace path
|
|
// that is empty or whitespace-only is treated as NOT set, the same "blank"
|
|
// test AC-TASKS-RUNNER-SWITCH-002.8 applies to payload identifiers. A
|
|
// permanently-unrecoverable false-immutable (whitespace trips the gate) is a
|
|
// worse product failure than a false-editable (whitespace does not trip it),
|
|
// since nothing has materialized yet and an editable task risks nothing but
|
|
// an extra no-op switch attempt.
|
|
func TestEvaluateRunnerMutability_WorkspacePathBoundary(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want bool // editable
|
|
}{
|
|
{"empty", "", true},
|
|
{"whitespace only", " \t\n", true},
|
|
{"non-empty", "/host/path", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
signals := eligibleSignals()
|
|
signals.WorkspacePath = tt.path
|
|
verdict := EvaluateRunnerMutability(signals)
|
|
if verdict.Editable != tt.want {
|
|
t.Fatalf("Editable = %v, want %v", verdict.Editable, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunnerSignalsFromTask(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
|
|
t.Run("archived", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{ArchivedAt: &now})
|
|
if !s.Archived {
|
|
t.Fatalf("Archived = false, want true")
|
|
}
|
|
})
|
|
|
|
t.Run("not archived", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{})
|
|
if s.Archived {
|
|
t.Fatalf("Archived = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("workspace path from metadata", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{Metadata: map[string]interface{}{
|
|
MetaKeyWorkspacePath: "/host/path",
|
|
}})
|
|
if s.WorkspacePath != "/host/path" {
|
|
t.Fatalf("WorkspacePath = %q, want /host/path", s.WorkspacePath)
|
|
}
|
|
})
|
|
|
|
t.Run("has parent", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{ParentID: "parent-1"})
|
|
if !s.HasParent {
|
|
t.Fatalf("HasParent = false, want true")
|
|
}
|
|
})
|
|
|
|
t.Run("workspace mode from nested metadata block", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{Metadata: map[string]interface{}{
|
|
"workspace": map[string]interface{}{"mode": "new_workspace"},
|
|
}})
|
|
if s.WorkspaceMode != "new_workspace" {
|
|
t.Fatalf("WorkspaceMode = %q, want new_workspace", s.WorkspaceMode)
|
|
}
|
|
})
|
|
|
|
t.Run("no metadata block", func(t *testing.T) {
|
|
s := RunnerSignalsFromTask(&Task{})
|
|
if s.WorkspaceMode != "" || s.WorkspacePath != "" {
|
|
t.Fatalf("expected empty mode/path, got %+v", s)
|
|
}
|
|
})
|
|
}
|