Go · 6412 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package main
4
5 import (
6 "bytes"
7 "encoding/json"
8 "reflect"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/jackc/pgx/v5/pgtype"
14
15 actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc"
16 )
17
18 func TestParseRunnerLabels(t *testing.T) {
19 tests := []struct {
20 name string
21 raw string
22 want []string
23 }{
24 {name: "empty", raw: "", want: []string{}},
25 {name: "trim and dedupe", raw: " self-hosted, linux,linux,ubuntu-24.04 ", want: []string{"self-hosted", "linux", "ubuntu-24.04"}},
26 {name: "underscore dot dash", raw: "gpu_cuda-12.4", want: []string{"gpu_cuda-12.4"}},
27 }
28 for _, tt := range tests {
29 t.Run(tt.name, func(t *testing.T) {
30 got, err := parseRunnerLabels(tt.raw)
31 if err != nil {
32 t.Fatalf("parseRunnerLabels: %v", err)
33 }
34 if !reflect.DeepEqual(got, tt.want) {
35 t.Fatalf("labels: got %#v, want %#v", got, tt.want)
36 }
37 })
38 }
39 }
40
41 func TestParseRunnerLabelsRejectsInvalid(t *testing.T) {
42 tests := []string{
43 "linux,",
44 "linux,,x64",
45 "has space",
46 "semi;colon",
47 }
48 for _, raw := range tests {
49 t.Run(raw, func(t *testing.T) {
50 if _, err := parseRunnerLabels(raw); err == nil {
51 t.Fatal("parseRunnerLabels returned nil error")
52 }
53 })
54 }
55 }
56
57 func TestWriteRunnerRegisterOutputJSON(t *testing.T) {
58 expiresAt := time.Date(2026, 5, 12, 16, 30, 0, 0, time.UTC)
59 var buf bytes.Buffer
60 err := writeRunnerRegisterOutput(&buf, "json", runnerRegisterOutput{
61 ID: 7,
62 Name: "runner-7",
63 Labels: []string{"self-hosted", "linux", "ubuntu-latest", "x64"},
64 Capacity: 2,
65 Token: "shithub_runner_token",
66 TokenExpiresAt: &expiresAt,
67 })
68 if err != nil {
69 t.Fatalf("writeRunnerRegisterOutput: %v", err)
70 }
71 if strings.Contains(buf.String(), "Store this token") {
72 t.Fatalf("json output included human prose: %s", buf.String())
73 }
74 var got struct {
75 ID int64 `json:"id"`
76 Name string `json:"name"`
77 Labels []string `json:"labels"`
78 Capacity int32 `json:"capacity"`
79 Token string `json:"token"`
80 TokenExpiresAt time.Time `json:"token_expires_at"`
81 }
82 if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
83 t.Fatalf("json.Unmarshal: %v", err)
84 }
85 if got.ID != 7 || got.Name != "runner-7" || got.Capacity != 2 || got.Token != "shithub_runner_token" {
86 t.Fatalf("unexpected output: %+v", got)
87 }
88 if !reflect.DeepEqual(got.Labels, []string{"self-hosted", "linux", "ubuntu-latest", "x64"}) {
89 t.Fatalf("labels: got %#v", got.Labels)
90 }
91 if !got.TokenExpiresAt.Equal(expiresAt) {
92 t.Fatalf("expires_at: got %s want %s", got.TokenExpiresAt, expiresAt)
93 }
94 }
95
96 func TestWriteRunnerRegisterOutputText(t *testing.T) {
97 var buf bytes.Buffer
98 err := writeRunnerRegisterOutput(&buf, "text", runnerRegisterOutput{
99 ID: 8,
100 Name: "runner-8",
101 Labels: []string{"self-hosted", "linux", "ubuntu-latest", "x64"},
102 Capacity: 1,
103 Token: "token-once",
104 })
105 if err != nil {
106 t.Fatalf("writeRunnerRegisterOutput: %v", err)
107 }
108 body := buf.String()
109 for _, want := range []string{
110 "runner registered",
111 "labels: self-hosted,linux,ubuntu-latest,x64",
112 "token_expires_at: never",
113 "token: token-once",
114 "Store this token now",
115 } {
116 if !strings.Contains(body, want) {
117 t.Fatalf("text output missing %q in %s", want, body)
118 }
119 }
120 }
121
122 func TestWriteRunnerQueueOutputJSON(t *testing.T) {
123 now := time.Date(2026, 5, 12, 16, 30, 0, 0, time.UTC)
124 rows := []actionsdb.ListQueuedWorkflowJobRunsOnRow{{
125 RunsOn: "windows-latest",
126 QueuedJobs: 3,
127 MatchingRunnerCount: 0,
128 OldestQueuedAt: pgtype.Timestamptz{Time: now.Add(-90 * time.Second), Valid: true},
129 }}
130 var buf bytes.Buffer
131 if err := writeRunnerQueueOutput(&buf, "json", rows, now); err != nil {
132 t.Fatalf("writeRunnerQueueOutput: %v", err)
133 }
134 var got []runnerQueueOutputRow
135 if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
136 t.Fatalf("json.Unmarshal: %v", err)
137 }
138 if len(got) != 1 {
139 t.Fatalf("rows=%d body=%s", len(got), buf.String())
140 }
141 if got[0].RunsOn != "windows-latest" || got[0].QueuedJobs != 3 || got[0].MatchingRunnerCount != 0 {
142 t.Fatalf("unexpected row: %+v", got[0])
143 }
144 if got[0].OldestQueuedSeconds != 90 {
145 t.Fatalf("oldest seconds=%d", got[0].OldestQueuedSeconds)
146 }
147 }
148
149 func TestWriteRunnerListOutputJSONIncludesOpsFields(t *testing.T) {
150 now := time.Date(2026, 5, 12, 16, 30, 0, 0, time.UTC)
151 rows := []actionsdb.ListRunnersRow{{
152 ID: 7,
153 Name: "runner-a",
154 Labels: []string{"self-hosted", "linux"},
155 Capacity: 2,
156 Status: actionsdb.WorkflowRunnerStatusBusy,
157 LastHeartbeatAt: pgtype.Timestamptz{Time: now.Add(-75 * time.Second), Valid: true},
158 HostName: "host-a",
159 Version: "dev-test",
160 DrainingAt: pgtype.Timestamptz{Time: now.Add(-30 * time.Second), Valid: true},
161 DrainReason: "maintenance",
162 ActiveJobCount: 1,
163 }}
164 var buf bytes.Buffer
165 if err := writeRunnerListOutput(&buf, "json", rows, now); err != nil {
166 t.Fatalf("writeRunnerListOutput: %v", err)
167 }
168 var got []runnerListOutputRow
169 if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
170 t.Fatalf("json.Unmarshal: %v", err)
171 }
172 if len(got) != 1 {
173 t.Fatalf("rows=%d body=%s", len(got), buf.String())
174 }
175 if got[0].HostName != "host-a" || got[0].Version != "dev-test" ||
176 got[0].ActiveJobCount != 1 || got[0].LastHeartbeatAgeSeconds != 75 ||
177 got[0].DrainingAt == "" || got[0].DrainReason != "maintenance" {
178 t.Fatalf("unexpected row: %+v", got[0])
179 }
180 }
181
182 func TestWriteRunnerStateOutputText(t *testing.T) {
183 var buf bytes.Buffer
184 if err := writeRunnerStateOutput(&buf, "text", "runner draining", runnerStateOutput{
185 ID: 7,
186 Name: "runner-a",
187 Status: "busy",
188 DrainingAt: "2026-05-12T16:30:00Z",
189 DrainReason: "maintenance",
190 }); err != nil {
191 t.Fatalf("writeRunnerStateOutput: %v", err)
192 }
193 body := buf.String()
194 for _, want := range []string{
195 "runner draining",
196 "id: 7",
197 "name: runner-a",
198 "drain_reason: maintenance",
199 } {
200 if !strings.Contains(body, want) {
201 t.Fatalf("text output missing %q in %s", want, body)
202 }
203 }
204 }
205
206 func TestParseRunnerIDRejectsInvalid(t *testing.T) {
207 for _, raw := range []string{"", "0", "-1", "abc"} {
208 t.Run(raw, func(t *testing.T) {
209 if _, err := parseRunnerID("admin runner test", raw); err == nil {
210 t.Fatal("parseRunnerID returned nil error")
211 }
212 })
213 }
214 }
215