Skip to content

[formal-spec] replace-label-compliance/README.md — Formal model & test suite — 2026-07-11 #44980

Description

@github-actions

Summary

The replace-label-compliance specification defines normative compliance fixtures for the replace-label safe-output type in GitHub Agentic Workflows. It formulates three core RL requirements (RL-001 glob semantics, RL-002 allowlist enforcement, RL-003 blocklist ordering) as structured YAML test scenarios that serve as the ground truth for conformance testing. This formal analysis extracts the underlying predicates — type invariants on label strings, partial-order properties on evaluation stages, and decision-function postconditions — and maps them into a Go testify suite that can run in-process without any JavaScript runtime dependency.

Specification

  • File: specs/replace-label-compliance/README.md
  • Focus area: Label access-control decision pipeline for the replace-label safe-output type
  • Formal notation used: F* (pre/post contracts) + Z3-style SMT predicates + TLA+ for stage ordering

Formal Model

Predicates and invariants (illustrative notation)

P1 — GlobSemantics (from §4.1.2 RL-001, fixture rl-001)

// Star (*) matches any sequence of characters within a label name;
// character classes ([a-z]) are supported.
predicate GlobSemantics (pattern label : string) =
  gobwasGlob.Match pattern label

P2 — AllowlistPermits (from §4.1.2 RL-002, fixture rl-002)

// An empty allowlist is universally permissive.
// A non-empty allowlist is restrictive: the label MUST match at least one pattern.
predicate AllowlistPermits (allowlist : list string) (label : string) =
  allowlist = [] \/ exists p <- allowlist, GlobSemantics p label

P3 — BlocklistRejects (from §4.1.2 RL-003, fixture rl-003)

// A label MUST be rejected if it matches ANY blocked pattern.
predicate BlocklistRejects (blocklist : list string) (label : string) =
  exists p <- blocklist, GlobSemantics p label

P4 — BlocklistBeforeAllowlist (from §4.1.2 RL-003 "security boundary")

-- TLA+ ordering invariant
LABEL_VALIDATION_ORDER ==
  /\ IF BlocklistRejects(blocked, label) THEN decision = DENY
     ELSE IF ~AllowlistPermits(allowlist, label) THEN decision = DENY
          ELSE decision = ALLOW
-- Corollary: a label blocked AND allowed is always denied
BlockOverridesAllow ==
  BlocklistRejects(blocked, label) => decision = DENY

P5 — SchemaRequiredFields (from §4.2 RL-004, RL-005)

; label_to_remove and label_to_add must be non-empty after trimming and
; must not exceed 128 characters.
(assert (and
  (> (str.len (str.trim label_to_remove)) 0)
  (<= (str.len label_to_remove) 128)
  (> (str.len (str.trim label_to_add)) 0)
  (<= (str.len label_to_add) 128)))

P6 — RepoMaxLength (from §4.2.1 RL-006)

(assert (<= (str.len repo) 256))

P7 — CountGateExclusive (from §5.2 RL-010, RL-011, RL-012)

// count must be strictly less than max; equality means rejection.
// Default max = 5.
predicate CountGatePermits (count max : nat) = count < max
lemma DefaultMax : CountGatePermits 0 5 = true
lemma GateClosed  : CountGatePermits 5 5 = false

P8 — LabelSetComputation (from §5.7 RL-029, RL-034)

// New label set = (current \ {label_to_remove}) ∪ {label_to_add}, deduplicated.
// When label_to_remove is absent the operation still succeeds.
let computeNewLabelSet (current : list string)
                       (toRemove toAdd : string) : list string =
  let without = List.filter (fun l -> l <> toRemove) current in
  List.dedupe (without @ [toAdd])
lemma MissingRemoveProceeds :
  ~List.mem toRemove current =>
  computeNewLabelSet current toRemove toAdd = List.dedupe (current @ [toAdd])

P9 — StagedNoWrite (from §5.6 RL-027, RL-028)

// When staged = true, no GitHub write API must be called.
predicate StagedPrecondition (staged : bool) (apiCallsMade : nat) =
  staged = true => apiCallsMade = 0
predicate StagedResult (result : handler_result) =
  result.success = true /\ result.staged = true

P10 — SingleRESTCall (from §6 RL-036, RL-043)

// Label replacement is executed via exactly one PUT call; separate add/remove
// calls are forbidden.
predicate SingleAtomicCall (putCalls addCalls removeCalls : nat) =
  putCalls = 1 /\ addCalls = 0 /\ removeCalls = 0

P11 — BlocklistAppliesSymmetrically (from §4.1.2 RL-003)

// The blocked list applies identically to both label_to_add and label_to_remove.
predicate BlockedSymmetric (blocked : list string) (toAdd toRemove : string) =
  (BlocklistRejects blocked toAdd => decision_add = DENY) /\
  (BlocklistRejects blocked toRemove => decision_remove = DENY)

P12 — RequiredLabelsGate (from §5.5 RL-024)

// All required-labels must be present on the item; absent required labels cause
// a skip (not a hard failure).
predicate RequiredLabelsSatisfied (required current : list string) =
  forall l <- required, List.mem l current
predicate GateSkip (skipped : bool) = skipped = true

P13 — TitlePrefixGate (from §5.5 RL-025)

// The item title must start with required-title-prefix; otherwise skip.
predicate TitlePrefixSatisfied (prefix title : string) =
  prefix = "" \/ String.startsWith title prefix

P14 — AddDeduplication (from §6.1 RL-041, RL-042)

// label_to_add appears exactly once in the final labels array.
lemma AddedOnce (result : list string) (toAdd : string) :
  List.count toAdd result = 1

P15 — HardErrorOnSetLabelsFail (from §7.2 RL-046)

// REST call failure is a hard error; workflow must not be silently skipped.
predicate HardError (result : handler_result) =
  result.success = false /\ result.error <> None

Behavioral Coverage Map

Predicate / Invariant Test Function Description
P1 — GlobSemantics TestFormalGlobSemantics Star and char-class glob matching from fixture rl-001
P2 — AllowlistPermits TestFormalAllowlistEnforcement Empty vs non-empty allowlist, add and remove directions
P3 + P4 — BlocklistRejects + BlocklistBeforeAllowlist TestFormalBlocklistOrdering Blocklist takes priority over allowlist; symmetric for add/remove
P5 — SchemaRequiredFields TestFormalSchemaRequiredFields Missing/empty/too-long label_to_remove and label_to_add
P6 — RepoMaxLength TestFormalRepoMaxLength repo field ≤ 256 characters
P7 — CountGateExclusive TestFormalCountGate count < max allowed, count = max rejected, default max = 5
P8 — LabelSetComputation TestFormalLabelSetComputation Correct set arithmetic; missing-remove proceeds
P9 — StagedNoWrite TestFormalStagedMode staged=true returns success+staged=true, no writes
P10 — SingleRESTCall TestFormalSingleRESTCall Verify exactly one PUT; no separate add/remove
P11 — BlocklistAppliesSymmetrically TestFormalBlocklistSymmetry blocked applies to both label_to_add and label_to_remove
P12 — RequiredLabelsGate TestFormalRequiredLabelsGate All required labels present → proceed; missing → skip
P13 — TitlePrefixGate TestFormalTitlePrefixGate Matching prefix → proceed; non-matching → skip
P14 — AddDeduplication TestFormalAddDeduplication label_to_add appears exactly once in output
P15 — HardErrorOnSetLabelsFail TestFormalHardErrorOnRESTFail REST failure yields success=false, non-nil error
Edge: Exact glob no-wildcard TestFormalGlobExactNoWildcard Exact pattern 'bug' does not match 'bug-fix'
Edge: Alias fields TestFormalItemNumberAliases issue_number/pr_number/pull_number resolve correctly
Edge: Cross-repo restriction TestFormalCrossRepoRestriction repo not in allowed-repos is rejected

Generated Test Suite

📄 `pkg/workflow/replace_label_compliance_formal_test.go`
(go/redacted):build !integration

// Package workflow – replace_label_compliance formal model tests.
//
// This file encodes the formal specification predicates (P1–P15 and edge cases)
// derived from specs/replace-label-compliance/README.md and the normative
// compliance fixtures rl-001-glob-semantics.yaml, rl-002-allowlist-enforcement.yaml,
// and rl-003-blocklist-ordering.yaml.
//
// Formal predicates encoded:
//   P1  GlobSemantics         — gobwas/glob star and char-class matching
//   P2  AllowlistPermits      — empty allowlist is universally permissive
//   P3  BlocklistRejects      — label matches any blocked pattern → denied
//   P4  BlocklistBeforeAllow  — blocked overrides allowed (security boundary)
//   P5  SchemaRequiredFields  — label_to_{remove,add} non-empty, ≤128 chars
//   P6  RepoMaxLength         — repo ≤256 chars
//   P7  CountGateExclusive    — count < max; default max=5
//   P8  LabelSetComputation   — (current \ remove) ∪ {add}, deduped
//   P9  StagedNoWrite         — staged=true → success+staged=true, no API writes
//   P10 SingleRESTCall        — exactly one PUT /issues/{n}/labels
//   P11 BlocklistSymmetric    — blocked applies to both add and remove
//   P12 RequiredLabelsGate    — all required-labels present → proceed; else skip
//   P13 TitlePrefixGate       — required-title-prefix must match; else skip
//   P14 AddDeduplication      — label_to_add appears exactly once
//   P15 HardErrorOnFail       — REST failure → success=false, non-nil error
package workflow

import (
	"path"
	"slices"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// Stub types — replace with real implementation when available
// ---------------------------------------------------------------------------

// stub — replace with real implementation
type complianceLabelDecision int

const (
	complianceAllow complianceLabelDecision = iota
	complianceDeny
)

// stub — replace with real implementation
type complianceHandlerResult struct {
	Success bool
	Skipped bool
	Staged  bool
	Error   *string
	Labels  []string
}

// ---------------------------------------------------------------------------
// Formal helpers (re-implement spec semantics for predicate testing)
// ---------------------------------------------------------------------------

// complianceMatchAnyGlob reports whether value matches any pattern in the list.
// Uses path.Match semantics as an approximation of gobwas/glob for unit tests.
// Production validation uses the gobwas/glob library; replace this helper if
// semantics diverge in edge cases.
func complianceMatchAnyGlob(value string, patterns []string) bool {
	lv := strings.ToLower(value)
	for _, p := range patterns {
		matched, err := path.Match(strings.ToLower(p), lv)
		if err == nil && matched {
			return true
		}
	}
	return false
}

// complianceLabelAllowed implements the P2+P3+P4 decision function.
// blocklist is evaluated before allowlist (security boundary, P4).
func complianceLabelAllowed(label string, allowlist, blocklist []string) complianceLabelDecision {
	if complianceMatchAnyGlob(label, blocklist) {
		return complianceDeny
	}
	if len(allowlist) > 0 && !complianceMatchAnyGlob(label, allowlist) {
		return complianceDeny
	}
	return complianceAllow
}

// complianceSchemaValid checks P5 and P6.
func complianceSchemaValid(labelToRemove, labelToAdd, repo string) bool {
	if strings.TrimSpace(labelToRemove) == "" || len(labelToRemove) > 128 {
		return false
	}
	if strings.TrimSpace(labelToAdd) == "" || len(labelToAdd) > 128 {
		return false
	}
	if len(repo) > 256 {
		return false
	}
	return true
}

// complianceCountGatePermits checks P7.
func complianceCountGatePermits(count, max int) bool {
	return count < max
}

// complianceComputeNewLabelSet implements P8:
// newLabels = dedup((current \ {toRemove}) ∪ {toAdd})
func complianceComputeNewLabelSet(current []string, toRemove, toAdd string) []string {
	seen := map[string]bool{}
	var result []string
	for _, l := range current {
		if l == toRemove {
			continue
		}
		if !seen[l] {
			seen[l] = true
			result = append(result, l)
		}
	}
	if !seen[toAdd] {
		result = append(result, toAdd)
	}
	return result
}

// complianceResolveItemNumber implements RL-016 alias priority.
func complianceResolveItemNumber(fields map[string]any) (int, bool) {
	for _, key := range []string{"item_number", "issue_number", "pr_number", "pull_number"} {
		if v, ok := fields[key]; ok {
			if n, ok := v.(int); ok {
				return n, true
			}
		}
	}
	return 0, false
}

// complianceRepoAllowed checks RL-015.
func complianceRepoAllowed(repo string, allowedRepos []string) bool {
	if len(allowedRepos) == 0 {
		return true
	}
	return slices.Contains(allowedRepos, repo)
}

// ---------------------------------------------------------------------------
// P1 — GlobSemantics (fixture rl-001)
// ---------------------------------------------------------------------------

func TestFormalGlobSemantics(t *testing.T) {
	tests := []struct {
		name     string
		pattern  string
		label    string
		wantMatch bool
	}{
		// rl-001-glob-star-match: star matches any characters
		{"star matches priority-high", "priority-*", "priority-high", true},
		// rl-001-glob-star-no-match: star does not match unrelated prefix
		{"star no match for 'bug'", "priority-*", "bug", false},
		// rl-001-glob-char-class-match
		{"char class p[0-9] matches p1", "p[0-9]", "p1", true},
		{"char class p[0-9] no match pX", "p[0-9]", "pX", false},
		// star at end matches empty suffix
		{"star matches base only", "bug-*", "bug-fix", true},
		// exact pattern with no wildcard
		{"exact 'done' matches 'done'", "done", "done", true},
		{"exact 'done' no match 'done-2'", "done", "done-2", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := complianceMatchAnyGlob(tt.label, []string{tt.pattern})
			assert.Equal(t, tt.wantMatch, got,
				"P1 GlobSemantics: pattern=%q label=%q expected match=%v", tt.pattern, tt.label, tt.wantMatch)
		})
	}
}

// P1 edge case: exact pattern must not match a longer string (rl-001-exact-no-glob)
func TestFormalGlobExactNoWildcard(t *testing.T) {
	allowedRemove := []string{"bug"}
	labelToRemove := "bug-fix"
	decision := complianceLabelAllowed(labelToRemove, allowedRemove, nil)
	assert.Equal(t, complianceDeny, decision,
		"P1 edge: exact pattern 'bug' must NOT match 'bug-fix' (no wildcard)")
}

// ---------------------------------------------------------------------------
// P2 — AllowlistPermits (fixture rl-002)
// ---------------------------------------------------------------------------

func TestFormalAllowlistEnforcement(t *testing.T) {
	tests := []struct {
		name      string
		allowlist []string
		label     string
		want      complianceLabelDecision
	}{
		// rl-002-allowed-add-match: non-empty allowlist, label matches
		{"non-empty allows matching label", []string{"done"}, "done", complianceAllow},
		// rl-002-allowed-add-miss: non-empty allowlist, label does not match
		{"non-empty rejects non-matching label", []string{"done"}, "wontfix", complianceDeny},
		// rl-002-empty-allowed-remove-permits: empty allowlist is universally permissive
		{"empty allowlist permits any label", []string{}, "state-in-progress", complianceAllow},
		// nil allowlist is also permissive
		{"nil allowlist permits any label", nil, "arbitrary-label", complianceAllow},
		// rl-002-allowed-remove-miss: glob pattern, no match
		{"state-* no match for 'bug'", []string{"state-*"}, "bug", complianceDeny},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := complianceLabelAllowed(tt.label, tt.allowlist, nil)
			assert.Equal(t, tt.want, got,
				"P2 AllowlistPermits: allowlist=%v label=%q", tt.allowlist, tt.label)
		})
	}
}

// ---------------------------------------------------------------------------
// P3 + P4 — BlocklistRejects + BlocklistBeforeAllowlist (fixture rl-003)
// ---------------------------------------------------------------------------

func TestFormalBlocklistOrdering(t *testing.T) {
	tests := []struct {
		name      string
		allowlist []string
		blocklist []string
		label     string
		want      complianceLabelDecision
	}{
		// rl-003-block-overrides-allow: blocked AND allowed → deny (P4)
		{
			"blocked label denied even when in allowlist",
			[]string{"security"}, []string{"security"}, "security",
			complianceDeny,
		},
		// rl-003-wildcard-block-overrides-wildcard-allow
		{
			"wildcard blocked overrides wildcard allowed",
			[]string{"*"}, []string{"security-*"}, "security-critical",
			complianceDeny,
		},
		// rl-003-unblocked-label-allowed
		{
			"non-blocked label passes through allowlist",
			[]string{"*"}, []string{"security-*"}, "bug",
			complianceAllow,
		},
		// rl-003-block-prevents-removal: blocked applies to removal too (P11)
		{
			"blocked pattern prevents removal of ~internal",
			[]string{"*"}, []string{"~*"}, "~internal",
			complianceDeny,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := complianceLabelAllowed(tt.label, tt.allowlist, tt.blocklist)
			assert.Equal(t, tt.want, got,
				"P3+P4 BlocklistOrdering: allowlist=%v blocklist=%v label=%q",
				tt.allowlist, tt.blocklist, tt.label)
		})
	}
}

// ---------------------------------------------------------------------------
// P11 — BlocklistAppliesSymmetrically
// ---------------------------------------------------------------------------

func TestFormalBlocklistSymmetry(t *testing.T) {
	blocklist := []string{"~*"}

	// blocked pattern applied to add direction
	addDecision := complianceLabelAllowed("~important", []string{"*"}, blocklist)
	assert.Equal(t, complianceDeny, addDecision,
		"P11 BlocklistSymmetric: blocked pattern must deny label_to_add")

	// blocked pattern applied to remove direction
	removeDecision := complianceLabelAllowed("~legacy", []string{"*"}, blocklist)
	assert.Equal(t, complianceDeny, removeDecision,
		"P11 BlocklistSymmetric: blocked pattern must deny label_to_remove")

	// non-matching label is allowed in both directions
	normalLabel := complianceLabelAllowed("bug", []string{"*"}, blocklist)
	assert.Equal(t, complianceAllow, normalLabel,
		"P11 BlocklistSymmetric: non-blocked label must be allowed in both directions")
}

// ---------------------------------------------------------------------------
// P5 — SchemaRequiredFields
// ---------------------------------------------------------------------------

func TestFormalSchemaRequiredFields(t *testing.T) {
	validRemove := "in-review"
	validAdd := "approved"

	tests := []struct {
		name          string
		labelToRemove string
		labelToAdd    string
		repo          string
		wantValid     bool
	}{
		{"valid message", validRemove, validAdd, "owner/repo", true},
		// T-RL-001: missing label_to_remove
		{"missing label_to_remove", "", validAdd, "", false},
		{"whitespace-only label_to_remove", "   ", validAdd, "", false},
		// T-RL-002: missing label_to_add
		{"missing label_to_add", validRemove, "", "", false},
		// T-RL-003: label_to_remove exceeds 128 chars
		{"label_to_remove > 128 chars", strings.Repeat("x", 129), validAdd, "", false},
		// T-RL-004: label_to_add exceeds 128 chars
		{"label_to_add > 128 chars", validRemove, strings.Repeat("y", 129), "", false},
		// T-RL-005: repo exceeds 256 chars
		{"repo > 256 chars", validRemove, validAdd, strings.Repeat("r", 257), false},
		// edge: exactly 128 chars is valid
		{"label exactly 128 chars is valid", strings.Repeat("a", 128), validAdd, "", true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := complianceSchemaValid(tt.labelToRemove, tt.labelToAdd, tt.repo)
			assert.Equal(t, tt.wantValid, got,
				"P5 SchemaRequired: remove=%q add=%q repo=%q",
				tt.labelToRemove, tt.labelToAdd, tt.repo)
		})
	}
}

// ---------------------------------------------------------------------------
// P6 — RepoMaxLength
// ---------------------------------------------------------------------------

func TestFormalRepoMaxLength(t *testing.T) {
	// exactly 256 chars: valid
	exact256 := strings.Repeat("o", 128) + "/" + strings.Repeat("r", 127)
	require.Len(t, exact256, 256, "test setup: exact256 must be 256 chars")
	assert.True(t, complianceSchemaValid("a", "b", exact256),
		"P6 RepoMaxLength: 256-char repo is valid")

	// 257 chars: invalid
	over256 := exact256 + "x"
	assert.False(t, complianceSchemaValid("a", "b", over256),
		"P6 RepoMaxLength: 257-char repo must be rejected")
}

// ---------------------------------------------------------------------------
// P7 — CountGateExclusive
// ---------------------------------------------------------------------------

func TestFormalCountGate(t *testing.T) {
	// T-RL-010: count < max → allowed
	assert.True(t, complianceCountGatePermits(0, 5),
		"P7 CountGate: count=0 < max=5 must be permitted")
	assert.True(t, complianceCountGatePermits(4, 5),
		"P7 CountGate: count=4 < max=5 must be permitted")

	// T-RL-011: count = max → rejected (gate is exclusive)
	assert.False(t, complianceCountGatePermits(5, 5),
		"P7 CountGate: count=5 = max=5 must be rejected (exclusive gate)")

	// T-RL-012: default max = 5
	const defaultMax = 5
	assert.True(t, complianceCountGatePermits(0, defaultMax),
		"P7 CountGate: default max must be 5")
	assert.False(t, complianceCountGatePermits(5, defaultMax),
		"P7 CountGate: count=5 with default max=5 must be rejected")

	// edge: count > max is also rejected
	assert.False(t, complianceCountGatePermits(10, 5),
		"P7 CountGate: count > max must be rejected")
}

// ---------------------------------------------------------------------------
// P8 — LabelSetComputation
// ---------------------------------------------------------------------------

func TestFormalLabelSetComputation(t *testing.T) {
	tests := []struct {
		name      string
		current   []string
		toRemove  string
		toAdd     string
		wantSet   []string
		wantHasAdd bool
		wantNoRemove bool
	}{
		{
			name:         "T-RL-040: remove present label, add new label",
			current:      []string{"in-review", "bug", "triage"},
			toRemove:     "in-review",
			toAdd:        "approved",
			wantSet:      []string{"bug", "triage", "approved"},
			wantHasAdd:   true,
			wantNoRemove: true,
		},
		{
			// T-RL-044: label_to_remove not present → still adds label_to_add
			name:         "T-RL-044: missing label_to_remove still adds label_to_add",
			current:      []string{"bug", "triage"},
			toRemove:     "in-review",
			toAdd:        "approved",
			wantSet:      []string{"bug", "triage", "approved"},
			wantHasAdd:   true,
			wantNoRemove: true,
		},
		{
			// deduplication: label_to_add already in current
			name:       "dedup: label_to_add already present",
			current:    []string{"bug", "approved"},
			toRemove:   "bug",
			toAdd:      "approved",
			wantSet:    []string{"approved"},
			wantHasAdd: true,
		},
		{
			// empty current
			name:       "empty current labels: only add",
			current:    []string{},
			toRemove:   "ghost",
			toAdd:      "new-label",
			wantSet:    []string{"new-label"},
			wantHasAdd: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := complianceComputeNewLabelSet(tt.current, tt.toRemove, tt.toAdd)
			assert.Equal(t, tt.wantSet, result,
				"P8 LabelSetComputation: current=%v remove=%q add=%q",
				tt.current, tt.toRemove, tt.toAdd)
			if tt.wantHasAdd {
				assert.Contains(t, result, tt.toAdd,
					"P8: label_to_add must be present in result")
			}
			if tt.wantNoRemove {
				assert.NotContains(t, result, tt.toRemove,
					"P8: label_to_remove must not be in result")
			}
		})
	}
}

// ---------------------------------------------------------------------------
// P14 — AddDeduplication (RL-041, RL-042)
// ---------------------------------------------------------------------------

func TestFormalAddDeduplication(t *testing.T) {
	// label_to_add appears in current labels — must still appear exactly once
	current := []string{"approved", "bug", "approved"}
	result := complianceComputeNewLabelSet(current, "bug", "approved")

	count := 0
	for _, l := range result {
		if l == "approved" {
			count++
		}
	}
	assert.Equal(t, 1, count,
		"P14 AddDeduplication: label_to_add must appear exactly once in the labels array")
}

// ---------------------------------------------------------------------------
// P9 — StagedMode
// ---------------------------------------------------------------------------

func TestFormalStagedMode(t *testing.T) {
	// Verify Go config parsing: staged:true is parsed correctly
	stageCases := []struct {
		name   string
		config map[string]any
		want   bool
	}{
		{"staged true", map[string]any{"staged": true}, true},
		{"staged false", map[string]any{"staged": false}, false},
		{"staged absent defaults false", map[string]any{}, false},
	}
	for _, tc := range stageCases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := &ReplaceLabelConfig{}
			if v, ok := tc.config["staged"].(bool); ok {
				cfg.Staged = v
			}
			assert.Equal(t, tc.want, cfg.Staged,
				"P9 StagedMode: staged field must be parsed correctly")
		})
	}

	// stub — replace with real implementation
	// When staged=true the handler must return {success:true, staged:true}
	// and must NOT increment write API call count.
	stagingResult := &complianceHandlerResult{Success: true, Staged: true}
	assert.True(t, stagingResult.Success,
		"P9 StagedMode: staged result must have success=true")
	assert.True(t, stagingResult.Staged,
		"P9 StagedMode: staged result must have staged=true")
	assert.Nil(t, stagingResult.Error,
		"P9 StagedMode: staged result must not carry an error")
}

// ---------------------------------------------------------------------------
// P10 — SingleRESTCall
// ---------------------------------------------------------------------------

func TestFormalSingleRESTCall(t *testing.T) {
	// stub — replace with real implementation
	// This test verifies the invariant via the call-count model.
	type apiCallCounts struct {
		PutSetLabels  int
		AddLabels     int
		RemoveLabels  int
	}

	successfulCall := apiCallCounts{PutSetLabels: 1, AddLabels: 0, RemoveLabels: 0}

	assert.Equal(t, 1, successfulCall.PutSetLabels,
		"P10 SingleRESTCall: exactly one PUT /issues/{n}/labels call must be made")
	assert.Equal(t, 0, successfulCall.AddLabels,
		"P10 SingleRESTCall: separate addLabels call must NOT be made")
	assert.Equal(t, 0, successfulCall.RemoveLabels,
		"P10 SingleRESTCall: separate removeLabels call must NOT be made")
}

// ---------------------------------------------------------------------------
// P12 — RequiredLabelsGate
// ---------------------------------------------------------------------------

func complianceRequiredLabelsSatisfied(required, current []string) bool {
	for _, req := range required {
		if !slices.Contains(current, req) {
			return false
		}
	}
	return true
}

func TestFormalRequiredLabelsGate(t *testing.T) {
	// T-RL-030: all required labels present → proceed
	assert.True(t,
		complianceRequiredLabelsSatisfied([]string{"triage", "approved"}, []string{"triage", "approved", "bug"}),
		"P12 RequiredLabelsGate: all required labels present must allow operation")

	// T-RL-031: missing required label → skip
	assert.False(t,
		complianceRequiredLabelsSatisfied([]string{"triage", "approved"}, []string{"bug"}),
		"P12 RequiredLabelsGate: missing required label must cause skip")

	// empty required-labels is always satisfied
	assert.True(t,
		complianceRequiredLabelsSatisfied([]string{}, []string{}),
		"P12 RequiredLabelsGate: empty required-labels must always be satisfied")

	// stub result for skipped case
	skippedResult := &complianceHandlerResult{Success: false, Skipped: true}
	assert.True(t, skippedResult.Skipped,
		"P12 RequiredLabelsGate: skipped result must have skipped=true")
	assert.False(t, skippedResult.Success,
		"P12 RequiredLabelsGate: skipped result must not be marked success")
}

// ---------------------------------------------------------------------------
// P13 — TitlePrefixGate
// ---------------------------------------------------------------------------

func complianceTitlePrefixSatisfied(prefix, title string) bool {
	if prefix == "" {
		return true
	}
	return strings.HasPrefix(title, prefix)
}

func TestFormalTitlePrefixGate(t *testing.T) {
	// T-RL-032: title matches required-title-prefix → proceed
	assert.True(t,
		complianceTitlePrefixSatisfied("[Bug]", "[Bug] user cannot log in"),
		"P13 TitlePrefixGate: matching prefix must allow operation")

	// T-RL-033: title does not match → skip
	assert.False(t,
		complianceTitlePrefixSatisfied("[Bug]", "[Feature] add dark mode"),
		"P13 TitlePrefixGate: non-matching prefix must cause skip")

	// empty prefix is always satisfied
	assert.True(t,
		complianceTitlePrefixSatisfied("", "anything"),
		"P13 TitlePrefixGate: empty prefix must always be satisfied")
}

// ---------------------------------------------------------------------------
// P15 — HardErrorOnRESTFail
// ---------------------------------------------------------------------------

func TestFormalHardErrorOnRESTFail(t *testing.T) {
	// stub — replace with real implementation
	errMsg := "REST setLabels failed: 422 Unprocessable Entity"
	failResult := &complianceHandlerResult{
		Success: false,
		Error:   &errMsg,
	}
	assert.False(t, failResult.Success,
		"P15 HardError: REST failure must set success=false")
	require.NotNil(t, failResult.Error,
		"P15 HardError: REST failure must carry a non-nil error message")
	assert.NotEmpty(t, *failResult.Error,
		"P15 HardError: error message must be non-empty")
	assert.False(t, failResult.Skipped,
		"P15 HardError: hard error must not be marked as skipped (skipped implies soft skip)")
}

// ---------------------------------------------------------------------------
// Edge: Item number alias resolution (RL-016)
// ---------------------------------------------------------------------------

func TestFormalItemNumberAliases(t *testing.T) {
	tests := []struct {
		name   string
		fields map[string]any
		wantN  int
		wantOK bool
	}{
		{"item_number takes priority", map[string]any{"item_number": 10, "issue_number": 20}, 10, true},
		{"issue_number when no item_number", map[string]any{"issue_number": 15}, 15, true},
		{"pr_number fallback", map[string]any{"pr_number": 7}, 7, true},
		{"pull_number last fallback", map[string]any{"pull_number": 3}, 3, true},
		{"no alias present", map[string]any{}, 0, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			n, ok := complianceResolveItemNumber(tt.fields)
			assert.Equal(t, tt.wantOK, ok,
				"ItemNumberAlias: ok mismatch for fields %v", tt.fields)
			if tt.wantOK {
				assert.Equal(t, tt.wantN, n,
					"ItemNumberAlias: resolved number mismatch")
			}
		})
	}
}

// ---------------------------------------------------------------------------
// Edge: Cross-repo restriction (RL-015)
// ---------------------------------------------------------------------------

func TestFormalCrossRepoRestriction(t *testing.T) {
	allowedRepos := []string{"owner/repo-a", "owner/repo-b"}

	// T-RL-070: repo in allowed-repos is accepted
	assert.True(t, complianceRepoAllowed("owner/repo-a", allowedRepos),
		"CrossRepoRestriction: repo in allowed-repos must be accepted")

	// T-RL-071: repo not in allowed-repos is rejected
	assert.False(t, complianceRepoAllowed("owner/repo-x", allowedRepos),
		"CrossRepoRestriction: repo not in allowed-repos must be rejected")

	// T-RL-072: empty allowed-repos permits any repo (fallback to target-repo)
	assert.True(t, complianceRepoAllowed("owner/any-repo", []string{}),
		"CrossRepoRestriction: empty allowed-repos must permit any repo")
}

Usage

  1. Copy replace_label_compliance_formal_test.go to pkg/workflow/.
  2. Replace any // stub types with real handler result types from the production codebase.
  3. Run: go test ./pkg/workflow/... -run FormalCompliance

Context

Generated by 🔬 Daily Formal Spec Verifier · 118.6 AIC · ⌖ 13.3 AIC · ⊞ 7.2K ·

  • expires on Jul 18, 2026, 7:53 AM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions