177 lines
5.9 KiB
Go
177 lines
5.9 KiB
Go
package issue_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"superwork-tui/internal/gitea"
|
|
"superwork-tui/internal/issue"
|
|
)
|
|
|
|
// sourceSchemaPath is the canonical state schema shared with the VS Code
|
|
// extension and the spx CLI, relative to this package directory.
|
|
const sourceSchemaPath = "../../../vscode/schemas/state-json.schema.json"
|
|
|
|
func TestStateSchemaCopyInSync(t *testing.T) {
|
|
src, err := os.ReadFile(sourceSchemaPath)
|
|
if err != nil {
|
|
t.Fatalf("read source schema: %v", err)
|
|
}
|
|
local, err := os.ReadFile("state-json.schema.json")
|
|
if err != nil {
|
|
t.Fatalf("read embedded schema copy: %v (run `go generate ./internal/issue`)", err)
|
|
}
|
|
if !bytes.Equal(src, local) {
|
|
t.Fatal("state-json.schema.json is out of date; run `go generate ./internal/issue`")
|
|
}
|
|
}
|
|
|
|
func TestParseStateComment_RecognizesEverySchemaField(t *testing.T) {
|
|
raw, err := os.ReadFile(sourceSchemaPath)
|
|
if err != nil {
|
|
t.Fatalf("read source schema: %v", err)
|
|
}
|
|
var schema struct {
|
|
Properties map[string]json.RawMessage `json:"properties"`
|
|
}
|
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
|
t.Fatalf("parse source schema: %v", err)
|
|
}
|
|
if len(schema.Properties) == 0 {
|
|
t.Fatal("source schema has no properties")
|
|
}
|
|
for field := range schema.Properties {
|
|
body := `{"` + field + `":"x"}`
|
|
if _, ok := issue.ParseStateComment(body); !ok {
|
|
t.Errorf("field %q not recognised as a state field", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseStateComment_AcceptedForms(t *testing.T) {
|
|
cases := map[string]string{
|
|
"hidden": "<!-- spx-state\n{\"column\":\"review\",\"pr\":\"7\"}\n-->",
|
|
"hidden padded": "\n <!-- spx-state\n{\"column\":\"review\",\"pr\":\"7\"}\n--> \n",
|
|
"hidden single line": `<!-- spx-state {"column":"review","pr":"7"} -->`,
|
|
"legacy youtrack": "<!--spx-state-->\n{\"column\":\"review\",\"pr\":\"7\"}",
|
|
"bare json": `{"column":"review","pr":"7"}`,
|
|
"bare json padded": " {\"column\":\"review\",\"pr\":\"7\"}\n",
|
|
"hidden escaped html": "<!-- spx-state\n{\"column\":\"review\",\"pr\":\"7\",\"branch\":\"a\\u003c\\u003e\\u0026b\"}\n-->",
|
|
}
|
|
for name, body := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
state, ok := issue.ParseStateComment(body)
|
|
if !ok {
|
|
t.Fatalf("expected %q to parse as a state comment", body)
|
|
}
|
|
if state["column"] != "review" || state["pr"] != "7" {
|
|
t.Errorf("got %v, want column=review pr=7", state)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseStateComment_Rejected(t *testing.T) {
|
|
cases := map[string]string{
|
|
"empty": "",
|
|
"whitespace": " \n ",
|
|
"plain text": "LGTM, merging",
|
|
"unknown fields only": `{"foo":"bar"}`,
|
|
"array": `[{"column":"todo"}]`,
|
|
"scalar": `"column"`,
|
|
"hidden unknown only": "<!-- spx-state\n{\"foo\":\"bar\"}\n-->",
|
|
"hidden broken json": "<!-- spx-state\n{\"column\":\n-->",
|
|
"hidden unterminated": "<!-- spx-state\n{\"column\":\"todo\"}",
|
|
"other html comment": "<!-- spx:review=1 -->\n{\"column\":\"todo\"}",
|
|
"legacy marker no obj": "<!--spx-state-->\nnot json",
|
|
}
|
|
for name, body := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if state, ok := issue.ParseStateComment(body); ok {
|
|
t.Errorf("expected %q to be rejected, got %v", body, state)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatStateComment_HiddenAndRoundTrips(t *testing.T) {
|
|
in := map[string]any{
|
|
"column": "review",
|
|
"branch": "feat/a-->b<c>&d",
|
|
}
|
|
body, err := issue.FormatStateComment(in)
|
|
if err != nil {
|
|
t.Fatalf("FormatStateComment: %v", err)
|
|
}
|
|
const open, close = "<!-- spx-state\n", "\n-->"
|
|
if !strings.HasPrefix(body, open) || !strings.HasSuffix(body, close) {
|
|
t.Fatalf("body %q is not wrapped as <!-- spx-state\\n...\\n-->", body)
|
|
}
|
|
payload := strings.TrimSuffix(strings.TrimPrefix(body, open), close)
|
|
if strings.ContainsAny(payload, "<>&") {
|
|
t.Errorf("payload %q must escape < > &", payload)
|
|
}
|
|
if strings.Contains(payload, "\n") {
|
|
t.Errorf("payload %q must be compact single-line JSON", payload)
|
|
}
|
|
if strings.Count(body, "-->") != 1 {
|
|
t.Errorf("body %q must contain exactly one -->", body)
|
|
}
|
|
out, ok := issue.ParseStateComment(body)
|
|
if !ok {
|
|
t.Fatalf("formatted body %q did not parse back", body)
|
|
}
|
|
if out["column"] != "review" || out["branch"] != "feat/a-->b<c>&d" {
|
|
t.Errorf("round trip got %v, want %v", out, in)
|
|
}
|
|
}
|
|
|
|
func TestExtractStateJSON_NewestValidWinsAcrossFormats(t *testing.T) {
|
|
comments := []gitea.Comment{
|
|
{Body: `{"column":"todo","branch":"feat/old"}`},
|
|
{Body: "<!-- spx-state\n{\"column\":\"review\",\"branch\":\"feat/new\"}\n-->"},
|
|
{Body: "<!-- spx-state\n{\"foo\":\"bar\"}\n-->"},
|
|
{Body: "just chatter"},
|
|
}
|
|
state := issue.ExtractStateJSON(comments)
|
|
if state["column"] != "review" || state["branch"] != "feat/new" {
|
|
t.Errorf("got %v, want the hidden review blob", state)
|
|
}
|
|
|
|
comments = append(comments, gitea.Comment{Body: "<!--spx-state-->\n{\"column\":\"done\"}"})
|
|
state = issue.ExtractStateJSON(comments)
|
|
if state["column"] != "done" {
|
|
t.Errorf("got %v, want the newer legacy YouTrack blob", state)
|
|
}
|
|
}
|
|
|
|
func TestMergeStateJSON_PostsHiddenFormatKeepingFields(t *testing.T) {
|
|
client := &fakeClient{
|
|
issueComments: map[int][]gitea.Comment{
|
|
5: {{Body: `{"column":"todo","prototypeSessionId":"proto-1","handoffBundleKey":"k"}`}},
|
|
},
|
|
}
|
|
err := issue.MergeStateJSON(context.Background(), client, "owner", "repo", 5, map[string]any{"column": "review"})
|
|
if err != nil {
|
|
t.Fatalf("MergeStateJSON: %v", err)
|
|
}
|
|
if len(client.posted) != 1 {
|
|
t.Fatalf("expected 1 posted comment, got %d", len(client.posted))
|
|
}
|
|
body := client.posted[0]
|
|
if !strings.HasPrefix(body, "<!-- spx-state\n") {
|
|
t.Errorf("posted body %q is not in the hidden format", body)
|
|
}
|
|
state, ok := issue.ParseStateComment(body)
|
|
if !ok {
|
|
t.Fatalf("posted body %q is not a state comment", body)
|
|
}
|
|
if state["column"] != "review" || state["prototypeSessionId"] != "proto-1" || state["handoffBundleKey"] != "k" {
|
|
t.Errorf("merged state %v lost or misapplied fields", state)
|
|
}
|
|
}
|