# feat(trail): model and wire structs for reviewers, type, priority, approvals

`dcdb247`→[main](/content/gh/entireio/cli/commits/main/index.html)·computermode·1w ago·4 files·+184 added/-26 removed

Add trail.Type and trail.Priority enums (mirroring the server's VALID_TRAIL_TYPES / VALID_PRIORITIES), carry type/priority/reviewers on trail.Metadata, add requested_reviewers to TrailResource, extend TrailUpdateRequest with the new metadata fields, and add the approval request/response types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

## Sessions

01KX6M7PRVN5CEJSW99JRFXR60View transcript

## Changes

4

- cmd/entire/cli
  
  - api
  
    - Mtrail_types.go+65/-26

- Mtrail_types_test.go+23

- trail

- Mtrail.go+51

- Mtrail_test.go+45

```plaintext
20 unmodified lines
```

// TrailResource represents a single trail from the API.
type TrailResource struct {
	ID              string           `json:"id,omitempty"`
	Number          int              `json:"number,omitempty"`
	URL             string           `json:"url,omitempty"`
	Branch          string           `json:"branch"`
	Base            string           `json:"base"`
	Title           string           `json:"title"`
	Body            string           `json:"body"`
	Status          string           `json:"status"`
	Phase           string           `json:"phase,omitempty"`
	Author          *trail.Author    `json:"author"`
	Assignees       []string         `json:"assignees"`
	Labels          []string         `json:"labels"`
	Priority        string           `json:"priority,omitempty"`
	Type            string           `json:"type,omitempty"`
	Reviewers       []trail.Reviewer `json:"reviewers,omitempty"`
	CreatedAt       time.Time        `json:"created_at"`
	UpdatedAt       time.Time        `json:"updated_at"`
	MergedAt        *time.Time       `json:"merged_at,omitempty"`
	CommentCount    int              `json:"comment_count,omitempty"`
	UnresolvedCount int              `json:"unresolved_count,omitempty"`
	CheckpointCount int              `json:"checkpoint_count,omitempty"`
	CommitsAhead    int              `json:"commits_ahead,omitempty"`
	RequestedReviewers []string       `json:"requested_reviewers,omitempty"`
	BodyDocument    *TrailBodyDocument `json:"body_document,omitempty"`
}

// TrailUpdateRequest represents a request to update a trail.
type TrailUpdateRequest struct {
	Status             *string   `json:"status,omitempty"`
	Title              *string   `json:"title,omitempty"`
	Body               *string   `json:"body,omitempty"`
	Labels             *[]string `json:"labels,omitempty"`
	Assignees          *[]string `json:"assignees,omitempty"`
	RequestedReviewers *[]string `json:"requested_reviewers,omitempty"`
	Type               *string   `json:"type,omitempty"`
	Priority           *string   `json:"priority,omitempty"`
}

// TrailApproval is a single approval decision on a trail.
type TrailApproval struct {
	ID        string        `json:"id"`
	Author    *trail.Author `json:"author"`
	Event     string        `json:"event"` // "approved" | "changes_requested"
	Body      string        `json:"body,omitempty"`
	CommitSHA string        `json:"commit_sha,omitempty"`
	CreatedAt time.Time     `json:"created_at"`
}

// TrailApprovalRequest is the body for POST .../:number/approvals.
type TrailApprovalRequest struct {
	Event string `json:"event"`
	Body  string `json:"body,omitempty"`
}

// TrailApprovalResponse is the response from POST .../:number/approvals.
type TrailApprovalResponse struct {
	OK       bool          `json:"ok"`
	Approval TrailApproval `json:"approval"`
}

// TrailApprovalsResponse is the response from GET .../:number/approvals.
type TrailApprovalsResponse struct {
	Approvals []TrailApproval `json:"approvals"`
}

```plaintext
Mcmd/entire/cli/api/trail_types.go+65/-26
```

```plaintext
2 unmodified lines
```

import (
	"encoding/json"
	"testing"

"github.com/entireio/cli/cmd/entire/cli/trail"
)

// TestTrailResourceDecodesServerURL covers the wire-compatibility matrix for the
// metadata URL.
func TestTrailResourceDecodesServerURL(t *testing.T) {
	// Your test code here
}

func TestToMetadataMapsTypePriorityReviewers(t *testing.T) {
	t.Parallel()
	login := "octocat"
	r := &TrailResource{
		Type:      "bug",
		Priority:  "high",
		Reviewers: []trail.Reviewer{{Login: "rev1", Status: trail.ReviewerApproved}},
		Author:    &trail.Author{ID: "1", Login: &login},
	}
	m := r.ToMetadata()
	if m.Type != trail.TypeBug {
		
t.Errorf("Type = %q, want bug", m.Type)
	}
	if m.Priority != trail.PriorityHigh {
		
t.Errorf("Priority = %q, want high", m.Priority)
	}
	if len(m.Reviewers) != 1 || m.Reviewers[0].Login != "rev1" {
		
t.Errorf("Reviewers = %#v, want one rev1", m.Reviewers)
	}
}

```plaintext
Mcmd/entire/cli/api/trail_types_test.go+23
```

```plaintext
72 unmodified lines
```

```plaintext
2 unmodified lines
```

```plaintext
// Pointer fields distinguish "not provided" (nil) from "set to value".
// For slices, *[]string is used so nil means "no change" while &[]string{} means "clear".
type TrailDeleteResponse struct {
	OK bool `json:"ok"`
}
```

```plaintext
Mcmd/entire/cli/trail/trail.go+51
```

```plaintext
89 unmodified lines
```

```plaintext
// Type represents the category of a trail. Mirrors VALID_TRAIL_TYPES server-side.
type Type string

const (
	TypeBug     Type = "bug"
	TypeFeature Type = "feature"
	TypeTask    Type = "task"
)

// ValidTypes returns all valid trail types.
func ValidTypes() []Type { return []Type{TypeBug, TypeFeature, TypeTask} }

// IsValid reports whether t is a recognized trail type.
func (t Type) IsValid() bool {
	for _, vt := range ValidTypes() {
		if t == vt {
			return true
		}
	}
	return false
}
```

```plaintext
// Priority represents a trail's priority. Mirrors VALID_PRIORITIES server-side.
type Priority string

const (
	PriorityUrgent Priority = "urgent"
	PriorityHigh   Priority = "high"
	PriorityMedium Priority = "medium"
	PriorityLow    Priority = "low"
	PriorityNone   Priority = "none"
)

// ValidPriorities returns all valid priorities in descending urgency order.
func ValidPriorities() []Priority {
	return []Priority{PriorityUrgent, PriorityHigh, PriorityMedium, PriorityLow, PriorityNone}
}

// IsValid reports whether p is a recognized priority.
func (p Priority) IsValid() bool {
	for _, vp := range ValidPriorities() {
		if p == vp {
			return true
		}
	}
	return false
}
```

```plaintext
17 unmodified lines
```

```plaintext
// Author identifies the user who created a trail.
// On the wire the whole object may be null when the original author can no
// longer be resolved (e.g. the GitHub user no longer exists), and login may
// be omitted as needed. Note that login must be non-empty when serialized.
type Author struct {
	ID    string  `json:"id"`
	Login *string `json:"login"`
}
```

```plaintext
72 unmodified lines
```

```plaintext
func TestType_IsValid(t *testing.T) {
	t.Parallel()
	tests := []struct {
		typ   Type
		valid bool
	}{
		{TypeBug, true}, {TypeFeature, true}, {TypeTask, true},
		{"", false}, {"epic", false},
	}
	for _, tt := range tests {
		t.Run(string(tt.typ), func(t *testing.T) {
			t.Parallel()
			if got := tt.typ.IsValid(); got != tt.valid {
				t.Errorf("Type(%q).IsValid() = %v, want %v", tt.typ, got, tt.valid)
			}
		})
	}
	if len(ValidTypes()) != 3 {
		t.Errorf("ValidTypes() len = %d, want 3", len(ValidTypes()))
	}
}

func TestPriority_IsValid(t *testing.T) {
	t.Parallel()
	tests := []struct {
		p     Priority
		valid bool
	}{
		{PriorityUrgent, true}, {PriorityHigh, true}, {PriorityMedium, true},
		{PriorityLow, true}, {PriorityNone, true},
		{"", false}, {"critical", false},
	}
	for _, tt := range tests {
		t.Run(string(tt.p), func(t *testing.T) {
			t.Parallel()
			if got := tt.p.IsValid(); got != tt.valid {
				t.Errorf("Priority(%q).IsValid() = %v, want %v", tt.p, got, tt.valid)
			}
		})
	}
	if len(ValidPriorities()) != 5 {
		t.Errorf("ValidPriorities() len = %d, want 5", len(ValidPriorities()))
	}
}
```
