@@ -761,3 +761,335 @@ describe("orchestrate(Checkpoint)", () => {
761761 expect ( report . summary . total_tasks ) . toBe ( 1 ) ;
762762 } ) ;
763763} ) ;
764+
765+ // ============================================================
766+ // DEC-011: Stigmergic Coordination — ActivationPredicate 評估
767+ // [Source] specs/DEC-011-stigmergic-coordination.md
768+ // ============================================================
769+
770+ describe ( "DEC-011: ActivationPredicate 評估" , ( ) => {
771+ describe ( "[AC-011-007] threshold 類型評估" , ( ) => {
772+ it ( "[Source] 條件不滿足時 task 被 skip" , async ( ) => {
773+ const adapter = createMockAdapter ( {
774+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => ( {
775+ task_id : task . id ,
776+ status : "success" ,
777+ cost_usd : 0.1 ,
778+ // T-001 回傳 metrics,fail_rate = 0.1(不超過 0.3)
779+ metrics : { fail_rate : 0.1 } ,
780+ } ) ) ,
781+ } ) ;
782+
783+ const plan : TaskPlan = {
784+ project : "test" ,
785+ tasks : [
786+ { id : "T-001" , title : "Run tests" , spec : "run all tests" } ,
787+ {
788+ id : "T-002" ,
789+ title : "Refactor" ,
790+ spec : "refactor if fail rate high" ,
791+ depends_on : [ "T-001" ] ,
792+ activationPredicate : {
793+ type : "threshold" ,
794+ metric : "fail_rate" ,
795+ operator : ">" ,
796+ value : 0.3 ,
797+ description : "失敗率超過 30% 才觸發重構" ,
798+ } ,
799+ } ,
800+ ] ,
801+ } ;
802+
803+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
804+
805+ expect ( report . tasks [ 1 ] . status ) . toBe ( "skipped" ) ;
806+ expect ( report . tasks [ 1 ] . error ) . toContain ( "activation predicate not met" ) ;
807+ expect ( report . tasks [ 1 ] . error ) . toContain ( "失敗率超過 30% 才觸發重構" ) ;
808+ // adapter 只被呼叫一次(T-001),T-002 被 skip 不執行
809+ expect ( adapter . executeTask ) . toHaveBeenCalledTimes ( 1 ) ;
810+ } ) ;
811+
812+ it ( "[Source] 條件滿足時 task 正常執行" , async ( ) => {
813+ let callCount = 0 ;
814+ const adapter = createMockAdapter ( {
815+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => {
816+ callCount ++ ;
817+ if ( task . id === "T-001" ) {
818+ return {
819+ task_id : task . id ,
820+ status : "success" ,
821+ cost_usd : 0.1 ,
822+ metrics : { fail_rate : 0.5 } , // 超過 0.3
823+ } ;
824+ }
825+ return { task_id : task . id , status : "success" , cost_usd : 0.1 } ;
826+ } ) ,
827+ } ) ;
828+
829+ const plan : TaskPlan = {
830+ project : "test" ,
831+ tasks : [
832+ { id : "T-001" , title : "Run tests" , spec : "run all tests" } ,
833+ {
834+ id : "T-002" ,
835+ title : "Refactor" ,
836+ spec : "refactor" ,
837+ depends_on : [ "T-001" ] ,
838+ activationPredicate : {
839+ type : "threshold" ,
840+ metric : "fail_rate" ,
841+ operator : ">" ,
842+ value : 0.3 ,
843+ description : "失敗率超過 30%" ,
844+ } ,
845+ } ,
846+ ] ,
847+ } ;
848+
849+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
850+
851+ expect ( report . tasks [ 1 ] . status ) . toBe ( "success" ) ;
852+ expect ( adapter . executeTask ) . toHaveBeenCalledTimes ( 2 ) ;
853+ } ) ;
854+
855+ it ( "[Derived] 前置任務無 metrics 時條件不滿足 → skip" , async ( ) => {
856+ const adapter = createMockAdapter ( {
857+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => ( {
858+ task_id : task . id ,
859+ status : "success" ,
860+ cost_usd : 0.1 ,
861+ // 無 metrics 欄位
862+ } ) ) ,
863+ } ) ;
864+
865+ const plan : TaskPlan = {
866+ project : "test" ,
867+ tasks : [
868+ { id : "T-001" , title : "A" , spec : "X" } ,
869+ {
870+ id : "T-002" ,
871+ title : "B" ,
872+ spec : "Y" ,
873+ depends_on : [ "T-001" ] ,
874+ activationPredicate : {
875+ type : "threshold" ,
876+ metric : "fail_rate" ,
877+ operator : ">" ,
878+ value : 0.3 ,
879+ description : "需要 fail_rate 度量" ,
880+ } ,
881+ } ,
882+ ] ,
883+ } ;
884+
885+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
886+
887+ expect ( report . tasks [ 1 ] . status ) . toBe ( "skipped" ) ;
888+ expect ( report . tasks [ 1 ] . error ) . toContain ( "activation predicate not met" ) ;
889+ } ) ;
890+ } ) ;
891+
892+ describe ( "[AC-011-008] state_flag 類型評估" , ( ) => {
893+ it ( "[Source] 條件不滿足時 task 被 skip" , async ( ) => {
894+ const adapter = createMockAdapter ( {
895+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => ( {
896+ task_id : task . id ,
897+ status : "success" , // T-001 是 success,不是 failed
898+ cost_usd : 0.1 ,
899+ } ) ) ,
900+ } ) ;
901+
902+ const plan : TaskPlan = {
903+ project : "test" ,
904+ tasks : [
905+ { id : "T-001" , title : "Run tests" , spec : "run tests" } ,
906+ {
907+ id : "T-002" ,
908+ title : "Fix" ,
909+ spec : "fix if failed" ,
910+ depends_on : [ "T-001" ] ,
911+ activationPredicate : {
912+ type : "state_flag" ,
913+ taskId : "T-001" ,
914+ expectedStatus : "failed" ,
915+ description : "T-001 失敗時才執行修復" ,
916+ } ,
917+ } ,
918+ ] ,
919+ } ;
920+
921+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
922+
923+ expect ( report . tasks [ 1 ] . status ) . toBe ( "skipped" ) ;
924+ } ) ;
925+
926+ it ( "[Source] 條件滿足時 task 正常執行" , async ( ) => {
927+ const adapter = createMockAdapter ( {
928+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => {
929+ if ( task . id === "T-001" ) {
930+ return { task_id : task . id , status : "done_with_concerns" , cost_usd : 0.1 , concerns : [ "perf" ] } ;
931+ }
932+ return { task_id : task . id , status : "success" , cost_usd : 0.1 } ;
933+ } ) ,
934+ } ) ;
935+
936+ const plan : TaskPlan = {
937+ project : "test" ,
938+ tasks : [
939+ { id : "T-001" , title : "A" , spec : "X" } ,
940+ {
941+ id : "T-002" ,
942+ title : "Review concerns" ,
943+ spec : "review" ,
944+ depends_on : [ "T-001" ] ,
945+ activationPredicate : {
946+ type : "state_flag" ,
947+ taskId : "T-001" ,
948+ expectedStatus : "done_with_concerns" ,
949+ description : "T-001 有疑慮時才審查" ,
950+ } ,
951+ } ,
952+ ] ,
953+ } ;
954+
955+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
956+
957+ expect ( report . tasks [ 1 ] . status ) . toBe ( "success" ) ;
958+ expect ( adapter . executeTask ) . toHaveBeenCalledTimes ( 2 ) ;
959+ } ) ;
960+ } ) ;
961+
962+ describe ( "[AC-011-009] custom 類型評估" , ( ) => {
963+ it ( "[Source] 指令回傳非零時 task 被 skip" , async ( ) => {
964+ const adapter = createMockAdapter ( ) ;
965+
966+ const plan : TaskPlan = {
967+ project : "test" ,
968+ tasks : [
969+ { id : "T-001" , title : "A" , spec : "X" } ,
970+ {
971+ id : "T-002" ,
972+ title : "Conditional" ,
973+ spec : "Y" ,
974+ depends_on : [ "T-001" ] ,
975+ activationPredicate : {
976+ type : "custom" ,
977+ command : "test -f nonexistent_file_that_does_not_exist" ,
978+ description : "檔案存在時才執行" ,
979+ } ,
980+ } ,
981+ ] ,
982+ } ;
983+
984+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
985+
986+ expect ( report . tasks [ 1 ] . status ) . toBe ( "skipped" ) ;
987+ expect ( report . tasks [ 1 ] . error ) . toContain ( "activation predicate not met" ) ;
988+ } ) ;
989+
990+ it ( "[Source] 指令回傳零時 task 正常執行" , async ( ) => {
991+ const adapter = createMockAdapter ( ) ;
992+
993+ const plan : TaskPlan = {
994+ project : "test" ,
995+ tasks : [
996+ { id : "T-001" , title : "A" , spec : "X" } ,
997+ {
998+ id : "T-002" ,
999+ title : "Conditional" ,
1000+ spec : "Y" ,
1001+ depends_on : [ "T-001" ] ,
1002+ activationPredicate : {
1003+ type : "custom" ,
1004+ command : "true" , // 永遠回傳 0
1005+ description : "永遠通過" ,
1006+ } ,
1007+ } ,
1008+ ] ,
1009+ } ;
1010+
1011+ // 使用實際存在的目錄(custom command 需要 cwd 存在)
1012+ const report = await orchestrate ( plan , adapter , { cwd : "/tmp" } ) ;
1013+
1014+ expect ( report . tasks [ 1 ] . status ) . toBe ( "success" ) ;
1015+ expect ( adapter . executeTask ) . toHaveBeenCalledTimes ( 2 ) ;
1016+ } ) ;
1017+ } ) ;
1018+
1019+ describe ( "[AC-011-010] 向後相容" , ( ) => {
1020+ it ( "[Source] 無 activationPredicate 時行為不變" , async ( ) => {
1021+ const adapter = createMockAdapter ( ) ;
1022+ const report = await orchestrate ( simplePlan , adapter , defaultOptions ) ;
1023+
1024+ expect ( report . summary . total_tasks ) . toBe ( 3 ) ;
1025+ expect ( report . summary . succeeded ) . toBe ( 3 ) ;
1026+ expect ( report . summary . skipped ) . toBe ( 0 ) ;
1027+ } ) ;
1028+
1029+ it ( "[Derived] 並行模式無 activationPredicate 時行為不變" , async ( ) => {
1030+ const adapter = createMockAdapter ( ) ;
1031+ const report = await orchestrate ( simplePlan , adapter , {
1032+ ...defaultOptions ,
1033+ parallel : true ,
1034+ } ) ;
1035+
1036+ expect ( report . summary . total_tasks ) . toBe ( 3 ) ;
1037+ expect ( report . summary . succeeded ) . toBe ( 3 ) ;
1038+ } ) ;
1039+ } ) ;
1040+
1041+ describe ( "[AC-011-011] TaskResult.metrics 欄位" , ( ) => {
1042+ it ( "[Source] adapter 回傳的 metrics 應保留在 TaskResult 中" , async ( ) => {
1043+ const adapter = createMockAdapter ( {
1044+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => ( {
1045+ task_id : task . id ,
1046+ status : "success" ,
1047+ cost_usd : 0.1 ,
1048+ metrics : { test_coverage : 0.85 , fail_rate : 0.05 } ,
1049+ } ) ) ,
1050+ } ) ;
1051+
1052+ const plan : TaskPlan = {
1053+ project : "test" ,
1054+ tasks : [ { id : "T-001" , title : "A" , spec : "X" } ] ,
1055+ } ;
1056+
1057+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
1058+
1059+ expect ( report . tasks [ 0 ] . metrics ) . toBeDefined ( ) ;
1060+ expect ( report . tasks [ 0 ] . metrics ! . test_coverage ) . toBe ( 0.85 ) ;
1061+ expect ( report . tasks [ 0 ] . metrics ! . fail_rate ) . toBe ( 0.05 ) ;
1062+ } ) ;
1063+
1064+ it ( "[Source] 無 metrics 時欄位為 undefined" , async ( ) => {
1065+ const adapter = createMockAdapter ( ) ;
1066+
1067+ const plan : TaskPlan = {
1068+ project : "test" ,
1069+ tasks : [ { id : "T-001" , title : "A" , spec : "X" } ] ,
1070+ } ;
1071+
1072+ const report = await orchestrate ( plan , adapter , defaultOptions ) ;
1073+
1074+ expect ( report . tasks [ 0 ] . metrics ) . toBeUndefined ( ) ;
1075+ } ) ;
1076+ } ) ;
1077+
1078+ describe ( "[AC-011-014] 既有測試零回歸" , ( ) => {
1079+ it ( "[Derived] 依賴失敗仍然 skip(不受 predicate 影響)" , async ( ) => {
1080+ const adapter = createMockAdapter ( {
1081+ executeTask : vi . fn ( async ( task : Task ) : Promise < TaskResult > => {
1082+ if ( task . id === "T-001" ) {
1083+ return { task_id : task . id , status : "failed" , error : "compile error" } ;
1084+ }
1085+ return { task_id : task . id , status : "success" } ;
1086+ } ) ,
1087+ } ) ;
1088+
1089+ const report = await orchestrate ( simplePlan , adapter , defaultOptions ) ;
1090+
1091+ expect ( report . summary . failed ) . toBe ( 1 ) ;
1092+ expect ( report . summary . skipped ) . toBe ( 2 ) ;
1093+ } ) ;
1094+ } ) ;
1095+ } ) ;
0 commit comments