Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions go/internal/forge/linear.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ const (
// linearClosedStateTypes are the Linear workflow-state `type` values that map
// to the forge's "closed" truth. Every other type maps to "open". Verified
// against Linear SDL WorkflowState.type: "triage", "backlog", "unstarted",
// "started", "completed", "canceled", "duplicate".
var linearClosedStateTypes = []string{"completed", "canceled"}
// "started", "completed", "canceled", "duplicate". "duplicate" is its own
// system-managed terminal category (not a member of "completed"), applied when
// an issue is marked a duplicate; Linear's own Active view is unstarted+started
// only, so a duplicate is never live work (RIG-3590).
var linearClosedStateTypes = []string{"completed", "canceled", "duplicate"}

// LinearConfig configures a Linear client.
type LinearConfig struct {
Expand Down
67 changes: 67 additions & 0 deletions go/internal/forge/linear_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,70 @@ func TestLinearActorProbeTransientErrorReprobed(t *testing.T) {
attributionUser, vars2["input"])
}
}

// TestMapLinearStateCoversEverySDLType pins the open/closed mapping for every
// workflow-state `type` Linear's SDL can return. A type absent from
// linearClosedStateTypes silently reads as open, so an issue closed as a
// duplicate would be re-served as live work.
func TestMapLinearStateCoversEverySDLType(t *testing.T) {
t.Parallel()

// All seven SDL types, not just the ones in the closed set: this pins the
// verdict for each by name, so narrowing the set fails with the type named.
// It does NOT detect enum drift — an eighth type would take the open
// fallback and stay green. RIG-3590 was a KNOWN type left unhandled.
for _, tc := range []struct {
stateType string
want string
}{
{"triage", stateOpen},
{"backlog", stateOpen},
{"unstarted", stateOpen},
{"started", stateOpen},
{"completed", stateClosed},
{"canceled", stateClosed},
{"duplicate", stateClosed},
// The documented fallback: an unrecognised type maps to open.
{"no_such_type", stateOpen},
} {
if got := mapLinearState(tc.stateType); got != tc.want {
t.Errorf("mapLinearState(%q) = %q, want %q", tc.stateType, got, tc.want)
}
}
}

// TestTeamIssueFilterExcludesDuplicateFromOpen proves the query filter and the
// read mapping agree. They share linearClosedStateTypes, so a state missing
// from it both mis-maps a fetched issue AND makes the server's open-issue query
// return it — the mapping test alone would not catch a divergence here.
func TestTeamIssueFilterExcludesDuplicateFromOpen(t *testing.T) {
t.Parallel()

openTypes := extractStateTypes(t, teamIssueFilter("RIG", IssueFilter{State: stateOpen}), "nin")
if !slices.Contains(openTypes, "duplicate") {
t.Errorf("open-issue filter must exclude duplicate; nin = %v", openTypes)
}
closedTypes := extractStateTypes(t, teamIssueFilter("RIG", IssueFilter{State: stateClosed}), "in")
if !slices.Contains(closedTypes, "duplicate") {
t.Errorf("closed-issue filter must include duplicate; in = %v", closedTypes)
}
}

// extractStateTypes digs the state-type list out of a teamIssueFilter result
// under the given set operator ("in" or "nin").
func extractStateTypes(t *testing.T, filter map[string]any, op string) []string {
t.Helper()
state, ok := filter["state"].(map[string]any)
if !ok {
t.Fatalf("filter has no state clause: %#v", filter)
}
typ, ok := state["type"].(map[string]any)
if !ok {
t.Fatalf("state clause has no type clause: %#v", state)
}
got, ok := typ[op].([]string)
if !ok {
t.Fatalf("type clause has no %q list: %#v", op, typ)
}
return got
}
2 changes: 1 addition & 1 deletion go/internal/forge/testdata/linear/list_issues.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"variables": {
"filter": {
"team": { "key": { "eq": "SEA" } },
"state": { "type": { "nin": ["completed", "canceled"] } }
"state": { "type": { "nin": ["completed", "canceled", "duplicate"] } }
}
}
}
Expand Down
Loading