feat: implement conditional bond type for mol bond (bd-kzda)
Conditional bonds now work as documented: "B runs only if A fails". Implementation: - Add DepConditionalBlocks dependency type to types.go - Add IsFailureClose() helper to detect failure keywords in close_reason - Update blocked cache to handle conditional-blocks: - B is blocked while A is open - B stays blocked if A closes with success - B becomes unblocked if A closes with failure Failure keywords: failed, rejected, wontfix, cancelled, abandoned, blocked, error, timeout, aborted (case-insensitive) Updated bondProtoProto, bondProtoMol, bondMolMol to use DepConditionalBlocks for conditional bond type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -365,8 +365,9 @@ type DependencyType string
|
||||
// Dependency type constants
|
||||
const (
|
||||
// Workflow types (affect ready work calculation)
|
||||
DepBlocks DependencyType = "blocks"
|
||||
DepParentChild DependencyType = "parent-child"
|
||||
DepBlocks DependencyType = "blocks"
|
||||
DepParentChild DependencyType = "parent-child"
|
||||
DepConditionalBlocks DependencyType = "conditional-blocks" // B runs only if A fails (bd-kzda)
|
||||
|
||||
// Association types
|
||||
DepRelated DependencyType = "related"
|
||||
@@ -395,7 +396,7 @@ func (d DependencyType) IsValid() bool {
|
||||
// Returns false for custom/user-defined types (which are still valid).
|
||||
func (d DependencyType) IsWellKnown() bool {
|
||||
switch d {
|
||||
case DepBlocks, DepParentChild, DepRelated, DepDiscoveredFrom,
|
||||
case DepBlocks, DepParentChild, DepConditionalBlocks, DepRelated, DepDiscoveredFrom,
|
||||
DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes,
|
||||
DepAuthoredBy, DepAssignedTo, DepApprovedBy:
|
||||
return true
|
||||
@@ -404,9 +405,41 @@ func (d DependencyType) IsWellKnown() bool {
|
||||
}
|
||||
|
||||
// AffectsReadyWork returns true if this dependency type blocks work.
|
||||
// Only "blocks" and "parent-child" relationships affect the ready work calculation.
|
||||
// Only "blocks", "parent-child", and "conditional-blocks" affect the ready work calculation.
|
||||
func (d DependencyType) AffectsReadyWork() bool {
|
||||
return d == DepBlocks || d == DepParentChild
|
||||
return d == DepBlocks || d == DepParentChild || d == DepConditionalBlocks
|
||||
}
|
||||
|
||||
// FailureCloseKeywords are keywords that indicate an issue was closed due to failure.
|
||||
// Used by conditional-blocks dependencies to determine if the condition is met.
|
||||
var FailureCloseKeywords = []string{
|
||||
"failed",
|
||||
"rejected",
|
||||
"wontfix",
|
||||
"won't fix",
|
||||
"cancelled",
|
||||
"canceled",
|
||||
"abandoned",
|
||||
"blocked",
|
||||
"error",
|
||||
"timeout",
|
||||
"aborted",
|
||||
}
|
||||
|
||||
// IsFailureClose returns true if the close reason indicates the issue failed.
|
||||
// This is used by conditional-blocks dependencies: B runs only if A fails.
|
||||
// A "failure" close reason contains one of the FailureCloseKeywords (case-insensitive).
|
||||
func IsFailureClose(closeReason string) bool {
|
||||
if closeReason == "" {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(closeReason)
|
||||
for _, keyword := range FailureCloseKeywords {
|
||||
if strings.Contains(lower, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Label represents a tag on an issue
|
||||
|
||||
@@ -482,6 +482,7 @@ func TestDependencyTypeAffectsReadyWork(t *testing.T) {
|
||||
}{
|
||||
{DepBlocks, true},
|
||||
{DepParentChild, true},
|
||||
{DepConditionalBlocks, true},
|
||||
{DepRelated, false},
|
||||
{DepDiscoveredFrom, false},
|
||||
{DepRepliesTo, false},
|
||||
@@ -503,6 +504,51 @@ func TestDependencyTypeAffectsReadyWork(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFailureClose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
closeReason string
|
||||
isFailure bool
|
||||
}{
|
||||
// Failure keywords
|
||||
{"failed", "Task failed due to timeout", true},
|
||||
{"rejected", "PR was rejected by reviewer", true},
|
||||
{"wontfix", "Closed as wontfix", true},
|
||||
{"won't fix", "Won't fix - by design", true},
|
||||
{"cancelled", "Work cancelled", true},
|
||||
{"canceled", "Work canceled", true},
|
||||
{"abandoned", "Abandoned feature", true},
|
||||
{"blocked", "Blocked by external dependency", true},
|
||||
{"error", "Encountered error during execution", true},
|
||||
{"timeout", "Test timeout exceeded", true},
|
||||
{"aborted", "Build aborted", true},
|
||||
|
||||
// Case insensitive
|
||||
{"FAILED upper", "FAILED", true},
|
||||
{"Failed mixed", "Failed to build", true},
|
||||
|
||||
// Success cases (no failure keywords)
|
||||
{"completed", "Completed successfully", false},
|
||||
{"done", "Done", false},
|
||||
{"merged", "Merged to main", false},
|
||||
{"fixed", "Bug fixed", false},
|
||||
{"implemented", "Feature implemented", false},
|
||||
{"empty", "", false},
|
||||
|
||||
// Partial matches should work
|
||||
{"prefixed", "prefailed", true}, // contains "failed"
|
||||
{"suffixed", "failedtest", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsFailureClose(tt.closeReason); got != tt.isFailure {
|
||||
t.Errorf("IsFailureClose(%q) = %v, want %v", tt.closeReason, got, tt.isFailure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueStructFields(t *testing.T) {
|
||||
// Test that all time fields work correctly
|
||||
now := time.Now()
|
||||
|
||||
Reference in New Issue
Block a user