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) } } }