- Implement backtest workflow validation for headless operator mode - Add handoff evidence tracking for backtest runs - Update operator client, runner, scenario, and output modules - Add test files for handoff, backtest runner, and error handling - Add test data for backtest scenarios and validation
141 lines
4.2 KiB
Go
141 lines
4.2 KiB
Go
package operator
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// requiredScenarios are the six headless validation scenarios the milestone
|
|
// handoff table requires evidence for. The handoff matrix and the expected
|
|
// output fixtures must cover all of them so the Flutter Operator Console MVP
|
|
// can validate each workflow before building any screen.
|
|
var requiredScenarios = []string{
|
|
"api_connection_smoke",
|
|
"market_data_status_query",
|
|
"backtest_run_request",
|
|
"backtest_run_polling",
|
|
"backtest_result_summary",
|
|
"invalid_request_matrix",
|
|
}
|
|
|
|
func handoffMatrixPath() string {
|
|
return filepath.Join("..", "..", "testdata", "operator", "headless_validation.md")
|
|
}
|
|
|
|
func expectedFixturePath(scenario string) string {
|
|
return filepath.Join("..", "..", "testdata", "operator", "expected", scenario+".jsonl")
|
|
}
|
|
|
|
// TestHeadlessValidationHandoffCoversRequiredScenarios fails if the handoff
|
|
// matrix drops any required scenario, which would leave the Flutter MVP without
|
|
// command/field evidence for that workflow.
|
|
func TestHeadlessValidationHandoffCoversRequiredScenarios(t *testing.T) {
|
|
data, err := os.ReadFile(handoffMatrixPath())
|
|
if err != nil {
|
|
t.Fatalf("read handoff matrix: %v", err)
|
|
}
|
|
doc := string(data)
|
|
for _, s := range requiredScenarios {
|
|
if !strings.Contains(doc, "`"+s+"`") {
|
|
t.Errorf("handoff matrix missing scenario %q", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestExpectedOutputFixturesAreValidJSONLines checks that every expected output
|
|
// fixture parses as JSON lines and that each record carries the machine-readable
|
|
// keys the handoff contract promises: scenario/status on every line, action on
|
|
// step lines, plus at least one step and one summary line.
|
|
func TestExpectedOutputFixturesAreValidJSONLines(t *testing.T) {
|
|
for _, s := range requiredScenarios {
|
|
path := expectedFixturePath(s)
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Errorf("open expected fixture %q: %v", path, err)
|
|
continue
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
lineNo := 0
|
|
sawStep := false
|
|
sawSummary := false
|
|
for scanner.Scan() {
|
|
lineNo++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Errorf("%s:%d not valid JSON: %v", path, lineNo, err)
|
|
continue
|
|
}
|
|
|
|
if rec["scenario"] != s {
|
|
t.Errorf("%s:%d scenario = %v, want %q", path, lineNo, rec["scenario"], s)
|
|
}
|
|
if _, ok := rec["status"]; !ok {
|
|
t.Errorf("%s:%d missing status field", path, lineNo)
|
|
}
|
|
|
|
switch rec["type"] {
|
|
case "step":
|
|
sawStep = true
|
|
if _, ok := rec["action"]; !ok {
|
|
t.Errorf("%s:%d step line missing action field", path, lineNo)
|
|
}
|
|
case "summary":
|
|
sawSummary = true
|
|
default:
|
|
t.Errorf("%s:%d unexpected type %v", path, lineNo, rec["type"])
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
t.Errorf("scan %q: %v", path, err)
|
|
}
|
|
f.Close()
|
|
|
|
if !sawStep {
|
|
t.Errorf("%s has no step line", path)
|
|
}
|
|
if !sawSummary {
|
|
t.Errorf("%s has no summary line", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHandoffDocumentedScenarioFixturesExist guards the handoff matrix against
|
|
// referencing fixture paths that do not exist. Each required scenario must have
|
|
// an input YAML fixture and an expected output fixture on disk, and the matrix
|
|
// must reference both relative paths so the documented commands stay runnable.
|
|
func TestHandoffDocumentedScenarioFixturesExist(t *testing.T) {
|
|
matrix, err := os.ReadFile(handoffMatrixPath())
|
|
if err != nil {
|
|
t.Fatalf("read handoff matrix: %v", err)
|
|
}
|
|
doc := string(matrix)
|
|
|
|
for _, s := range requiredScenarios {
|
|
inputPath := filepath.Join("..", "..", "testdata", "operator", s+".yaml")
|
|
if _, err := os.Stat(inputPath); err != nil {
|
|
t.Errorf("input fixture for %q missing: %v", s, err)
|
|
}
|
|
if _, err := os.Stat(expectedFixturePath(s)); err != nil {
|
|
t.Errorf("expected fixture for %q missing: %v", s, err)
|
|
}
|
|
|
|
inputRel := "testdata/operator/" + s + ".yaml"
|
|
expectedRel := "testdata/operator/expected/" + s + ".jsonl"
|
|
if !strings.Contains(doc, inputRel) {
|
|
t.Errorf("handoff matrix does not reference input path %q", inputRel)
|
|
}
|
|
if !strings.Contains(doc, expectedRel) {
|
|
t.Errorf("handoff matrix does not reference expected path %q", expectedRel)
|
|
}
|
|
}
|
|
}
|