pms-infra-cmdrun-0.1.1.0: test/PMS/Infra/CmdRun/App/ControlSpec.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module PMS.Infra.CmdRun.App.ControlSpec (spec) where
import Test.Hspec
import Control.Concurrent.Async
import qualified Control.Concurrent.STM as STM
import Control.Lens
import Data.Default
import Data.Aeson (encode)
import System.Directory (getCurrentDirectory)
import System.FilePath ((</>))
import qualified PMS.Domain.Model.DM.Type as DM
import qualified PMS.Infra.CmdRun.App.Control as SUT
import qualified PMS.Infra.CmdRun.DM.Type as SUT
-- |
--
data SpecContext = SpecContext {
_domainDataSpecContext :: DM.DomainData
, _appDataSpecContext :: SUT.AppData
}
makeLenses ''SpecContext
defaultSpecContext :: IO SpecContext
defaultSpecContext = do
domDat <- DM.defaultDomainData
appDat <- SUT.defaultAppData
return SpecContext {
_domainDataSpecContext = domDat
, _appDataSpecContext = appDat
}
-- |
--
spec :: Spec
spec = do
runIO $ putStrLn "Start Spec."
beforeAll setUpOnce $
afterAll tearDownOnce .
beforeWith setUp .
after tearDown $ run
-- |
--
setUpOnce :: IO SpecContext
setUpOnce = do
putStrLn "[INFO] EXECUTED ONLY ONCE BEFORE ALL TESTS START."
defaultSpecContext
-- |
--
tearDownOnce :: SpecContext -> IO ()
tearDownOnce _ = do
putStrLn "[INFO] EXECUTED ONLY ONCE AFTER ALL TESTS FINISH."
-- |
--
setUp :: SpecContext -> IO SpecContext
setUp ctx = do
putStrLn "[INFO] EXECUTED BEFORE EACH TEST STARTS."
domDat <- DM.defaultDomainData
appDat <- SUT.defaultAppData
return ctx {
_domainDataSpecContext = domDat
, _appDataSpecContext = appDat
}
-- |
--
tearDown :: SpecContext -> IO ()
tearDown _ = do
putStrLn "[INFO] EXECUTED AFTER EACH TEST FINISHES."
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
-- | Script directory used by TC-02 to TC-04.
-- Uses an absolute path so shells do not interpret the leading "test" segment
-- as the POSIX test command.
testScriptsDir :: IO String
testScriptsDir = do
cwd <- getCurrentDirectory
return $ cwd </> "test" </> "scripts"
-- | Build a RawJsonByteString that wraps StringToolParams.
mkArgs :: String -> DM.RawJsonByteString
mkArgs s = DM.RawJsonByteString $ encode
(SUT.StringToolParams { SUT._argumentsStringToolParams = s })
-- | Send a DefaultCmdRunCommand and wait for one MCP response.
sendCmdRun :: DM.DomainData -> SUT.AppData -> String -> String -> IO DM.McpToolsCallResponseData
sendCmdRun domDat appDat name argsStr = do
let cmdQ = domDat^.DM.cmdRunQueueDomainData
resQ = domDat^.DM.responseQueueDomainData
jsonR = def { DM._methodJsonRpcRequest = name }
dat = DM.DefaultCmdRunCommandData jsonR name (mkArgs argsStr)
cmd = DM.DefaultCmdRunCommand dat
thId <- async $ SUT.runWithAppData appDat domDat
STM.atomically $ STM.writeTQueue cmdQ cmd
(DM.McpToolsCallResponse res) <- STM.atomically $ STM.readTQueue resQ
cancel thId
return res
-- | Extract the isError flag from a response.
isError :: DM.McpToolsCallResponseData -> Bool
isError dat =
dat^.DM.resultMcpToolsCallResponseData^.DM.isErrorMcpToolsCallResponseResult
-- | Extract text of the first content entry (stdout).
firstText :: DM.McpToolsCallResponseData -> String
firstText dat =
let cs = dat^.DM.resultMcpToolsCallResponseData^.DM.contentMcpToolsCallResponseResult
in DM._textMcpToolsCallResponseResultContent (head cs)
-- | Extract text of the second content entry (stderr).
secondText :: DM.McpToolsCallResponseData -> String
secondText dat =
let cs = dat^.DM.resultMcpToolsCallResponseData^.DM.contentMcpToolsCallResponseResult
in DM._textMcpToolsCallResponseResultContent (cs !! 1)
-- ---------------------------------------------------------------------------
-- Test suite
-- ---------------------------------------------------------------------------
-- |
--
run :: SpecWith SpecContext
run = do
-- TC-01: echo command (regression check)
describe "TC-01: echo command" $ do
context "when echo command issued." $ do
it "should call callback" $ \ctx -> do
putStrLn "[INFO] TC-01: EXECUTING ECHO TEST."
let domDat = ctx^.domainDataSpecContext
appDat = ctx^.appDataSpecContext
cmdQ = domDat^.DM.cmdRunQueueDomainData
resQ = domDat^.DM.responseQueueDomainData
expect = "abc"
jsonR = def {DM._jsonrpcJsonRpcRequest = expect}
argDat = DM.EchoCmdRunCommandData jsonR ""
args = DM.EchoCmdRunCommand argDat
thId <- async $ SUT.runWithAppData appDat domDat
STM.atomically $ STM.writeTQueue cmdQ args
(DM.McpToolsCallResponse dat) <- STM.atomically $ STM.readTQueue resQ
let actual = dat^.DM.jsonrpcMcpToolsCallResponseData^.DM.jsonrpcJsonRpcRequest
actual `shouldBe` expect
cancel thId
-- TC-02: stdout-only command
-- Verifies that stdout is correctly captured after the concurrent-read fix.
describe "TC-02: stdout-only command" $ do
context "when a command writes only to stdout" $ do
it "should return isError=False and capture stdout" $ \ctx -> do
putStrLn "[INFO] TC-02: EXECUTING STDOUT-ONLY TEST."
let domDat = ctx^.domainDataSpecContext
appDat = ctx^.appDataSpecContext
token = "hellostdout"
scriptsDir <- testScriptsDir
let domDat' = domDat { DM._toolsDirDomainData = scriptsDir }
res <- sendCmdRun domDat' appDat "echo_stdout" token
-- debug: print actual response for diagnosis
putStrLn $ "[DEBUG] TC-02 isError: " ++ show (isError res)
putStrLn $ "[DEBUG] TC-02 firstText: " ++ firstText res
putStrLn $ "[DEBUG] TC-02 secondText: " ++ secondText res
isError res `shouldBe` False
firstText res `shouldContain` token
-- TC-03: stderr-only command
-- Verifies that stderr is correctly captured via the concurrent read thread.
describe "TC-03: stderr-only command" $ do
context "when a command writes only to stderr" $ do
it "should return isError=False and capture stderr" $ \ctx -> do
putStrLn "[INFO] TC-03: EXECUTING STDERR-ONLY TEST."
let domDat = ctx^.domainDataSpecContext
appDat = ctx^.appDataSpecContext
token = "hellostderr"
scriptsDir <- testScriptsDir
let domDat' = domDat { DM._toolsDirDomainData = scriptsDir }
res <- sendCmdRun domDat' appDat "echo_stderr" token
isError res `shouldBe` False
secondText res `shouldContain` token
-- TC-04: timeout
-- Verifies that a long-running command is terminated within timeoutMicrosec
-- and returns an error response containing "timeout occurred.".
describe "TC-04: timeout" $ do
context "when a command exceeds the timeout" $ do
it "should return isError=True with 'timeout occurred.'" $ \ctx -> do
putStrLn "[INFO] TC-04: EXECUTING TIMEOUT TEST."
let domDat = ctx^.domainDataSpecContext
appDat = ctx^.appDataSpecContext
scriptsDir <- testScriptsDir
let domDat' = domDat
{ DM._toolsDirDomainData = scriptsDir
, DM._timeoutMicrosecDomainData = 3 * 1000 * 1000
}
-- sleep.bat sleeps for 60 s, which exceeds the 3-second timeout.
res <- sendCmdRun domDat' appDat "sleep" ""
-- debug: print actual response for diagnosis
putStrLn $ "[DEBUG] TC-04 isError: " ++ show (isError res)
putStrLn $ "[DEBUG] TC-04 firstText: " ++ firstText res
isError res `shouldBe` True
secondText res `shouldContain` "timeout occurred."