//go:build integration package integration import ( "context" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/modules/opportunities" "github.com/missionworkforce/missionmatch/internal/platform/audit" ) // SPLIT, MERGE AND DRAFT GENERATION AGAINST A REAL DATABASE. // // THESE EXIST BECAUSE OF FINDING F3, and repeating F3 would be inexcusable. The document upload path was // committed, reviewed, and reported as built while having never once written a row, because every test it had // was pure and none of them touched Postgres. The column list was therefore never executed, and a timestamptz // scanned into a Go string only failed when a real database refused it. // // Split, merge and generation are new code with pure validation tests. Pure tests cannot see a column that // does not exist, a constraint that refuses the write, or a citation that fails to carry. So they run here too. func oppSvc() *opportunities.Service { return opportunities.NewService(appDB, audit.New(appDB)) } // seedScopeFixture builds one opportunity with one cited extracted scope item, through the migrator, and // returns a recruiter context that can review it. func seedScopeFixture(t *testing.T, scopeType, title string) (org, opp, item uuid.UUID, ctx context.Context) { t.Helper() bg := context.Background() org = seedOrg(t) opp, item = uuid.New(), uuid.New() notice := "SR-" + opp.String()[:8] if _, err := migrator.Exec(bg, ` INSERT INTO sam_opportunities (notice_id, title, department, naics_code, raw, source) VALUES ($1, 'Enterprise Operations Support', 'DEPT OF THE AIR FORCE', '541512', '{}'::jsonb, 'test') ON CONFLICT (notice_id) DO NOTHING`, notice); err != nil { t.Fatalf("seed cache: %v", err) } if _, err := migrator.Exec(bg, ` INSERT INTO tracked_opportunities (organization_id, id, sam_notice_id, source_type, title, status) VALUES ($1,$2,$3,'sam_gov','Enterprise Operations Support','qualifying')`, org, opp, notice); err != nil { t.Fatalf("seed tracked: %v", err) } // A CITED extracted item. source_quote_hash is what ck_scope_items_cited accepts for a machine item, and // it is the value every split child must inherit. if _, err := migrator.Exec(bg, ` INSERT INTO opportunity_scope_items (organization_id, id, opportunity_id, scope_type, title, description, source_section_label, source_quote_hash, extraction_method, extraction_confidence, review_status) VALUES ($1,$2,$3,$4,$5,'The contractor shall provide four Cyber Systems Operations specialists holding TS/SCI with a CI polygraph, minimum of 5 years experience.','C.3.1','quote-hash-abc','numbered_clause',1.0,'extracted')`, org, item, opp, scopeType, title); err != nil { t.Fatalf("seed scope item: %v", err) } _, ctx = seedRoleUser(t, org, "Rita Reviewer", "recruiter") t.Cleanup(func() { c := context.Background() _, _ = migrator.Exec(c, `DELETE FROM requirement_drafts WHERE organization_id=$1`, org) _, _ = migrator.Exec(c, `DELETE FROM opportunity_scope_items WHERE organization_id=$1`, org) _, _ = migrator.Exec(c, `DELETE FROM tracked_opportunities WHERE organization_id=$1`, org) _, _ = migrator.Exec(c, `DELETE FROM sam_opportunities WHERE notice_id=$1`, notice) }) return org, opp, item, ctx } func TestSplitWritesChildrenThatInheritTheCitation(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } _, _, item, ctx := seedScopeFixture(t, "task", "Monitoring and reporting") svc := oppSvc() children, err := svc.SplitScopeItem(ctx, item.String(), []opportunities.SplitPart{ {Title: "Continuous monitoring of the enclave"}, {Title: "Incident response reporting", ScopeType: "reporting"}, }) if err != nil { t.Fatalf("split: %v", err) } if len(children) != 2 { t.Fatalf("children = %d, want 2", len(children)) } for _, c := range children { // THE CITATION IS THE WHOLE POINT. A child with no source is a machine item with no provenance, and // ck_scope_items_cited would have refused it, so this asserts the design rather than the constraint. if c.SourceQuoteHash != "quote-hash-abc" { t.Errorf("child %q lost the citation, quote hash = %q", c.Title, c.SourceQuoteHash) } if c.SourceSectionLabel != "C.3.1" { t.Errorf("child %q lost the section label, got %q", c.Title, c.SourceSectionLabel) } if c.ReviewStatus != "edited" { t.Errorf("child %q status = %q, want edited", c.Title, c.ReviewStatus) } } // Scope type inheritance: the first child inherits, the second overrides. if children[0].ScopeType != "task" { t.Errorf("first child scope_type = %q, want the inherited task", children[0].ScopeType) } if children[1].ScopeType != "reporting" { t.Errorf("second child scope_type = %q, want the override reporting", children[1].ScopeType) } // THE PARENT IS RETIRED, NOT DELETED. Migration 0084 added 'split' for exactly this. var status string if err := migrator.QueryRow(context.Background(), `SELECT review_status FROM opportunity_scope_items WHERE id=$1`, item).Scan(&status); err != nil { t.Fatalf("read parent: %v", err) } if status != "split" { t.Errorf("parent status = %q, want split", status) } t.Logf("split wrote %d children, each carrying the parent's citation, parent retired to %q", len(children), status) } func TestSplitRefusesAnAlreadyRetiredItem(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } _, _, item, ctx := seedScopeFixture(t, "task", "Already gone") svc := oppSvc() if _, err := svc.SplitScopeItem(ctx, item.String(), []opportunities.SplitPart{{Title: "a"}, {Title: "b"}}); err != nil { t.Fatalf("first split: %v", err) } // A second split would orphan the first set of children from the queue. if _, err := svc.SplitScopeItem(ctx, item.String(), []opportunities.SplitPart{{Title: "c"}, {Title: "d"}}); err == nil { t.Fatal("splitting an already split item succeeded, which would orphan the first children") } } func TestMergeRetiresTheAbsorbedAndKeepsTheSurvivorsCitation(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } org, opp, primary, ctx := seedScopeFixture(t, "task", "Primary task") svc := oppSvc() // A second cited item to absorb. other := uuid.New() if _, err := migrator.Exec(context.Background(), ` INSERT INTO opportunity_scope_items (organization_id, id, opportunity_id, scope_type, title, source_section_label, source_quote_hash, extraction_method, extraction_confidence, review_status) VALUES ($1,$2,$3,'task','Secondary task','C.3.2','quote-hash-def','numbered_clause',1.0,'extracted')`, org, other, opp); err != nil { t.Fatalf("seed second item: %v", err) } survivor, err := svc.MergeScopeItems(ctx, primary.String(), []string{other.String()}, "Monitoring and reporting combined", "", "") if err != nil { t.Fatalf("merge: %v", err) } if survivor.Title != "Monitoring and reporting combined" { t.Errorf("survivor title = %q", survivor.Title) } if survivor.SourceQuoteHash != "quote-hash-abc" { t.Errorf("survivor lost its own citation, quote hash = %q", survivor.SourceQuoteHash) } if survivor.ReviewStatus != "edited" { t.Errorf("survivor status = %q, want edited", survivor.ReviewStatus) } var absorbedStatus, parentRef string if err := migrator.QueryRow(context.Background(), `SELECT review_status, COALESCE(parent_scope_item_id::text,'') FROM opportunity_scope_items WHERE id=$1`, other).Scan(&absorbedStatus, &parentRef); err != nil { t.Fatalf("read absorbed: %v", err) } if absorbedStatus != "merged" { t.Errorf("absorbed status = %q, want merged", absorbedStatus) } if parentRef != primary.String() { t.Errorf("absorbed parent ref = %q, want the survivor %q", parentRef, primary) } t.Logf("merge kept the survivor's citation and retired the absorbed item to %q pointing at the survivor", absorbedStatus) } func TestMergeRefusesAnItemThatDoesNotExist(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } _, _, primary, ctx := seedScopeFixture(t, "task", "Primary") svc := oppSvc() // A random uuid is not in this tenant, so RLS hides it and the count check must catch the shortfall rather // than silently merging nothing. if _, err := svc.MergeScopeItems(ctx, primary.String(), []string{uuid.New().String()}, "Combined", "", ""); err == nil { t.Fatal("merging a nonexistent item succeeded, so a caller could retire the survivor for nothing") } } func TestGenerateDraftsDerivesFromAcceptedScopeAndIsIdempotent(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } _, opp, item, ctx := seedScopeFixture(t, "labor_category", "Cyber Systems Operations Specialist") svc := oppSvc() // EXTRACTED SCOPE MUST YIELD NOTHING. Generating from unreviewed output would move the machine's own text // one step closer to the engine with no human in between, which is the boundary this phase is built on. created, _, err := svc.GenerateDrafts(ctx, opp.String()) if err != nil { t.Fatalf("generate on extracted scope: %v", err) } if created != 0 { t.Fatalf("generated %d draft(s) from UNREVIEWED scope, which bypasses the human gate", created) } // Accept it, then generate. if _, err := svc.ReviewScopeItem(ctx, item.String(), opportunities.ScopeReview{ReviewStatus: "accepted"}); err != nil { t.Fatalf("accept: %v", err) } created, skipped, err := svc.GenerateDrafts(ctx, opp.String()) if err != nil { t.Fatalf("generate: %v", err) } if created != 1 || skipped != 0 { t.Fatalf("created=%d skipped=%d, want 1 and 0", created, skipped) } // THE DERIVED VALUES came from the item's own text, deterministically. var laborCat, clearance, poly, method, srcItem string var qty, years int if err := migrator.QueryRow(context.Background(), ` SELECT labor_category, clearance_level, polygraph, derivation_method, COALESCE(source_scope_item_id::text,''), quantity, minimum_years_experience FROM requirement_drafts WHERE opportunity_id=$1`, opp). Scan(&laborCat, &clearance, &poly, &method, &srcItem, &qty, &years); err != nil { t.Fatalf("read draft: %v", err) } if laborCat != "Cyber Systems Operations Specialist" { t.Errorf("labor category = %q", laborCat) } if clearance != "ts_sci" { t.Errorf("clearance = %q, want ts_sci read from the scope text", clearance) } if poly != "ci" { t.Errorf("polygraph = %q, want ci", poly) } if qty != 4 { t.Errorf("quantity = %d, want 4", qty) } if years != 5 { t.Errorf("minimum years = %d, want 5", years) } // LINEAGE. This column is what the amendment view follows to say which approved requirement is affected. if srcItem != item.String() { t.Errorf("lineage lost: source_scope_item_id = %q, want %q", srcItem, item) } if method != "deterministic_scope" { t.Errorf("derivation method = %q", method) } // IDEMPOTENCE. A reviewer presses generate after every batch, so a second run must add nothing. created2, skipped2, err := svc.GenerateDrafts(ctx, opp.String()) if err != nil { t.Fatalf("second generate: %v", err) } if created2 != 0 || skipped2 != 1 { t.Fatalf("second run created=%d skipped=%d, want 0 and 1", created2, skipped2) } t.Logf("generated one draft with clearance %s, %d needed, %d years, lineage intact, second run idempotent", clearance, qty, years) } func TestPatchDraftClosesTheGapItReports(t *testing.T) { if appDB == nil { t.Skip("integration pools not configured") } _, opp, item, ctx := seedScopeFixture(t, "labor_category", "Program Analyst") svc := oppSvc() // Overwrite the description so nothing states a clearance, which is the gap case screen 5 exists for. if _, err := migrator.Exec(context.Background(), `UPDATE opportunity_scope_items SET description='Analytical support to the program office.' WHERE id=$1`, item); err != nil { t.Fatalf("blank the description: %v", err) } if _, err := svc.ReviewScopeItem(ctx, item.String(), opportunities.ScopeReview{ReviewStatus: "accepted"}); err != nil { t.Fatalf("accept: %v", err) } if _, _, err := svc.GenerateDrafts(ctx, opp.String()); err != nil { t.Fatalf("generate: %v", err) } var draftID, clearance string if err := migrator.QueryRow(context.Background(), `SELECT id::text, clearance_level FROM requirement_drafts WHERE opportunity_id=$1`, opp). Scan(&draftID, &clearance); err != nil { t.Fatalf("read draft: %v", err) } // The document said nothing, so the draft must carry unknown rather than a guess. if clearance != "unknown" { t.Fatalf("clearance = %q, want unknown when the text never states one", clearance) } secret := "secret" out, err := svc.PatchDraft(ctx, draftID, opportunities.DraftPatch{ClearanceLevel: &secret}) if err != nil { t.Fatalf("patch: %v", err) } if got, _ := out["clearance_level"].(string); got != "secret" { t.Errorf("after the patch clearance = %q, want secret", got) } missing, _ := out["missing_required_fields"].([]string) if len(missing) != 0 { t.Errorf("after supplying the clearance, still missing %v", missing) } t.Logf("the gap was reported as unknown, a human closed it, and the draft now reports %v missing", missing) }