@@ -743,3 +743,151 @@ describe('File Pattern Restrictions', () => {
743743 } ) ;
744744 }
745745} ) ;
746+
747+ describe ( 'WORKFLOWS=off environment variable' , ( ) => {
748+ it ( 'registers tools when WORKFLOWS=off, but execute throws a clear disabled error' , async ( ) => {
749+ const dir = createTempDir ( ) ;
750+ const originalEnv = process . env . WORKFLOWS ;
751+ try {
752+ process . env . WORKFLOWS = 'off' ;
753+
754+ const hooks = await WorkflowsPlugin ( createMockPluginInput ( dir ) ) ;
755+
756+ // Tools are still registered (so /workflow on can re-enable them)
757+ expect ( hooks . tool ) . toBeDefined ( ) ;
758+ expect ( hooks . tool ) . toHaveProperty ( 'start_development' ) ;
759+ expect ( hooks . tool ) . toHaveProperty ( 'proceed_to_phase' ) ;
760+ expect ( hooks . tool ) . toHaveProperty ( 'conduct_review' ) ;
761+ expect ( hooks . tool ) . toHaveProperty ( 'reset_development' ) ;
762+ expect ( hooks . tool ) . toHaveProperty ( 'setup_project_docs' ) ;
763+
764+ // But executing a tool throws with a clear message
765+ await expect (
766+ hooks . tool ! [ 'start_development' ] . execute ( { workflow : 'minor' } , {
767+ sessionID : 'test-session' ,
768+ } as unknown )
769+ ) . rejects . toThrow ( / d i s a b l e d / i) ;
770+
771+ // Command hook is available for toggling
772+ expect ( hooks [ 'command.execute.before' ] ) . toBeDefined ( ) ;
773+ } finally {
774+ if ( originalEnv === undefined ) {
775+ delete process . env . WORKFLOWS ;
776+ } else {
777+ process . env . WORKFLOWS = originalEnv ;
778+ }
779+ cleanupDir ( dir ) ;
780+ }
781+ } ) ;
782+
783+ it ( 'allows tool execution after /wf on when started with WORKFLOWS=off' , async ( ) => {
784+ const dir = createTempDir ( ) ;
785+ const originalEnv = process . env . WORKFLOWS ;
786+ try {
787+ process . env . WORKFLOWS = 'off' ;
788+
789+ const hooks = await WorkflowsPlugin ( createMockPluginInput ( dir ) ) ;
790+
791+ // Confirm disabled initially
792+ await expect (
793+ hooks . tool ! [ 'start_development' ] . execute ( { workflow : 'minor' } , {
794+ sessionID : 'test-session' ,
795+ } as unknown )
796+ ) . rejects . toThrow ( / d i s a b l e d / i) ;
797+
798+ // Toggle on via command
799+ const output : { parts : Part [ ] } = { parts : [ ] } ;
800+ await hooks [ 'command.execute.before' ] ! (
801+ { command : 'workflow' , arguments : 'on' , sessionID : 'test-session' } ,
802+ output
803+ ) ;
804+ expect (
805+ output . parts [ 0 ] ?. type === 'text' && output . parts [ 0 ] . text
806+ ) . toContain ( 'enabled' ) ;
807+
808+ // Now the tool should no longer throw the disabled error
809+ // (it may fail for other reasons like no plan file, but not the disabled guard)
810+ let thrownMessage : string | undefined ;
811+ try {
812+ await hooks . tool ! [ 'start_development' ] . execute ( { workflow : 'minor' } , {
813+ sessionID : 'test-session' ,
814+ } as unknown ) ;
815+ } catch ( err ) {
816+ thrownMessage = ( err as Error ) . message ;
817+ }
818+ // If it did throw, it must NOT be the disabled message
819+ if ( thrownMessage !== undefined ) {
820+ expect ( thrownMessage ) . not . toMatch ( / d i s a b l e d / i) ;
821+ }
822+ } finally {
823+ if ( originalEnv === undefined ) {
824+ delete process . env . WORKFLOWS ;
825+ } else {
826+ process . env . WORKFLOWS = originalEnv ;
827+ }
828+ cleanupDir ( dir ) ;
829+ }
830+ } ) ;
831+
832+ it ( 'loads all tools and hooks when WORKFLOWS is not set (default)' , async ( ) => {
833+ const dir = createTempDir ( ) ;
834+ const originalEnv = process . env . WORKFLOWS ;
835+ try {
836+ delete process . env . WORKFLOWS ;
837+
838+ const hooks = await WorkflowsPlugin ( createMockPluginInput ( dir ) ) ;
839+
840+ // When WORKFLOWS is not set, all hooks and tools should be registered
841+ expect ( hooks [ 'chat.message' ] ) . toBeDefined ( ) ;
842+ expect ( hooks [ 'tool.execute.before' ] ) . toBeDefined ( ) ;
843+ expect ( hooks [ 'experimental.session.compacting' ] ) . toBeDefined ( ) ;
844+ expect ( hooks [ 'command.execute.before' ] ) . toBeDefined ( ) ;
845+ expect ( hooks . tool ) . toBeDefined ( ) ;
846+
847+ // Tools should be populated
848+ expect ( hooks . tool ) . toHaveProperty ( 'start_development' ) ;
849+ expect ( hooks . tool ) . toHaveProperty ( 'proceed_to_phase' ) ;
850+ expect ( hooks . tool ) . toHaveProperty ( 'conduct_review' ) ;
851+ expect ( hooks . tool ) . toHaveProperty ( 'reset_development' ) ;
852+ expect ( hooks . tool ) . toHaveProperty ( 'setup_project_docs' ) ;
853+ } finally {
854+ if ( originalEnv === undefined ) {
855+ delete process . env . WORKFLOWS ;
856+ } else {
857+ process . env . WORKFLOWS = originalEnv ;
858+ }
859+ cleanupDir ( dir ) ;
860+ }
861+ } ) ;
862+
863+ it ( 'loads all tools and hooks when WORKFLOWS=on' , async ( ) => {
864+ const dir = createTempDir ( ) ;
865+ const originalEnv = process . env . WORKFLOWS ;
866+ try {
867+ process . env . WORKFLOWS = 'on' ;
868+
869+ const hooks = await WorkflowsPlugin ( createMockPluginInput ( dir ) ) ;
870+
871+ // When WORKFLOWS=on, all hooks and tools should be registered
872+ expect ( hooks [ 'chat.message' ] ) . toBeDefined ( ) ;
873+ expect ( hooks [ 'tool.execute.before' ] ) . toBeDefined ( ) ;
874+ expect ( hooks [ 'experimental.session.compacting' ] ) . toBeDefined ( ) ;
875+ expect ( hooks [ 'command.execute.before' ] ) . toBeDefined ( ) ;
876+ expect ( hooks . tool ) . toBeDefined ( ) ;
877+
878+ // Tools should be populated
879+ expect ( hooks . tool ) . toHaveProperty ( 'start_development' ) ;
880+ expect ( hooks . tool ) . toHaveProperty ( 'proceed_to_phase' ) ;
881+ expect ( hooks . tool ) . toHaveProperty ( 'conduct_review' ) ;
882+ expect ( hooks . tool ) . toHaveProperty ( 'reset_development' ) ;
883+ expect ( hooks . tool ) . toHaveProperty ( 'setup_project_docs' ) ;
884+ } finally {
885+ if ( originalEnv === undefined ) {
886+ delete process . env . WORKFLOWS ;
887+ } else {
888+ process . env . WORKFLOWS = originalEnv ;
889+ }
890+ cleanupDir ( dir ) ;
891+ }
892+ } ) ;
893+ } ) ;
0 commit comments