@@ -433,3 +433,217 @@ def test_fails_when_case_not_in_datalayer(
433433 case_actor_participant_id = f"{ actor_id } /case-actor/participant" ,
434434 )
435435 assert result .status == py_trees .common .Status .FAILURE
436+
437+
438+ # ---------------------------------------------------------------------------
439+ # ProposeCaseToActorNode tests
440+ # ---------------------------------------------------------------------------
441+
442+
443+ class TestProposeCaseToActorNode :
444+ """ProposeCaseToActorNode sends Create(as_CaseProposal) to the case-actor."""
445+
446+ from vultron .core .behaviors .case .nodes .actor import (
447+ ProposeCaseToActorNode ,
448+ )
449+
450+ CASE_ACTOR_ID = "https://example.org/actors/case-actor-service"
451+
452+ def test_succeeds_and_queues_proposal_to_outbox (
453+ self ,
454+ bt_scenario : BTTestScenario ,
455+ actor : VultronCaseActor ,
456+ actor_id : str ,
457+ report : VultronReport ,
458+ case_obj : VultronCase ,
459+ ) -> None :
460+ """Happy path: node returns SUCCESS and enqueues a Create activity."""
461+ from vultron .core .behaviors .case .nodes .actor import (
462+ ProposeCaseToActorNode ,
463+ )
464+
465+ outbox_before = list (
466+ bt_scenario .dl .outbox_list_for_actor (actor_id ) or []
467+ )
468+ result = bt_scenario .run (
469+ ProposeCaseToActorNode (),
470+ actor_id = actor_id ,
471+ case_id = case_obj .id_ ,
472+ case_actor_id = self .CASE_ACTOR_ID ,
473+ )
474+ bt_scenario .assert_success (result )
475+
476+ outbox_after = list (
477+ bt_scenario .dl .outbox_list_for_actor (actor_id ) or []
478+ )
479+ assert len (outbox_after ) > len (
480+ outbox_before
481+ ), "ProposeCaseToActorNode must enqueue an activity to the outbox"
482+
483+ def test_persists_create_activity_in_datalayer (
484+ self ,
485+ bt_scenario : BTTestScenario ,
486+ actor : VultronCaseActor ,
487+ actor_id : str ,
488+ report : VultronReport ,
489+ case_obj : VultronCase ,
490+ ) -> None :
491+ """Create(as_CaseProposal) activity is persisted to the DataLayer."""
492+ from vultron .core .behaviors .case .nodes .actor import (
493+ ProposeCaseToActorNode ,
494+ )
495+
496+ create_activities_before = bt_scenario .dl .list_objects ("Create" )
497+ bt_scenario .run (
498+ ProposeCaseToActorNode (),
499+ actor_id = actor_id ,
500+ case_id = case_obj .id_ ,
501+ case_actor_id = self .CASE_ACTOR_ID ,
502+ )
503+
504+ create_activities_after = bt_scenario .dl .list_objects ("Create" )
505+ assert len (create_activities_after ) > len (
506+ create_activities_before
507+ ), "At least one new Create activity should be in the DataLayer"
508+
509+ def test_fails_without_case_id (
510+ self ,
511+ bt_scenario : BTTestScenario ,
512+ actor : VultronCaseActor ,
513+ actor_id : str ,
514+ ) -> None :
515+ """Node returns FAILURE when case_id is missing from the blackboard."""
516+ from vultron .core .behaviors .case .nodes .actor import (
517+ ProposeCaseToActorNode ,
518+ )
519+
520+ result = bt_scenario .run (
521+ ProposeCaseToActorNode (),
522+ actor_id = actor_id ,
523+ case_actor_id = self .CASE_ACTOR_ID ,
524+ # case_id intentionally omitted
525+ )
526+ bt_scenario .assert_failure (result )
527+
528+ def test_fails_without_case_actor_id (
529+ self ,
530+ bt_scenario : BTTestScenario ,
531+ actor : VultronCaseActor ,
532+ actor_id : str ,
533+ case_obj : VultronCase ,
534+ ) -> None :
535+ """Node returns FAILURE when case_actor_id is missing from the blackboard."""
536+ from vultron .core .behaviors .case .nodes .actor import (
537+ ProposeCaseToActorNode ,
538+ )
539+
540+ result = bt_scenario .run (
541+ ProposeCaseToActorNode (),
542+ actor_id = actor_id ,
543+ case_id = case_obj .id_ ,
544+ # case_actor_id intentionally omitted
545+ )
546+ bt_scenario .assert_failure (result )
547+
548+ def test_fails_when_case_has_no_reports (
549+ self ,
550+ bt_scenario : BTTestScenario ,
551+ actor : VultronCaseActor ,
552+ actor_id : str ,
553+ ) -> None :
554+ """Node returns FAILURE when the case has no linked VulnerabilityReport."""
555+ from vultron .core .behaviors .case .nodes .actor import (
556+ ProposeCaseToActorNode ,
557+ )
558+
559+ empty_case = VultronCase (
560+ id_ = "https://example.org/cases/empty-case" ,
561+ name = "Empty Case" ,
562+ vulnerability_reports = [],
563+ )
564+ bt_scenario .dl .create (empty_case )
565+
566+ result = bt_scenario .run (
567+ ProposeCaseToActorNode (),
568+ actor_id = actor_id ,
569+ case_id = empty_case .id_ ,
570+ case_actor_id = self .CASE_ACTOR_ID ,
571+ )
572+ bt_scenario .assert_failure (result )
573+
574+ def test_fails_when_report_not_in_datalayer (
575+ self ,
576+ bt_scenario : BTTestScenario ,
577+ actor : VultronCaseActor ,
578+ actor_id : str ,
579+ ) -> None :
580+ """Node returns FAILURE when the linked report is absent from DataLayer."""
581+ from vultron .core .behaviors .case .nodes .actor import (
582+ ProposeCaseToActorNode ,
583+ )
584+
585+ dangling_case = VultronCase (
586+ id_ = "https://example.org/cases/dangling" ,
587+ name = "Dangling Case" ,
588+ vulnerability_reports = ["https://example.org/reports/ghost" ],
589+ )
590+ bt_scenario .dl .create (dangling_case )
591+
592+ result = bt_scenario .run (
593+ ProposeCaseToActorNode (),
594+ actor_id = actor_id ,
595+ case_id = dangling_case .id_ ,
596+ case_actor_id = self .CASE_ACTOR_ID ,
597+ )
598+ bt_scenario .assert_failure (result )
599+
600+ def test_fails_when_case_not_in_datalayer (
601+ self ,
602+ bt_scenario : BTTestScenario ,
603+ actor : VultronCaseActor ,
604+ actor_id : str ,
605+ ) -> None :
606+ """Node returns FAILURE when the case record is absent from DataLayer."""
607+ from vultron .core .behaviors .case .nodes .actor import (
608+ ProposeCaseToActorNode ,
609+ )
610+
611+ result = bt_scenario .run (
612+ ProposeCaseToActorNode (),
613+ actor_id = actor_id ,
614+ case_id = "https://example.org/cases/nonexistent" ,
615+ case_actor_id = self .CASE_ACTOR_ID ,
616+ )
617+ bt_scenario .assert_failure (result )
618+
619+ def test_fails_when_no_trigger_activity_factory (
620+ self ,
621+ actor : VultronCaseActor ,
622+ actor_id : str ,
623+ report : VultronReport ,
624+ case_obj : VultronCase ,
625+ ) -> None :
626+ """Node returns FAILURE when trigger_activity_factory is absent."""
627+ import py_trees
628+
629+ from vultron .adapters .driven .datalayer_sqlite import SqliteDataLayer
630+ from vultron .core .behaviors .bridge import BTBridge
631+ from vultron .core .behaviors .case .nodes .actor import (
632+ ProposeCaseToActorNode ,
633+ )
634+
635+ # Build a bridge with NO trigger_activity_factory injected.
636+ dl = SqliteDataLayer ("sqlite:///:memory:" )
637+ dl .create (actor )
638+ dl .create (report )
639+ dl .create (case_obj )
640+ bridge_no_factory = BTBridge (datalayer = dl )
641+
642+ py_trees .blackboard .Blackboard .storage .clear ()
643+ result = bridge_no_factory .execute_with_setup (
644+ tree = ProposeCaseToActorNode (),
645+ actor_id = actor_id ,
646+ case_id = case_obj .id_ ,
647+ case_actor_id = self .CASE_ACTOR_ID ,
648+ )
649+ assert result .status == py_trees .common .Status .FAILURE
0 commit comments