package executor
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"testing"
"tangled.org/core/spindle/engine"
millv1 "tangled.org/core/spindle/mill/proto/gen"
"tangled.org/core/spindle/models"
"tangled.org/core/workflow"
)
type placementEngine struct {
*fakeEngine
validationErr error
validated bool
}
func (e *placementEngine) ValidateWorkflowPlacement(*models.Workflow) error {
e.validated = true
return e.validationErr
}
func TestHandleReserveRejectsMissingTriggerMetadata(t *testing.T) {
enc := newCaptureEncoder()
e := &Executor{
l: slog.New(slog.NewTextHandler(io.Discard, nil)),
enc: enc,
seats: 1,
engines: map[string]models.Engine{"microvm": &fakeEngine{}},
active: make(map[string]*reservation),
}
twf, err := json.Marshal(workflow.WorkflowSpec{Name: "build"})
if err != nil {
t.Fatal(err)
}
tpl, err := json.Marshal(workflow.PipelineSpec{})
if err != nil {
t.Fatal(err)
}
// engines deref TriggerMetadata unconditionally. this must be a reject,
// not a panic that takes the whole executor down
e.handleReserve(context.Background(), &millv1.ReserveSeat{
LeaseId: "lease-1",
TargetEngine: "microvm",
RawWorkflowJson: string(twf),
RawPipelineJson: string(tpl),
PipelineId: "r",
})
result := (<-enc.messages).GetReserveResult()
if result == nil {
t.Fatal("handleReserve() did not send ReserveResult")
}
if result.GetAccepted() {
t.Fatal("handleReserve() accepted a pipeline without trigger metadata")
}
if result.GetRejectClass() != millv1.RejectClass_REJECT_CLASS_INCOMPATIBLE {
t.Fatalf("reject class = %v, want incompatible", result.GetRejectClass())
}
if len(e.active) != 0 {
t.Fatalf("active reservations = %d, want 0", len(e.active))
}
}
func TestHandleReserveValidatesPlacementBeforeAcquiringSlot(t *testing.T) {
enc := newCaptureEncoder()
validationErr := errors.New("image architecture is not native")
eng := &placementEngine{fakeEngine: &fakeEngine{}, validationErr: validationErr}
e := &Executor{
l: slog.New(slog.NewTextHandler(io.Discard, nil)),
enc: enc,
seats: 1,
engines: map[string]models.Engine{"microvm": eng},
active: make(map[string]*reservation),
}
twf, err := json.Marshal(workflow.WorkflowSpec{Name: "build"})
if err != nil {
t.Fatal(err)
}
tpl, err := json.Marshal(workflow.PipelineSpec{TriggerMetadata: &workflow.TriggerMetadata{}})
if err != nil {
t.Fatal(err)
}
e.handleReserve(context.Background(), &millv1.ReserveSeat{
LeaseId: "lease-1",
TargetEngine: "microvm",
RawWorkflowJson: string(twf),
RawPipelineJson: string(tpl),
PipelineId: "r",
RepoDid: "did:web:example.com",
})
result := (<-enc.messages).GetReserveResult()
if result == nil {
t.Fatal("handleReserve() did not send ReserveResult")
}
if result.GetAccepted() {
t.Fatal("handleReserve() accepted placement validation failure")
}
if result.GetRejectClass() != millv1.RejectClass_REJECT_CLASS_INCOMPATIBLE {
t.Fatalf("reject class = %v, want incompatible", result.GetRejectClass())
}
if !eng.validated {
t.Fatal("placement validator was not called")
}
if eng.acquireCalled {
t.Fatal("slot acquisition ran after placement validation failed")
}
if len(e.active) != 0 {
t.Fatalf("active reservations = %d, want 0", len(e.active))
}
}
func TestHandleReservePreservesTypedInitFailure(t *testing.T) {
enc := newCaptureEncoder()
eng := &fakeEngine{
initErr: engine.ClassifiedFailure(
engine.FailureClassUser,
engine.FailureReasonWorkflowInvalid,
errors.New("invalid workflow"),
),
}
e := &Executor{
l: slog.New(slog.NewTextHandler(io.Discard, nil)),
enc: enc,
seats: 1,
engines: map[string]models.Engine{"microvm": eng},
active: make(map[string]*reservation),
}
e.handleReserve(context.Background(), testReserveSeat(t, "lease-1", "microvm"))
result := (<-enc.messages).GetReserveResult()
if result == nil || result.GetAccepted() {
t.Fatalf("reserve result = %+v, want rejection", result)
}
if result.GetFailureClass() != string(engine.FailureClassUser) ||
result.GetFailureReason() != string(engine.FailureReasonWorkflowInvalid) {
t.Fatalf(
"failure attribution = %q/%q, want user/workflow_invalid",
result.GetFailureClass(),
result.GetFailureReason(),
)
}
}
func TestHandleReserveKeepsTriggerMetadataForExecution(t *testing.T) {
e := testExecutor(t)
e.enc = newCaptureEncoder()
e.seats = 1
e.engines = map[string]models.Engine{"microvm": &fakeEngine{}}
repoDID := "did:web:example.com"
metadata := &workflow.TriggerMetadata{
Kind: "push",
Push: &workflow.PushTriggerData{
NewSha: "0123456789abcdef",
Ref: "refs/heads/main",
},
Repo: &workflow.TriggerRepo{RepoDid: &repoDID},
}
twf, err := json.Marshal(workflow.WorkflowSpec{Name: "build"})
if err != nil {
t.Fatal(err)
}
tpl, err := json.Marshal(workflow.PipelineSpec{TriggerMetadata: metadata})
if err != nil {
t.Fatal(err)
}
e.handleReserve(context.Background(), &millv1.ReserveSeat{
LeaseId: "lease-1",
TargetEngine: "microvm",
RawWorkflowJson: string(twf),
RawPipelineJson: string(tpl),
PipelineId: "r",
RepoDid: repoDID,
})
result := (<-e.enc.(*captureEncoder).messages).GetReserveResult()
if result == nil || !result.GetAccepted() {
t.Fatalf("ReserveResult = %+v, want accepted", result)
}
if got := result.GetRunBudgetSeconds(); got != 7*60 {
t.Fatalf("run budget = %ds, want the engine's 7 minutes", got)
}
res := e.active["lease-1"]
if res == nil || res.pipeline.TriggerMetadata == nil || res.pipeline.TriggerMetadata.Push == nil {
t.Fatal("reservation dropped trigger metadata")
}
if got := res.pipeline.TriggerMetadata.Push.NewSha; got != metadata.Push.NewSha {
t.Fatalf("trigger commit = %q, want %q", got, metadata.Push.NewSha)
}
res.ttlTimer.Stop()
}