1
0

workflow_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "os"
  6. "testing"
  7. "github.com/urfave/cli/v3"
  8. )
  9. // TestWorkflowCommandStructure tests that the workflow command is properly structured
  10. func TestWorkflowCommandStructure(t *testing.T) {
  11. cmd := WorkflowCommand()
  12. if cmd.Name != "workflow" {
  13. t.Errorf("Expected command name 'workflow', got '%s'", cmd.Name)
  14. }
  15. if cmd.Usage == "" {
  16. t.Error("Command usage should not be empty")
  17. }
  18. // Check subcommands exist
  19. expectedSubcmds := []string{"template", "instance", "node"}
  20. subcmdMap := make(map[string]bool)
  21. for _, subcmd := range cmd.Commands {
  22. subcmdMap[subcmd.Name] = true
  23. }
  24. for _, expected := range expectedSubcmds {
  25. if !subcmdMap[expected] {
  26. t.Errorf("Missing expected subcommand: %s", expected)
  27. }
  28. }
  29. }
  30. // TestWorkflowTemplateSubcommands tests the template subcommand structure
  31. func TestWorkflowTemplateSubcommands(t *testing.T) {
  32. cmd := WorkflowCommand()
  33. var templateCmd *cli.Command
  34. for _, c := range cmd.Commands {
  35. if c.Name == "template" {
  36. templateCmd = c
  37. break
  38. }
  39. }
  40. if templateCmd == nil {
  41. t.Fatal("template subcommand not found")
  42. }
  43. expectedActions := []string{"list", "get", "create", "update", "delete"}
  44. actionMap := make(map[string]bool)
  45. for _, action := range templateCmd.Commands {
  46. actionMap[action.Name] = true
  47. }
  48. for _, expected := range expectedActions {
  49. if !actionMap[expected] {
  50. t.Errorf("Missing template action: %s", expected)
  51. }
  52. }
  53. }
  54. // TestWorkflowInstanceSubcommands tests the instance subcommand structure
  55. func TestWorkflowInstanceSubcommands(t *testing.T) {
  56. cmd := WorkflowCommand()
  57. var instanceCmd *cli.Command
  58. for _, c := range cmd.Commands {
  59. if c.Name == "instance" {
  60. instanceCmd = c
  61. break
  62. }
  63. }
  64. if instanceCmd == nil {
  65. t.Fatal("instance subcommand not found")
  66. }
  67. expectedActions := []string{"list", "get", "start", "cancel"}
  68. actionMap := make(map[string]bool)
  69. for _, action := range instanceCmd.Commands {
  70. actionMap[action.Name] = true
  71. }
  72. for _, expected := range expectedActions {
  73. if !actionMap[expected] {
  74. t.Errorf("Missing instance action: %s", expected)
  75. }
  76. }
  77. }
  78. // TestWorkflowNodeSubcommands tests the node subcommand structure
  79. func TestWorkflowNodeSubcommands(t *testing.T) {
  80. cmd := WorkflowCommand()
  81. var nodeCmd *cli.Command
  82. for _, c := range cmd.Commands {
  83. if c.Name == "node" {
  84. nodeCmd = c
  85. break
  86. }
  87. }
  88. if nodeCmd == nil {
  89. t.Fatal("node subcommand not found")
  90. }
  91. expectedActions := []string{"list", "get", "retry"}
  92. actionMap := make(map[string]bool)
  93. for _, action := range nodeCmd.Commands {
  94. actionMap[action.Name] = true
  95. }
  96. for _, expected := range expectedActions {
  97. if !actionMap[expected] {
  98. t.Errorf("Missing node action: %s", expected)
  99. }
  100. }
  101. }
  102. // TestWorkflowTemplateFlags tests that template command flags are properly defined
  103. func TestWorkflowTemplateFlags(t *testing.T) {
  104. cmd := WorkflowCommand()
  105. var templateCmd *cli.Command
  106. for _, c := range cmd.Commands {
  107. if c.Name == "template" {
  108. templateCmd = c
  109. break
  110. }
  111. }
  112. // Test create command flags
  113. var createCmd *cli.Command
  114. for _, c := range templateCmd.Commands {
  115. if c.Name == "create" {
  116. createCmd = c
  117. break
  118. }
  119. }
  120. flagNames := make(map[string]bool)
  121. for _, flag := range createCmd.Flags {
  122. flagNames[flag.Names()[0]] = true
  123. }
  124. expectedFlags := []string{"name", "description", "definition", "active"}
  125. for _, expected := range expectedFlags {
  126. if !flagNames[expected] {
  127. t.Errorf("Missing create flag: %s", expected)
  128. }
  129. }
  130. // Test that id flag is required for get command
  131. var getCmd *cli.Command
  132. for _, c := range templateCmd.Commands {
  133. if c.Name == "get" {
  134. getCmd = c
  135. break
  136. }
  137. }
  138. var idFlag *cli.StringFlag
  139. for _, flag := range getCmd.Flags {
  140. if flag.Names()[0] == "id" {
  141. idFlag = flag.(*cli.StringFlag)
  142. break
  143. }
  144. }
  145. if idFlag == nil {
  146. t.Error("Missing id flag on get command")
  147. } else if !idFlag.Required {
  148. t.Error("id flag should be required on get command")
  149. }
  150. }
  151. // TestWorkflowInstanceFlags tests that instance command flags are properly defined
  152. func TestWorkflowInstanceFlags(t *testing.T) {
  153. cmd := WorkflowCommand()
  154. var instanceCmd *cli.Command
  155. for _, c := range cmd.Commands {
  156. if c.Name == "instance" {
  157. instanceCmd = c
  158. break
  159. }
  160. }
  161. // Test start command flags
  162. var startCmd *cli.Command
  163. for _, c := range instanceCmd.Commands {
  164. if c.Name == "start" {
  165. startCmd = c
  166. break
  167. }
  168. }
  169. flagNames := make(map[string]bool)
  170. for _, flag := range startCmd.Flags {
  171. flagNames[flag.Names()[0]] = true
  172. }
  173. expectedFlags := []string{"template", "service", "context"}
  174. for _, expected := range expectedFlags {
  175. if !flagNames[expected] {
  176. t.Errorf("Missing start flag: %s", expected)
  177. }
  178. }
  179. // Test that template flag is required
  180. var templateFlag *cli.StringFlag
  181. for _, flag := range startCmd.Flags {
  182. if flag.Names()[0] == "template" {
  183. templateFlag = flag.(*cli.StringFlag)
  184. break
  185. }
  186. }
  187. if templateFlag == nil {
  188. t.Error("Missing template flag on start command")
  189. } else if !templateFlag.Required {
  190. t.Error("template flag should be required on start command")
  191. }
  192. }
  193. // TestWorkflowNodeFlags tests that node command flags are properly defined
  194. func TestWorkflowNodeFlags(t *testing.T) {
  195. cmd := WorkflowCommand()
  196. var nodeCmd *cli.Command
  197. for _, c := range cmd.Commands {
  198. if c.Name == "node" {
  199. nodeCmd = c
  200. break
  201. }
  202. }
  203. // Test list command flags
  204. var listCmd *cli.Command
  205. for _, c := range nodeCmd.Commands {
  206. if c.Name == "list" {
  207. listCmd = c
  208. break
  209. }
  210. }
  211. var instanceFlag *cli.StringFlag
  212. for _, flag := range listCmd.Flags {
  213. if flag.Names()[0] == "instance" {
  214. instanceFlag = flag.(*cli.StringFlag)
  215. break
  216. }
  217. }
  218. if instanceFlag == nil {
  219. t.Error("Missing instance flag on node list command")
  220. } else if !instanceFlag.Required {
  221. t.Error("instance flag should be required on node list command")
  222. }
  223. }
  224. // TestReadFileOrString tests the helper function for reading files
  225. func TestReadFileOrString(t *testing.T) {
  226. // Test with regular string (no @ prefix)
  227. input := "hello world"
  228. result := ReadFileOrString(input)
  229. if result != input {
  230. t.Errorf("Expected '%s', got '%s'", input, result)
  231. }
  232. // Test with non-existent file (should return original)
  233. input = "@nonexistent_file.json"
  234. result = ReadFileOrString(input)
  235. if result != input {
  236. t.Errorf("Expected original input for non-existent file, got '%s'", result)
  237. }
  238. // Test with actual file
  239. tmpFile, err := os.CreateTemp("", "test_*.json")
  240. if err != nil {
  241. t.Fatalf("Failed to create temp file: %v", err)
  242. }
  243. defer os.Remove(tmpFile.Name())
  244. testContent := `{"test": "content"}`
  245. if _, err := tmpFile.WriteString(testContent); err != nil {
  246. t.Fatalf("Failed to write to temp file: %v", err)
  247. }
  248. tmpFile.Close()
  249. result = ReadFileOrString("@" + tmpFile.Name())
  250. if result != testContent {
  251. t.Errorf("Expected file content '%s', got '%s'", testContent, result)
  252. }
  253. }
  254. // TestWorkflowTemplateJSON tests JSON marshaling of WorkflowTemplate
  255. func TestWorkflowTemplateJSON(t *testing.T) {
  256. template := WorkflowTemplate{
  257. ID: "1",
  258. Name: "Test Template",
  259. Description: "A test template",
  260. Definition: `{"nodes": []}`,
  261. IsActive: true,
  262. CreatedBy: &User{ID: "1", Email: "test@example.com"},
  263. CreatedAt: "2024-01-01T00:00:00Z",
  264. UpdatedAt: "2024-01-01T00:00:00Z",
  265. }
  266. data, err := json.Marshal(template)
  267. if err != nil {
  268. t.Fatalf("Failed to marshal WorkflowTemplate: %v", err)
  269. }
  270. var unmarshaled WorkflowTemplate
  271. if err := json.Unmarshal(data, &unmarshaled); err != nil {
  272. t.Fatalf("Failed to unmarshal WorkflowTemplate: %v", err)
  273. }
  274. if unmarshaled.ID != template.ID {
  275. t.Errorf("Expected ID '%s', got '%s'", template.ID, unmarshaled.ID)
  276. }
  277. if unmarshaled.Name != template.Name {
  278. t.Errorf("Expected Name '%s', got '%s'", template.Name, unmarshaled.Name)
  279. }
  280. if unmarshaled.IsActive != template.IsActive {
  281. t.Errorf("Expected IsActive %v, got %v", template.IsActive, unmarshaled.IsActive)
  282. }
  283. }
  284. // TestWorkflowInstanceJSON tests JSON marshaling of WorkflowInstance
  285. func TestWorkflowInstanceJSON(t *testing.T) {
  286. completedAt := "2024-01-02T00:00:00Z"
  287. instance := WorkflowInstance{
  288. ID: "1",
  289. Template: &WorkflowTemplate{ID: "1", Name: "Test"},
  290. Status: "running",
  291. Context: `{"key": "value"}`,
  292. Service: &Service{ID: "1", Name: "Test Service"},
  293. CreatedAt: "2024-01-01T00:00:00Z",
  294. UpdatedAt: "2024-01-01T00:00:00Z",
  295. CompletedAt: &completedAt,
  296. }
  297. data, err := json.Marshal(instance)
  298. if err != nil {
  299. t.Fatalf("Failed to marshal WorkflowInstance: %v", err)
  300. }
  301. var unmarshaled WorkflowInstance
  302. if err := json.Unmarshal(data, &unmarshaled); err != nil {
  303. t.Fatalf("Failed to unmarshal WorkflowInstance: %v", err)
  304. }
  305. if unmarshaled.ID != instance.ID {
  306. t.Errorf("Expected ID '%s', got '%s'", instance.ID, unmarshaled.ID)
  307. }
  308. if unmarshaled.Status != instance.Status {
  309. t.Errorf("Expected Status '%s', got '%s'", instance.Status, unmarshaled.Status)
  310. }
  311. if unmarshaled.CompletedAt == nil || *unmarshaled.CompletedAt != completedAt {
  312. t.Errorf("Expected CompletedAt '%s', got '%v'", completedAt, unmarshaled.CompletedAt)
  313. }
  314. }
  315. // TestWorkflowNodeJSON tests JSON marshaling of WorkflowNode
  316. func TestWorkflowNodeJSON(t *testing.T) {
  317. startedAt := "2024-01-01T01:00:00Z"
  318. node := WorkflowNode{
  319. ID: "1",
  320. NodeKey: "node_1",
  321. NodeType: "task",
  322. Status: "running",
  323. Task: &Task{ID: "1", Title: "Test Task"},
  324. InputData: `{"input": "data"}`,
  325. OutputData: `{"output": "data"}`,
  326. RetryCount: 2,
  327. CreatedAt: "2024-01-01T00:00:00Z",
  328. UpdatedAt: "2024-01-01T00:00:00Z",
  329. StartedAt: &startedAt,
  330. CompletedAt: nil,
  331. }
  332. data, err := json.Marshal(node)
  333. if err != nil {
  334. t.Fatalf("Failed to marshal WorkflowNode: %v", err)
  335. }
  336. var unmarshaled WorkflowNode
  337. if err := json.Unmarshal(data, &unmarshaled); err != nil {
  338. t.Fatalf("Failed to unmarshal WorkflowNode: %v", err)
  339. }
  340. if unmarshaled.ID != node.ID {
  341. t.Errorf("Expected ID '%s', got '%s'", node.ID, unmarshaled.ID)
  342. }
  343. if unmarshaled.NodeKey != node.NodeKey {
  344. t.Errorf("Expected NodeKey '%s', got '%s'", node.NodeKey, unmarshaled.NodeKey)
  345. }
  346. if unmarshaled.RetryCount != node.RetryCount {
  347. t.Errorf("Expected RetryCount %d, got %d", node.RetryCount, unmarshaled.RetryCount)
  348. }
  349. }
  350. // TestWorkflowTemplateListJSONFlag tests the JSON flag on template list
  351. func TestWorkflowTemplateListJSONFlag(t *testing.T) {
  352. cmd := WorkflowCommand()
  353. var templateCmd *cli.Command
  354. for _, c := range cmd.Commands {
  355. if c.Name == "template" {
  356. templateCmd = c
  357. break
  358. }
  359. }
  360. var listCmd *cli.Command
  361. for _, c := range templateCmd.Commands {
  362. if c.Name == "list" {
  363. listCmd = c
  364. break
  365. }
  366. }
  367. var jsonFlag *cli.BoolFlag
  368. for _, flag := range listCmd.Flags {
  369. if flag.Names()[0] == "json" {
  370. jsonFlag = flag.(*cli.BoolFlag)
  371. break
  372. }
  373. }
  374. if jsonFlag == nil {
  375. t.Error("Missing json flag on template list command")
  376. }
  377. }
  378. // TestWorkflowCommandInRoot tests that WorkflowCommand is registered in RootCommand
  379. func TestWorkflowCommandInRoot(t *testing.T) {
  380. rootCmd := RootCommand()
  381. found := false
  382. for _, cmd := range rootCmd.Commands {
  383. if cmd.Name == "workflow" {
  384. found = true
  385. break
  386. }
  387. }
  388. if !found {
  389. t.Error("WorkflowCommand not found in RootCommand")
  390. }
  391. }
  392. // TestWorkflowCommandHelp tests that help text is available
  393. func TestWorkflowCommandHelp(t *testing.T) {
  394. cmd := WorkflowCommand()
  395. if cmd.Description == "" {
  396. t.Error("WorkflowCommand should have a description")
  397. }
  398. // Test that subcommands have descriptions
  399. for _, subcmd := range cmd.Commands {
  400. if subcmd.Usage == "" {
  401. t.Errorf("Subcommand '%s' should have usage text", subcmd.Name)
  402. }
  403. }
  404. }
  405. // BenchmarkWorkflowCommand benchmarks the command creation
  406. func BenchmarkWorkflowCommand(b *testing.B) {
  407. for i := 0; i < b.N; i++ {
  408. _ = WorkflowCommand()
  409. }
  410. }
  411. // TestWorkflowTemplateRequiredFlags tests that required flags are enforced
  412. func TestWorkflowTemplateRequiredFlags(t *testing.T) {
  413. tests := []struct {
  414. name string
  415. action string
  416. shouldHave []string
  417. }{
  418. {"get requires id", "get", []string{"id"}},
  419. {"delete requires id", "delete", []string{"id"}},
  420. {"update requires id", "update", []string{"id"}},
  421. }
  422. cmd := WorkflowCommand()
  423. var templateCmd *cli.Command
  424. for _, c := range cmd.Commands {
  425. if c.Name == "template" {
  426. templateCmd = c
  427. break
  428. }
  429. }
  430. for _, tt := range tests {
  431. t.Run(tt.name, func(t *testing.T) {
  432. var actionCmd *cli.Command
  433. for _, c := range templateCmd.Commands {
  434. if c.Name == tt.action {
  435. actionCmd = c
  436. break
  437. }
  438. }
  439. if actionCmd == nil {
  440. t.Fatalf("Action '%s' not found", tt.action)
  441. }
  442. for _, requiredFlag := range tt.shouldHave {
  443. found := false
  444. for _, flag := range actionCmd.Flags {
  445. if flag.Names()[0] == requiredFlag {
  446. if strFlag, ok := flag.(*cli.StringFlag); ok {
  447. if strFlag.Required {
  448. found = true
  449. }
  450. }
  451. break
  452. }
  453. }
  454. if !found {
  455. t.Errorf("Flag '%s' should be required for action '%s'", requiredFlag, tt.action)
  456. }
  457. }
  458. })
  459. }
  460. }
  461. // TestWorkflowInstanceRequiredFlags tests that required flags are enforced
  462. func TestWorkflowInstanceRequiredFlags(t *testing.T) {
  463. tests := []struct {
  464. name string
  465. action string
  466. shouldHave []string
  467. }{
  468. {"get requires id", "get", []string{"id"}},
  469. {"start requires template", "start", []string{"template"}},
  470. {"cancel requires id", "cancel", []string{"id"}},
  471. }
  472. cmd := WorkflowCommand()
  473. var instanceCmd *cli.Command
  474. for _, c := range cmd.Commands {
  475. if c.Name == "instance" {
  476. instanceCmd = c
  477. break
  478. }
  479. }
  480. for _, tt := range tests {
  481. t.Run(tt.name, func(t *testing.T) {
  482. var actionCmd *cli.Command
  483. for _, c := range instanceCmd.Commands {
  484. if c.Name == tt.action {
  485. actionCmd = c
  486. break
  487. }
  488. }
  489. if actionCmd == nil {
  490. t.Fatalf("Action '%s' not found", tt.action)
  491. }
  492. for _, requiredFlag := range tt.shouldHave {
  493. found := false
  494. for _, flag := range actionCmd.Flags {
  495. if flag.Names()[0] == requiredFlag {
  496. if strFlag, ok := flag.(*cli.StringFlag); ok {
  497. if strFlag.Required {
  498. found = true
  499. }
  500. }
  501. break
  502. }
  503. }
  504. if !found {
  505. t.Errorf("Flag '%s' should be required for action '%s'", requiredFlag, tt.action)
  506. }
  507. }
  508. })
  509. }
  510. }
  511. // TestWorkflowNodeRequiredFlags tests that required flags are enforced
  512. func TestWorkflowNodeRequiredFlags(t *testing.T) {
  513. tests := []struct {
  514. name string
  515. action string
  516. shouldHave []string
  517. }{
  518. {"list requires instance", "list", []string{"instance"}},
  519. {"get requires id", "get", []string{"id"}},
  520. {"retry requires id", "retry", []string{"id"}},
  521. }
  522. cmd := WorkflowCommand()
  523. var nodeCmd *cli.Command
  524. for _, c := range cmd.Commands {
  525. if c.Name == "node" {
  526. nodeCmd = c
  527. break
  528. }
  529. }
  530. for _, tt := range tests {
  531. t.Run(tt.name, func(t *testing.T) {
  532. var actionCmd *cli.Command
  533. for _, c := range nodeCmd.Commands {
  534. if c.Name == tt.action {
  535. actionCmd = c
  536. break
  537. }
  538. }
  539. if actionCmd == nil {
  540. t.Fatalf("Action '%s' not found", tt.action)
  541. }
  542. for _, requiredFlag := range tt.shouldHave {
  543. found := false
  544. for _, flag := range actionCmd.Flags {
  545. if flag.Names()[0] == requiredFlag {
  546. if strFlag, ok := flag.(*cli.StringFlag); ok {
  547. if strFlag.Required {
  548. found = true
  549. }
  550. }
  551. break
  552. }
  553. }
  554. if !found {
  555. t.Errorf("Flag '%s' should be required for action '%s'", requiredFlag, tt.action)
  556. }
  557. }
  558. })
  559. }
  560. }
  561. // Mock context for testing command actions
  562. var _ = context.Background()