206 lines
5.1 KiB
Go
206 lines
5.1 KiB
Go
package harness
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Event struct {
|
|
Step int `json:"step"`
|
|
Type string `json:"type"`
|
|
OK bool `json:"ok"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Artifact string `json:"artifact_dir,omitempty"`
|
|
}
|
|
|
|
type RunResult struct {
|
|
OK bool `json:"ok"`
|
|
Steps int `json:"steps"`
|
|
Artifact string `json:"artifact_dir,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Events []Event `json:"-"`
|
|
}
|
|
|
|
func RunScenario(opts Options, emit func(Event)) (RunResult, error) {
|
|
sc := opts.Scenario
|
|
s, err := NewCLI(opts)
|
|
if err != nil {
|
|
return RunResult{OK: false, Error: err.Error()}, err
|
|
}
|
|
defer s.Close()
|
|
results := map[string]json.RawMessage{}
|
|
for i, step := range sc.Steps {
|
|
ev := Event{Step: i + 1, Type: step.Type}
|
|
err := runStep(s, step, results)
|
|
if err != nil {
|
|
ev.OK = false
|
|
ev.Error = err.Error()
|
|
art, aerr := s.DumpArtifacts(sc, i, err)
|
|
if aerr == nil {
|
|
ev.Artifact = art.Dir
|
|
}
|
|
if emit != nil {
|
|
emit(ev)
|
|
}
|
|
return RunResult{OK: false, Steps: i + 1, Error: err.Error(), Artifact: ev.Artifact}, err
|
|
}
|
|
ev.OK = true
|
|
if step.SaveAs != "" {
|
|
ev.Result = results[step.SaveAs]
|
|
}
|
|
if emit != nil {
|
|
emit(ev)
|
|
}
|
|
}
|
|
return RunResult{OK: true, Steps: len(sc.Steps)}, nil
|
|
}
|
|
|
|
func runStep(s *Session, step Step, results map[string]json.RawMessage) error {
|
|
switch step.Type {
|
|
case "wait_stable":
|
|
return s.WaitForStable(timeoutMS(step.TimeoutMS))
|
|
case "send_chord":
|
|
return s.SendChord(step.Chord)
|
|
case "send_text":
|
|
return s.SendText(step.Text)
|
|
case "assert_contains":
|
|
screen, err := s.Screen()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.Contains(screen, step.Contains) {
|
|
return fmt.Errorf("screen does not contain %q:\n%s", step.Contains, screen)
|
|
}
|
|
return nil
|
|
case "assert_regex":
|
|
return s.WaitForRegex(step.Regex, timeoutMS(step.TimeoutMS))
|
|
case "wait_text":
|
|
return s.WaitForText(step.Contains, timeoutMS(step.TimeoutMS))
|
|
case "assert_cursor":
|
|
cur, err := s.Cursor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if step.CursorRow != nil && int(cur.Row) != *step.CursorRow {
|
|
return fmt.Errorf("cursor row = %d, want %d", cur.Row, *step.CursorRow)
|
|
}
|
|
if step.CursorCol != nil && int(cur.Col) != *step.CursorCol {
|
|
return fmt.Errorf("cursor col = %d, want %d", cur.Col, *step.CursorCol)
|
|
}
|
|
return nil
|
|
case "mcp_call":
|
|
params, err := resolveParams(step.Params, results)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := s.MCPCall(step.Method, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if step.SaveAs != "" {
|
|
results[step.SaveAs] = raw
|
|
}
|
|
return nil
|
|
case "assert_mcp":
|
|
params, perr := resolveParams(step.Params, results)
|
|
if perr != nil {
|
|
return perr
|
|
}
|
|
raw, err := s.MCPCall(step.Method, params)
|
|
if step.ErrorKind != "" {
|
|
if err == nil {
|
|
return fmt.Errorf("expected MCP error kind %q, got success %s", step.ErrorKind, string(raw))
|
|
}
|
|
rpcErr, ok := err.(*RPCError)
|
|
if !ok {
|
|
return err
|
|
}
|
|
if rpcErr.Data != nil {
|
|
if data, ok := rpcErr.Data.(map[string]any); ok && data["kind"] == step.ErrorKind {
|
|
return nil
|
|
}
|
|
}
|
|
if strings.Contains(rpcErr.Message, step.ErrorKind) || strings.Contains(rpcErr.Message, strings.ReplaceAll(step.ErrorKind, "_", " ")) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("MCP error = %#v, want kind %q", rpcErr, step.ErrorKind)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertJSONValue(raw, step.Path, step.Equals, step.Contains, step.AllowSubstring)
|
|
case "assert_saved":
|
|
raw, ok := results[step.From]
|
|
if !ok {
|
|
return fmt.Errorf("no saved result %q", step.From)
|
|
}
|
|
return assertJSONValue(raw, step.Path, step.Equals, step.Contains, step.AllowSubstring)
|
|
}
|
|
return fmt.Errorf("unknown step type %q", step.Type)
|
|
}
|
|
|
|
func resolveParams(raw json.RawMessage, saved map[string]json.RawMessage) (json.RawMessage, error) {
|
|
if len(raw) == 0 {
|
|
return raw, nil
|
|
}
|
|
var v any
|
|
if err := json.Unmarshal(raw, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
resolved, err := resolveValue(v, saved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(resolved)
|
|
}
|
|
|
|
func resolveValue(v any, saved map[string]json.RawMessage) (any, error) {
|
|
switch x := v.(type) {
|
|
case string:
|
|
if strings.HasPrefix(x, "{{") && strings.HasSuffix(x, "}}") {
|
|
ref := strings.TrimSuffix(strings.TrimPrefix(x, "{{"), "}}")
|
|
parts := strings.SplitN(ref, ".", 2)
|
|
raw, ok := saved[parts[0]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown saved result %q", parts[0])
|
|
}
|
|
var sv any
|
|
if err := json.Unmarshal(raw, &sv); err != nil {
|
|
return nil, err
|
|
}
|
|
path := ""
|
|
if len(parts) == 2 {
|
|
path = parts[1]
|
|
}
|
|
got, ok := valueAt(sv, path)
|
|
if !ok {
|
|
return nil, fmt.Errorf("path %q not found in saved result %q", path, parts[0])
|
|
}
|
|
return got, nil
|
|
}
|
|
return x, nil
|
|
case []any:
|
|
for i := range x {
|
|
r, err := resolveValue(x[i], saved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
x[i] = r
|
|
}
|
|
return x, nil
|
|
case map[string]any:
|
|
for k := range x {
|
|
r, err := resolveValue(x[k], saved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
x[k] = r
|
|
}
|
|
return x, nil
|
|
default:
|
|
return x, nil
|
|
}
|
|
}
|