package api
import (
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
func agentStep(name string) planStepIn {
return planStepIn{Op: "agent.new", Input: json.RawMessage(fmt.Sprintf(`{"name":%q}`, name))}
}
// A plan is one request: approved once, its steps run in order as the
// person, each with its own outcome.
func TestAPlanRunsItsStepsInOrderOnOneApproval(t *testing.T) {
r, _, _, _, _ := signOffRig(t)
out, err := r.Invoke(asUser("fbot"), planOp, planIn{Title: "Set up two testers", Reason: "QA",
Steps: []planStepIn{agentStep("alpha"), agentStep("beta")}})
if err != nil {
t.Fatalf("FAIL: asking: %v", err)
}
asked := out.(planOut)
if asked.SignOff.State != "pending" || asked.SignOff.Owner != "dmestas" || len(asked.Steps) != 2 {
t.Fatalf("FAIL: the request: %+v", asked)
}
if asked.Steps[0].State != "" || !strings.Contains(asked.Steps[1].Sentence, "beta") {
t.Errorf("FAIL: a waiting plan's steps: %+v", asked.Steps)
}
if _, err := r.Invoke(asUser("dmestas"), "signoff.decide", signOffDecideIn{ID: asked.SignOff.ID, Approve: true}); err != nil {
t.Fatalf("FAIL: approving: %v", err)
}
got, err := r.Invoke(asUser("fbot"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID})
if err != nil {
t.Fatal(err)
}
g := got.(signOffGetOut)
if g.Request.State != "approved" || len(g.Steps) != 2 || g.Steps[0].State != "done" || g.Steps[1].State != "done" {
t.Errorf("FAIL: after approval: state=%s steps=%+v", g.Request.State, g.Steps)
}
if !strings.Contains(string(g.Result), "alpha") || !strings.Contains(string(g.Result), "beta") {
t.Errorf("FAIL: the steps' answers: %s", g.Result)
}
}
// The first step that fails stops the plan: earlier steps stand, later
// ones do not run, and the request says which step it stopped at.
func TestAPlanStopsAtTheFirstStepThatFails(t *testing.T) {
r, _, _, _, _ := signOffRig(t)
out, err := r.Invoke(asUser("fbot"), planOp, planIn{Title: "Three", Steps: []planStepIn{
agentStep("alpha"), agentStep("alpha"), agentStep("gamma")}})
if err != nil {
t.Fatalf("FAIL: asking: %v", err)
}
id := out.(planOut).SignOff.ID
if _, err := r.Invoke(asUser("dmestas"), "signoff.decide", signOffDecideIn{ID: id, Approve: true}); err == nil {
t.Fatal("FAIL: a plan with a failing step approved cleanly")
}
got, err := r.Invoke(asUser("fbot"), "signoff.get", signOffGetIn{ID: id})
if err != nil {
t.Fatal(err)
}
g := got.(signOffGetOut)
if g.Request.State != "failed" || !strings.Contains(g.Request.Err, "step 2") {
t.Errorf("FAIL: the request: state=%s err=%q", g.Request.State, g.Request.Err)
}
want := []string{"done", "failed", "not run"}
for i, st := range g.Steps {
if st.State != want[i] {
t.Errorf("FAIL: step %d is %q, want %q", i+1, st.State, want[i])
}
}
if strings.Contains(string(g.Result), "gamma") {
t.Errorf("FAIL: a step after the failure ran: %s", g.Result)
}
}
// A plan is refused whole, naming the step, when any step could not be
// asked for on its own.
func TestAPlanIsRefusedWholeNamingTheStep(t *testing.T) {
r, _, _, _, _ := signOffRig(t)
tooMany := make([]planStepIn, maxPlanSteps+1)
for i := range tooMany {
tooMany[i] = agentStep(fmt.Sprintf("a%d", i))
}
for name, c := range map[string]struct {
steps []planStepIn
step string
}{
"unknown op": {[]planStepIn{agentStep("a"), {Op: "no.such"}}, "step 2"},
"not askable": {[]planStepIn{{Op: "signoff.decide", Input: json.RawMessage(`{"id":"x"}`)}}, "step 1"},
"bad input": {[]planStepIn{agentStep("a"), {Op: "agent.new", Input: json.RawMessage(`{"nope":1}`)}}, "step 2"},
"a read": {[]planStepIn{{Op: "signoff.list"}}, "step 1"},
"no steps": {nil, ""},
"too many": {tooMany, ""},
"a nested plan": {[]planStepIn{{Op: planOp, Input: json.RawMessage(`{"title":"x"}`)}}, "step 1"},
} {
_, err := r.Invoke(asUser("fbot"), planOp, planIn{Title: "t", Steps: c.steps})
if err == nil || !strings.Contains(err.Error(), c.step) {
t.Errorf("FAIL: %s: %v", name, err)
}
}
if _, err := r.Invoke(asUser("dmestas"), planOp, planIn{Title: "t", Steps: []planStepIn{agentStep("a")}}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a person asked for a plan: %v", err)
}
}