import json import os import subprocess import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] GO_RUNTIME_TEST = r''' package automation import ( "context" "encoding/json" "os" "path/filepath" "reflect" "testing" kernelconnector "github.com/nexuscrm/kernel/pkg/connector" "github.com/nexuscrm/kernel/pkg/formula" "github.com/nexuscrm/shared/pkg/models" connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector" ) func TestL2HubSpotVerticalDependencyThroughAuthoredFlow(t *testing.T) { root := filepath.Join(os.Getenv("L2_VERTICAL_CONTEXT_ROOT"), "packs/PingCAP-HubSpot-Companion") raw, err := os.ReadFile(filepath.Join(root,"flows/HubSpot_Contact_Lead_Upsert.flow.json")) if err != nil { t.Fatal(err) } var flow models.FlowMetadata if err := json.Unmarshal(raw,&flow); err != nil { t.Fatal(err) } assignment := func(name string) *models.FlowAssignment { t.Helper() for i := range flow.Assignments { if flow.Assignments[i].Name == name { return &flow.Assignments[i] } } t.Fatalf("assignment %s missing",name); return nil } action := func(name string) *models.FlowActionCall { t.Helper() for i := range flow.ActionCalls { if flow.ActionCalls[i].Name == name { return &flow.ActionCalls[i] } } t.Fatalf("action %s missing",name); return nil } var mappings []connector.ConnectorFieldMappingInput for _, name := range []string{"contact_lead_inbound.json","contact_inbound.json"} { raw,err := os.ReadFile(filepath.Join(root,"field_mappings",name)); if err!=nil {t.Fatal(err)} rows,err := kernelconnector.ParseConnectorFieldMappingInputsFromPayload(raw,"pingcap-hubspot-companion"); if err!=nil {t.Fatal(err)} mappings=append(mappings,rows...) } for _, tc := range []struct{name,property,value,assignment,action string; missingParent,coalesced bool; want models.SObject}{ {name:"single Vertical delta",property:"salesforce_industry__vertical_",value:"Payment",assignment:"UseDeltaSalesforceIndustryVerticalForMappings",action:"ApplyMappingsToCreatedLead",want:models.SObject{"Vertical__c":"Payment"}}, {name:"coalesced child-only Lead delta",property:"salesforce_industry__vertical_",value:"Payment",assignment:"UseCoalescedPropertyChangesForMappings",action:"ApplyCoalescedMappingsToCreatedLead",coalesced:true,want:models.SObject{"Vertical__c":"Payment"}}, {name:"single missing parent remains skipped",property:"salesforce_industry__vertical_",value:"Payment",assignment:"UseDeltaSalesforceIndustryVerticalForMappings",action:"ApplyMappingsToCreatedLead",missingParent:true}, {name:"coalesced missing parent remains skipped",property:"salesforce_industry__vertical_",value:"Payment",assignment:"UseCoalescedPropertyChangesForMappings",action:"ApplyCoalescedMappingsToCreatedLead",coalesced:true,missingParent:true}, {name:"unrelated Lead delta ignores hydrated owner and lifecycle",property:"firstname",value:"Observed",assignment:"UseCoalescedPropertyChangesForMappings",action:"ApplyCoalescedMappingsToCreatedLead",coalesced:true,want:models.SObject{"FirstName":"Observed"}}, {name:"matched Contact coalesced action remains sparse",property:"firstname",value:"Observed",assignment:"UseCoalescedPropertyChangesForMappings",action:"ApplyCoalescedMappingsToMatchedContact",coalesced:true,want:models.SObject{"FirstName":"Observed"}}, } { t.Run(tc.name,func(t *testing.T){ record:=models.SObject{"salesforce_industry":"FSI-FinTech","salesforce_industry__vertical_":"stale", "firstname":"Snapshot", "hubspot_owner_id":"snapshot-owner", "lifecyclestage":"customer", "lead_status":"Suspect"} if tc.missingParent { delete(record,"salesforce_industry") } before,_:=json.Marshal(record) payload:=models.SObject{"propertyName":tc.property,"propertyValue":tc.value,"occurredAt":int64(300)} interview:=&models.FlowInterview{Id:"vertical-context",ContextData:map[string]interface{}{"record":record,"payload":payload,"mappingSourceRecord":models.SObject{},"leadId":"lead-current","matchedContactId":"contact-current"}} if tc.coalesced {interview.ContextData["coalescedPayloads"]=[]interface{}{models.SObject{"payload":payload}}} capture:=&captureUpdate{} exec:=&FlowExecutor{formulaEngine:formula.NewEngine(),persistence:&stubPersistenceCapture{capture:capture}} exec.connectorMappingResolverFn=func(_ context.Context,q connector.ResolveQuery)([]connector.ConnectorFieldMappingInput,error){ var selected []connector.ConnectorFieldMappingInput for _,row:=range mappings {if !row.ProjectionOnly && row.Source==q.Source && row.SourceObject==q.SourceObject && row.TargetObject==q.TargetObject && row.ActionType==q.ActionType && row.Direction==q.Direction {selected=append(selected,row)}} if len(selected)==0 {t.Fatalf("mapping tuple missing: %#v",q)} if !tc.coalesced && (len(selected)!=1 || selected[0].TargetField!="Vertical__c") {t.Fatalf("single delta expanded target group: %#v",selected)} return selected,nil } ctx:=connectorMappingContext("pingcap-hubspot-companion") if _,err:=exec.executeAssignment(ctx,interview,&flow,assignment(tc.assignment),nil);err!=nil {t.Fatal(err)} selectedAction:=action(tc.action) if tc.coalesced { contextInputs:=0 for _,p:=range selectedAction.InputParameters {if p.Name=="transformSourceRecord" {contextInputs++; if !reflect.DeepEqual(p.Value,map[string]interface{}{"elementReference":"record"}) {t.Fatalf("context must use the existing hydrated record, got %#v",p.Value)}}} if contextInputs!=1 {t.Fatal("coalesced action lacks one explicit hydrated transform input")} } if _,err:=exec.executeApplyConnectorFieldMappings(ctx,interview,selectedAction,nil);err!=nil {t.Fatal(err)} if len(tc.want)==0 {if len(capture.calls)!=0 {t.Fatalf("missing dependency wrote %#v",capture.calls)}} else if len(capture.calls)!=1 || !reflect.DeepEqual(capture.calls[0].updates,tc.want) {t.Fatalf("writes=%#v want only %#v",capture.calls,tc.want)} after,_:=json.Marshal(record);if string(before)!=string(after){t.Fatal("flow changed its hydrated input")} }) } } ''' class HubSpotVerticalTransformRuntimeTest(unittest.TestCase): def test_authored_flow_executes_with_native_mapping_engine(self): upstream_value = os.environ.get("NEXUSCRM_ROOT") self.assertTrue(upstream_value, "NEXUSCRM_ROOT must identify the Core candidate") upstream = Path(upstream_value).resolve() package_dir = upstream / "nexuscrm-automation" self.assertTrue(package_dir.is_dir()) expected_head = os.environ.get("NEXUSCRM_EXPECTED_HEAD") if expected_head: actual_head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=upstream, text=True).strip() self.assertEqual(actual_head, expected_head) with tempfile.TemporaryDirectory(prefix="l2-vertical-context-") as temporary: temporary = Path(temporary) source = temporary / "l2_vertical_context_test.go" source.write_text(GO_RUNTIME_TEST) overlay = temporary / "overlay.json" overlay.write_text(json.dumps({"Replace": {str(package_dir / "l2_vertical_context_test.go"): str(source)}})) completed = subprocess.run( ["go", "test", f"-overlay={overlay}", ".", "-run", "^TestL2HubSpotVerticalDependencyThroughAuthoredFlow$", "-count=1", "-timeout=60s"], cwd=package_dir, env={**os.environ, "GOWORK": "off", "L2_VERTICAL_CONTEXT_ROOT": str(REPO_ROOT)}, capture_output=True, text=True, check=False, timeout=180, ) self.assertEqual(completed.returncode, 0, f"go test failed\n{completed.stdout}\n{completed.stderr}") if __name__ == "__main__": unittest.main()