package opportunities import ( "errors" "strings" "testing" ) // PHASE 2 ITEM 4, the two operations that were missing. Screen 4 requires accept, edit and accept, reject, // SPLIT, MERGE, and add manually. Four existed. These tests cover the pure validation, which is where the // rules that protect the citation live; the database behaviour is proven live against staging. // // THE RULE THAT MATTERS MOST: neither operation may produce an uncited machine item. A split inherits the // parent's citation for every child, and a merge keeps the primary's. If either invented a fresh item with no // source, ck_scope_items_cited would refuse it, and that refusal is the schema catching a bug rather than the // design working. So validation refuses it first, and the constraint stays as the backstop. func TestValidateSplitRequiresAtLeastTwoParts(t *testing.T) { cases := map[string][]SplitPart{ "no parts": {}, "one part": {{Title: "Only one"}}, } for name, parts := range cases { if err := validateSplit(parts); err == nil { t.Errorf("%s: split accepted %d part(s). Splitting into one is an edit, not a split, and it "+ "would retire the parent while changing nothing", name, len(parts)) } else if !errors.Is(err, ErrInvalidInput) { t.Errorf("%s: error = %v, want ErrInvalidInput", name, err) } } } func TestValidateSplitAcceptsTwoOrMore(t *testing.T) { parts := []SplitPart{{Title: "Monitoring of the enclave"}, {Title: "Incident response reporting"}} if err := validateSplit(parts); err != nil { t.Fatalf("a legitimate two way split was refused: %v", err) } } func TestValidateSplitRefusesAnEmptyTitle(t *testing.T) { parts := []SplitPart{{Title: "Real half"}, {Title: " "}} if err := validateSplit(parts); err == nil { t.Fatal("a part with a blank title was accepted, which would create an unreadable scope row") } } func TestValidateSplitRefusesAnUnknownScopeType(t *testing.T) { parts := []SplitPart{{Title: "A", ScopeType: "objective"}, {Title: "B", ScopeType: "not_a_type"}} if err := validateSplit(parts); err == nil { t.Fatal("an out of vocabulary scope_type was accepted; the column CHECK would then refuse the write") } } func TestValidateSplitAllowsAnEmptyScopeTypeMeaningInherit(t *testing.T) { // An empty scope type is not missing data, it means keep the parent's classification. Requiring the // reviewer to restate it on every child would invite them to get it wrong. parts := []SplitPart{{Title: "A"}, {Title: "B"}} if err := validateSplit(parts); err != nil { t.Fatalf("empty scope_type should inherit from the parent, got %v", err) } } func TestValidateSplitRefusesTooManyParts(t *testing.T) { parts := make([]SplitPart, 51) for i := range parts { parts[i] = SplitPart{Title: "part"} } if err := validateSplit(parts); err == nil { t.Fatal("51 parts accepted; an unbounded split is a way to flood the review queue from one item") } } func TestValidateMergeRequiresSomethingToAbsorb(t *testing.T) { if err := validateMerge("11111111-1111-1111-1111-111111111111", nil, "Combined"); err == nil { t.Fatal("a merge with nothing to absorb was accepted, which is an edit wearing a merge's name") } } func TestValidateMergeRefusesMergingAnItemIntoItself(t *testing.T) { id := "11111111-1111-1111-1111-111111111111" if err := validateMerge(id, []string{id}, "Combined"); err == nil { t.Fatal("an item was allowed to absorb itself, which would retire the survivor and lose the item") } } func TestValidateMergeRefusesADuplicateInTheAbsorbList(t *testing.T) { a := "11111111-1111-1111-1111-111111111111" b := "22222222-2222-2222-2222-222222222222" if err := validateMerge(a, []string{b, b}, "Combined"); err == nil { t.Fatal("a duplicated id was accepted; the second write would find the row already retired") } } func TestValidateMergeRefusesANonUUID(t *testing.T) { a := "11111111-1111-1111-1111-111111111111" if err := validateMerge(a, []string{"not-a-uuid"}, "Combined"); err == nil { t.Fatal("a non uuid was accepted into the absorb list") } } func TestValidateMergeRequiresATitle(t *testing.T) { a := "11111111-1111-1111-1111-111111111111" b := "22222222-2222-2222-2222-222222222222" if err := validateMerge(a, []string{b}, " "); err == nil { t.Fatal("a merge with a blank title was accepted; the combined item must say what it now covers") } } func TestValidateMergeAcceptsALegitimateMerge(t *testing.T) { a := "11111111-1111-1111-1111-111111111111" b := "22222222-2222-2222-2222-222222222222" c := "33333333-3333-3333-3333-333333333333" if err := validateMerge(a, []string{b, c}, "Continuous monitoring and reporting"); err != nil { t.Fatalf("a legitimate three way merge was refused: %v", err) } } // The status vocabulary must carry split, and migration 0084 adds it. A test on the map is cheap insurance // against someone reverting the Go side and leaving the column permitting a value the code rejects. func TestSplitIsAValidReviewStatus(t *testing.T) { if !validScopeReviewStatuses["split"] { t.Fatal("split is not an accepted review status in Go, so the API would refuse what the column permits") } for _, want := range []string{"extracted", "accepted", "edited", "rejected", "merged", "manual"} { if !validScopeReviewStatuses[want] { t.Errorf("status %q was dropped from the vocabulary; extend, never remove", want) } } } func TestSplitTitlesAreTrimmedNotRejectedForWhitespace(t *testing.T) { parts := []SplitPart{{Title: " Leading and trailing "}, {Title: "Second"}} if err := validateSplit(parts); err != nil { t.Fatalf("surrounding whitespace should be trimmed, not refused: %v", err) } if strings.TrimSpace(parts[0].Title) != "Leading and trailing" { t.Errorf("unexpected trim result %q", strings.TrimSpace(parts[0].Title)) } }