package main import ( "encoding/json" "reflect" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/link" ) // The queue answers as data as well as text (novox/hq issue 455): each ask's repository, commit, kind, // machine, since and position among the waiting, read from the same queue the table is written from. const aCommit = "0123456789abcdef0123456789abcdef01234567" func aQueueOfEachKind(now time.Time) link.Queue { asked := now.Add(-5 * time.Minute) started := now.Add(-2 * time.Minute) return link.Queue{Seat: link.TheBuildMachine, Delivered: 2, AckPending: 1, MaxDeliver: 5, Asks: []link.QueuedAsk{ {Seq: 1, ID: "build-dead", Repository: "ssh://forge/novox/x", AskedAt: asked, State: link.AskDead}, {Seq: 2, ID: "build-running", Repository: "ssh://forge/novox/mesh-host", Ref: aCommit, AskedAt: asked, State: link.AskInFlight, On: "ace", Started: started, Request: link.BuildRequest{ID: "build-running", Check: &link.CheckRequest{Owner: "novox", Repo: "mesh-host"}}}, {Seq: 3, ID: "build-first", Repository: "ssh://forge/novox/mesh-catalog", Ref: "main", AskedAt: asked, State: link.AskWaiting}, {Seq: 4, ID: "build-second", Repository: "ssh://forge/novox/mesh-catalog", Path: "modules/gitea", Ref: aCommit, AskedAt: now.Add(-time.Minute), State: link.AskWaiting, Request: link.BuildRequest{ID: "build-second", Check: &link.CheckRequest{Owner: "novox", Repo: "mesh-catalog"}}}, }} } func TestTheQueueVerbAnswersEachAskAsData(t *testing.T) { now := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC) q := aQueueOfEachKind(now) got := queueAnswerOf(q, now) if got.Waiting != 2 || got.InFlight != 1 || got.Dead != 1 || got.Seat != link.TheBuildMachine { t.Fatalf("counts: %+v", got) } if got.Text != queueText(q, now) { t.Fatalf("the data's text is not the table read from the same queue:\n%s", got.Text) } byID := map[string]queuedAnswer{} for _, a := range got.Asks { byID[a.ID] = a } running := byID["build-running"] if running.Kind != "check" || running.Machine != "ace" || running.Commit != aCommit || running.Position != 0 || !running.Since.Equal(now.Add(-2*time.Minute)) || running.State != link.AskInFlight { t.Errorf("in flight: %+v", running) } first, second := byID["build-first"], byID["build-second"] if first.Position != 1 || first.Kind != "build" || first.Commit != "" || first.Ref != "main" || first.Machine != "" || !first.Since.Equal(now.Add(-5*time.Minute)) { t.Errorf("first waiting: %+v", first) } if second.Position != 2 || second.Kind != "check" || second.Commit != aCommit || second.Path != "modules/gitea" || second.Repository != "ssh://forge/novox/mesh-catalog" { t.Errorf("second waiting: %+v", second) } if dead := byID["build-dead"]; dead.Position != 0 || dead.State != link.AskDead { t.Errorf("dead: %+v", dead) } // In the table's order: in flight, then the waiting by position, then the dead. var order []string for _, a := range got.Asks { order = append(order, a.ID) } if want := []string{"build-running", "build-first", "build-second", "build-dead"}; !reflect.DeepEqual(order, want) { t.Errorf("order %v, want %v", order, want) } } // The verb asks its command for JSON, and its answer carries the data while its output stays the table a // person reads, so a text caller reads what it always did. func TestTheQueueVerbKeepsTheTableAndCarriesTheData(t *testing.T) { argv, err := argvFor("queue", nil) if err != nil || !reflect.DeepEqual(argv, []string{"queue", "--json"}) { t.Fatalf("queue's command line: %v %v", argv, err) } now := time.Now() q := aQueueOfEachKind(now) body, err := json.Marshal(queueAnswerOf(q, now)) if err != nil { t.Fatal(err) } answer := answerOf(argv, body, "", true) if answer.Output != queueText(q, now) { t.Fatalf("the output is not the table:\n%s", answer.Output) } data, ok := answer.Answer.(map[string]any) if !ok { t.Fatalf("no data: %#v", answer.Answer) } if _, doubled := data["text"]; doubled { t.Error("the table is said twice: once as the output and again inside the data") } asks, _ := data["asks"].([]any) if len(asks) != 4 || data["waiting"] != float64(2) { t.Fatalf("data: %v", data) } second := asks[2].(map[string]any) if second["position"] != float64(2) || second["kind"] != "check" || second["commit"] != aCommit { t.Errorf("second waiting: %v", second) } for _, a := range asks { if _, held := a.(map[string]any)["held"]; held { t.Error("an ask carries what the mesh holds") } } // A failure is said as text, as before. failed := answerOf(argv, nil, "mesh-controller: the bus cannot be reached\n", false) if failed.Answer != nil || !strings.Contains(failed.Output, "cannot be reached") { t.Errorf("a failure: %+v", failed) } } // A repository named with a password never shows it, in the table or the data. func TestTheQueueNeverShowsARepositorysPassword(t *testing.T) { if got := link.WithoutPassword("https://agent:s3cret@forge.example/novox/x.git"); strings.Contains(got, "s3cret") || !strings.Contains(got, "agent@forge.example/novox/x.git") { t.Errorf("redacted: %s", got) } now := time.Now() q := link.Queue{Seat: link.TheBuildMachine, Asks: []link.QueuedAsk{{Seq: 1, ID: "build-1", Repository: "https://agent:s3cret@forge.example/novox/x.git", State: link.AskWaiting}}} body, _ := json.Marshal(queueAnswerOf(q, now)) if strings.Contains(string(body), "s3cret") { t.Errorf("the queue said a password: %s", body) } for _, kept := range []string{"ssh://git@forge:222/novox/x.git", "novox/x", "git@forge:novox/x.git", ""} { if got := link.WithoutPassword(kept); got != kept { t.Errorf("%q became %q", kept, got) } } }