v316_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. package workflow_test
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "workflow"
  7. )
  8. // ── helpers ───────────────────────────────────────────────────────────────────
  9. func intPtr(n int) *int { return &n }
  10. // toInt converts various numeric types to int for test assertions.
  11. func toInt(v interface{}) (int, error) {
  12. switch n := v.(type) {
  13. case int:
  14. return n, nil
  15. case int64:
  16. return int(n), nil
  17. case float64:
  18. return int(n), nil
  19. case int32:
  20. return int(n), nil
  21. default:
  22. return 0, fmt.Errorf("not a number: %T %v", v, v)
  23. }
  24. }
  25. func v316Registry(vars ...string) workflow.Registry {
  26. return workflow.Registry{
  27. Services: []string{},
  28. Components: []string{},
  29. Vars: vars,
  30. }
  31. }
  32. func v316Adapters() *workflow.Adapters {
  33. return &workflow.Adapters{
  34. Service: workflow.NewDefaultServiceAdapter(),
  35. Component: workflow.NewDefaultComponentAdapter(),
  36. LLM: workflow.NewDefaultLLMAdapter(),
  37. }
  38. }
  39. func mustEngineV316(t *testing.T, wf *workflow.Workflow) *workflow.Engine {
  40. t.Helper()
  41. eng, err := workflow.NewEngine(wf)
  42. if err != nil {
  43. t.Fatalf("NewEngine: %v", err)
  44. }
  45. return eng
  46. }
  47. // ── Validation tests ─────────────────────────────────────────────────────────
  48. func TestV316_Validate_WhileAndSourceMutuallyExclusive(t *testing.T) {
  49. wf := &workflow.Workflow{
  50. Version: "3.16",
  51. Name: "while+source",
  52. Registry: v316Registry("$x(INT)"),
  53. Steps: []workflow.Step{
  54. {ID: "Loop_test", Source: "=$x", While: "=true", MaxIterations: intPtr(3), Mode: "serial", Children: []string{"Stop_End"}, Next: "Stop_End"},
  55. {ID: "Stop_End"},
  56. },
  57. }
  58. _, err := workflow.NewEngine(wf)
  59. if err == nil {
  60. t.Fatal("expected error for while + source, got nil")
  61. }
  62. t.Logf("OK: %v", err)
  63. }
  64. func TestV316_Validate_WhileRequiresMaxIterations(t *testing.T) {
  65. wf := &workflow.Workflow{
  66. Version: "3.16",
  67. Name: "while-no-max",
  68. Registry: v316Registry(),
  69. Steps: []workflow.Step{
  70. {ID: "Loop_test", While: "=true", Mode: "serial", Children: []string{"Noop_body"}, Next: "Stop_End"},
  71. {ID: "Noop_body", Next: "RETURN"},
  72. {ID: "Stop_End"},
  73. },
  74. }
  75. _, err := workflow.NewEngine(wf)
  76. if err == nil {
  77. t.Fatal("expected error for while without maxIterations, got nil")
  78. }
  79. t.Logf("OK: %v", err)
  80. }
  81. func TestV316_Validate_MaxIterationsLessThan1(t *testing.T) {
  82. wf := &workflow.Workflow{
  83. Version: "3.16",
  84. Name: "max-iter-zero",
  85. Registry: v316Registry(),
  86. Steps: []workflow.Step{
  87. {ID: "Loop_test", While: "=true", MaxIterations: intPtr(0), Mode: "serial", Children: []string{"Noop_body"}, Next: "Stop_End"},
  88. {ID: "Noop_body", Next: "RETURN"},
  89. {ID: "Stop_End"},
  90. },
  91. }
  92. _, err := workflow.NewEngine(wf)
  93. if err == nil {
  94. t.Fatal("expected error for maxIterations < 1, got nil")
  95. }
  96. t.Logf("OK: %v", err)
  97. }
  98. func TestV316_Validate_WhileRequiresSerialMode(t *testing.T) {
  99. wf := &workflow.Workflow{
  100. Version: "3.16",
  101. Name: "while-parallel",
  102. Registry: v316Registry(),
  103. Steps: []workflow.Step{
  104. {ID: "Loop_test", While: "=true", MaxIterations: intPtr(5), Mode: "parallel", Children: []string{"Noop_body"}, Next: "Stop_End"},
  105. {ID: "Noop_body", Next: "RETURN"},
  106. {ID: "Stop_End"},
  107. },
  108. }
  109. _, err := workflow.NewEngine(wf)
  110. if err == nil {
  111. t.Fatal("expected error for while + parallel, got nil")
  112. }
  113. t.Logf("OK: %v", err)
  114. }
  115. func TestV316_Validate_BreakOutsideLoop(t *testing.T) {
  116. wf := &workflow.Workflow{
  117. Version: "3.16",
  118. Name: "break-outside",
  119. Registry: v316Registry(),
  120. Steps: []workflow.Step{
  121. {ID: "Set_x", Target: "$x", Value: "1", Next: "BREAK"},
  122. {ID: "Stop_End"},
  123. },
  124. }
  125. _, err := workflow.NewEngine(wf)
  126. if err == nil {
  127. t.Fatal("expected error for BREAK outside loop, got nil")
  128. }
  129. t.Logf("OK: %v", err)
  130. }
  131. func TestV316_Validate_BreakAsStepID(t *testing.T) {
  132. wf := &workflow.Workflow{
  133. Version: "3.16",
  134. Name: "break-stepid",
  135. Registry: v316Registry(),
  136. Steps: []workflow.Step{
  137. {ID: "BREAK"},
  138. },
  139. }
  140. _, err := workflow.NewEngine(wf)
  141. if err == nil {
  142. t.Fatal("expected error for BREAK as stepId, got nil")
  143. }
  144. t.Logf("OK: %v", err)
  145. }
  146. func TestV316_Validate_ModelMalformedSlash(t *testing.T) {
  147. wf := &workflow.Workflow{
  148. Version: "3.16",
  149. Name: "model-slash",
  150. Registry: v316Registry("$a(STRING)"),
  151. Steps: []workflow.Step{
  152. {
  153. ID: "LLM_test",
  154. Model: "openai/", // empty modelId
  155. In: workflow.StepInput{"messages": []interface{}{map[string]interface{}{"role": "user", "content": "hi"}}},
  156. Out: workflow.StepOutput{"$a": "=_result"},
  157. Next: "Stop_End",
  158. },
  159. {ID: "Stop_End"},
  160. },
  161. }
  162. _, err := workflow.NewEngine(wf)
  163. if err == nil {
  164. t.Fatal("expected error for model with empty modelId, got nil")
  165. }
  166. t.Logf("OK: %v", err)
  167. }
  168. func TestV316_Validate_ModelMalformedSlashNoProvider(t *testing.T) {
  169. wf := &workflow.Workflow{
  170. Version: "3.16",
  171. Name: "model-no-provider",
  172. Registry: v316Registry("$a(STRING)"),
  173. Steps: []workflow.Step{
  174. {
  175. ID: "LLM_test",
  176. Model: "/gpt-4.1", // empty provider
  177. In: workflow.StepInput{"messages": []interface{}{map[string]interface{}{"role": "user", "content": "hi"}}},
  178. Out: workflow.StepOutput{"$a": "=_result"},
  179. Next: "Stop_End",
  180. },
  181. {ID: "Stop_End"},
  182. },
  183. }
  184. _, err := workflow.NewEngine(wf)
  185. if err == nil {
  186. t.Fatal("expected error for model with empty provider, got nil")
  187. }
  188. t.Logf("OK: %v", err)
  189. }
  190. func TestV316_Validate_BreakInsideLoopOK(t *testing.T) {
  191. wf := &workflow.Workflow{
  192. Version: "3.16",
  193. Name: "break-inside-loop",
  194. Registry: v316Registry("$items([INT])"),
  195. Steps: []workflow.Step{
  196. {ID: "Set_items", Target: "$items", Value: "[1,2,3]", Next: "Loop_x"},
  197. {ID: "Loop_x", Source: "=$items", Mode: "serial", Children: []string{"Set_brk"}, Next: "Stop_End"},
  198. {ID: "Set_brk", Target: "$items", Value: "1", Next: "BREAK"},
  199. {ID: "Stop_End"},
  200. },
  201. }
  202. _, err := workflow.NewEngine(wf)
  203. if err != nil {
  204. t.Fatalf("expected no error for BREAK inside loop, got: %v", err)
  205. }
  206. }
  207. func TestV316_Validate_WhileLoopOK(t *testing.T) {
  208. wf := &workflow.Workflow{
  209. Version: "3.16",
  210. Name: "while-loop-ok",
  211. Registry: v316Registry("$counter(INT)"),
  212. Steps: []workflow.Step{
  213. {ID: "Set_init", Target: "$counter", Value: "0", Next: "Loop_w"},
  214. {ID: "Loop_w", While: "=$counter < 5", MaxIterations: intPtr(10), Mode: "serial", Children: []string{"Set_inc"}, Next: "Stop_End"},
  215. {ID: "Set_inc", Target: "$counter", Value: "=$counter + 1", Next: "RETURN"},
  216. {ID: "Stop_End"},
  217. },
  218. }
  219. _, err := workflow.NewEngine(wf)
  220. if err != nil {
  221. t.Fatalf("expected no error for valid while loop, got: %v", err)
  222. }
  223. }
  224. // ── Execution tests ──────────────────────────────────────────────────────────
  225. func TestV316_WhileLoop_BasicCounter(t *testing.T) {
  226. // While loop counting $counter from 0 to 4
  227. wf := &workflow.Workflow{
  228. Version: "3.16",
  229. Name: "while-counter",
  230. Registry: v316Registry("$counter(INT)"),
  231. Steps: []workflow.Step{
  232. {ID: "Set_init", Target: "$counter", Value: "0", Next: "Loop_w"},
  233. {ID: "Loop_w", While: "=$counter < 5", MaxIterations: intPtr(10), Mode: "serial", Children: []string{"Set_inc"}, Next: "Stop_End"},
  234. {ID: "Set_inc", Target: "$counter", Value: "=$counter + 1", Next: "RETURN"},
  235. {ID: "Stop_End"},
  236. },
  237. }
  238. eng := mustEngineV316(t, wf)
  239. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  240. if err != nil {
  241. t.Fatalf("Execute: %v", err)
  242. }
  243. events := collectEvents(result.RunEventStream)
  244. // Verify counter reached 5
  245. counter, ok := result.Context.Variables["$counter"]
  246. if !ok {
  247. t.Fatal("$counter not found in variables")
  248. }
  249. // counter should be 5 (0 + 5 increments)
  250. counterVal, err2 := toInt(counter)
  251. if err2 != nil {
  252. t.Fatalf("$counter: %v", err2)
  253. }
  254. if counterVal != 5 {
  255. t.Errorf("expected $counter=5, got %d", counterVal)
  256. }
  257. // Count step_start events for Set_inc (should be 5)
  258. incCount := 0
  259. for _, ev := range events {
  260. if ev.Type == workflow.RunEventStepStart && ev.StepID != nil && *ev.StepID == "Set_inc" {
  261. incCount++
  262. }
  263. }
  264. if incCount != 5 {
  265. t.Errorf("expected 5 Set_inc step_start events, got %d", incCount)
  266. }
  267. }
  268. func TestV316_WhileLoop_MaxIterationsForceExit(t *testing.T) {
  269. // while=true but maxIterations=3 → exactly 3 iterations
  270. wf := &workflow.Workflow{
  271. Version: "3.16",
  272. Name: "while-maxiter",
  273. Registry: v316Registry("$counter(INT)"),
  274. Steps: []workflow.Step{
  275. {ID: "Set_init", Target: "$counter", Value: "0", Next: "Loop_w"},
  276. {ID: "Loop_w", While: "=true", MaxIterations: intPtr(3), Mode: "serial", Children: []string{"Set_inc"}, Next: "Stop_End"},
  277. {ID: "Set_inc", Target: "$counter", Value: "=$counter + 1", Next: "RETURN"},
  278. {ID: "Stop_End"},
  279. },
  280. }
  281. eng := mustEngineV316(t, wf)
  282. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  283. if err != nil {
  284. t.Fatalf("Execute: %v", err)
  285. }
  286. collectEvents(result.RunEventStream)
  287. counter, _ := result.Context.Variables["$counter"]
  288. counterVal, err2 := toInt(counter)
  289. if err2 != nil {
  290. t.Fatalf("$counter: %v", err2)
  291. }
  292. if counterVal != 3 {
  293. t.Errorf("expected $counter=3, got %d", counterVal)
  294. }
  295. }
  296. func TestV316_WhileLoop_ConditionFalseImmediately(t *testing.T) {
  297. // while=false from the start → 0 iterations
  298. wf := &workflow.Workflow{
  299. Version: "3.16",
  300. Name: "while-false",
  301. Registry: v316Registry("$counter(INT)"),
  302. Steps: []workflow.Step{
  303. {ID: "Set_init", Target: "$counter", Value: "0", Next: "Loop_w"},
  304. {ID: "Loop_w", While: "=false", MaxIterations: intPtr(10), Mode: "serial", Children: []string{"Set_inc"}, Next: "Stop_End"},
  305. {ID: "Set_inc", Target: "$counter", Value: "=$counter + 1", Next: "RETURN"},
  306. {ID: "Stop_End"},
  307. },
  308. }
  309. eng := mustEngineV316(t, wf)
  310. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  311. if err != nil {
  312. t.Fatalf("Execute: %v", err)
  313. }
  314. events := collectEvents(result.RunEventStream)
  315. counter, _ := result.Context.Variables["$counter"]
  316. counterVal, err2 := toInt(counter)
  317. if err2 != nil {
  318. t.Fatalf("$counter: %v", err2)
  319. }
  320. if counterVal != 0 {
  321. t.Errorf("expected $counter=0, got %d", counterVal)
  322. }
  323. // Set_inc should never fire
  324. for _, ev := range events {
  325. if ev.Type == workflow.RunEventStepStart && ev.StepID != nil && *ev.StepID == "Set_inc" {
  326. t.Error("Set_inc should not have started")
  327. }
  328. }
  329. }
  330. func TestV316_Break_SerialLoop(t *testing.T) {
  331. // Loop over [1,2,3,4,5], BREAK when _item == 3
  332. wf := &workflow.Workflow{
  333. Version: "3.16",
  334. Name: "break-serial",
  335. Registry: v316Registry("$items([INT])", "$results([INT])"),
  336. Steps: []workflow.Step{
  337. {ID: "Set_items", Target: "$items", Value: "[1, 2, 3, 4, 5]", Next: "Loop_x"},
  338. {ID: "Loop_x", Source: "=$items", Mode: "serial", Children: []string{"Branch_chk"}, Next: "Stop_End"},
  339. {ID: "Branch_chk", Cases: [][]string{{"=_item == 3", "Set_brk"}, {"ELSE", "Set_ok"}}, Next: "RETURN"},
  340. {ID: "Set_brk", Target: "$results[_index]", Value: "=_item", Next: "BREAK"},
  341. {ID: "Set_ok", Target: "$results[_index]", Value: "=_item", Next: "RETURN"},
  342. {ID: "Stop_End"},
  343. },
  344. }
  345. eng := mustEngineV316(t, wf)
  346. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  347. if err != nil {
  348. t.Fatalf("Execute: %v", err)
  349. }
  350. events := collectEvents(result.RunEventStream)
  351. // Should have results for indices 0,1,2 (items 1,2,3) — BREAK at item 3
  352. results, _ := result.Context.Variables["$results"]
  353. resultsSlice, ok := results.([]interface{})
  354. if !ok {
  355. t.Fatalf("$results not a slice: %T", results)
  356. }
  357. if len(resultsSlice) != 3 {
  358. t.Errorf("expected 3 results, got %d: %v", len(resultsSlice), resultsSlice)
  359. }
  360. // Items 4 and 5 should NOT have been processed
  361. setOkCount := 0
  362. for _, ev := range events {
  363. if ev.Type == workflow.RunEventStepStart && ev.StepID != nil && *ev.StepID == "Set_ok" {
  364. setOkCount++
  365. }
  366. }
  367. if setOkCount != 2 { // items 1 and 2
  368. t.Errorf("expected 2 Set_ok executions, got %d", setOkCount)
  369. }
  370. }
  371. func TestV316_Break_WhileLoop(t *testing.T) {
  372. // While loop that BREAKs after 2 iterations
  373. wf := &workflow.Workflow{
  374. Version: "3.16",
  375. Name: "break-while",
  376. Registry: v316Registry("$counter(INT)"),
  377. Steps: []workflow.Step{
  378. {ID: "Set_init", Target: "$counter", Value: "0", Next: "Loop_w"},
  379. {ID: "Loop_w", While: "=true", MaxIterations: intPtr(10), Mode: "serial", Children: []string{"Branch_chk"}, Next: "Stop_End"},
  380. {ID: "Branch_chk", Cases: [][]string{{"=$counter >= 2", "Set_brk"}, {"ELSE", "Set_inc"}}, Next: "RETURN"},
  381. {ID: "Set_brk", Target: "$counter", Value: "=$counter", Next: "BREAK"},
  382. {ID: "Set_inc", Target: "$counter", Value: "=$counter + 1", Next: "RETURN"},
  383. {ID: "Stop_End"},
  384. },
  385. }
  386. eng := mustEngineV316(t, wf)
  387. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  388. if err != nil {
  389. t.Fatalf("Execute: %v", err)
  390. }
  391. collectEvents(result.RunEventStream)
  392. counter, _ := result.Context.Variables["$counter"]
  393. counterVal, err2 := toInt(counter)
  394. if err2 != nil {
  395. t.Fatalf("$counter: %v", err2)
  396. }
  397. if counterVal != 2 {
  398. t.Errorf("expected $counter=2, got %d", counterVal)
  399. }
  400. }
  401. func TestV316_SourceLoop_MaxIterationsCaps(t *testing.T) {
  402. // source has 5 items, maxIterations: 3 → only 3 iterations
  403. wf := &workflow.Workflow{
  404. Version: "3.16",
  405. Name: "source-maxiter",
  406. Registry: v316Registry("$items([INT])", "$results([INT])"),
  407. Steps: []workflow.Step{
  408. {ID: "Set_items", Target: "$items", Value: "[10, 20, 30, 40, 50]", Next: "Loop_x"},
  409. {ID: "Loop_x", Source: "=$items", Mode: "serial", MaxIterations: intPtr(3), Children: []string{"Set_col"}, Next: "Stop_End"},
  410. {ID: "Set_col", Target: "$results[_index]", Value: "=_item", Next: "RETURN"},
  411. {ID: "Stop_End"},
  412. },
  413. }
  414. eng := mustEngineV316(t, wf)
  415. result, err := eng.Execute(context.Background(), nil, v316Adapters())
  416. if err != nil {
  417. t.Fatalf("Execute: %v", err)
  418. }
  419. collectEvents(result.RunEventStream)
  420. results, _ := result.Context.Variables["$results"]
  421. resultsSlice, ok := results.([]interface{})
  422. if !ok {
  423. t.Fatalf("$results not a slice: %T", results)
  424. }
  425. if len(resultsSlice) != 3 {
  426. t.Errorf("expected 3 results, got %d: %v", len(resultsSlice), resultsSlice)
  427. }
  428. }
  429. // ── LLMAdapterRegistry tests ─────────────────────────────────────────────────
  430. func TestV316_LLMAdapterRegistry_Resolve(t *testing.T) {
  431. mockOpenAI := workflow.NewDefaultLLMAdapter()
  432. mockAnthropic := workflow.NewDefaultLLMAdapter()
  433. registry := workflow.NewLLMAdapterRegistry(mockOpenAI, "openai")
  434. registry.Register("openai", mockOpenAI)
  435. registry.Register("anthropic", mockAnthropic)
  436. // Empty spec → default adapter
  437. adapter, override, err := registry.Resolve("")
  438. if err != nil {
  439. t.Fatalf("Resolve empty: %v", err)
  440. }
  441. if adapter != mockOpenAI {
  442. t.Error("expected default openai adapter")
  443. }
  444. if override != "" {
  445. t.Errorf("expected no override, got %q", override)
  446. }
  447. // Provider-only → correct adapter, no override
  448. adapter, override, err = registry.Resolve("anthropic")
  449. if err != nil {
  450. t.Fatalf("Resolve anthropic: %v", err)
  451. }
  452. if adapter != mockAnthropic {
  453. t.Error("expected anthropic adapter")
  454. }
  455. if override != "" {
  456. t.Errorf("expected no override, got %q", override)
  457. }
  458. // Full spec → correct adapter with model override
  459. adapter, override, err = registry.Resolve("openai/gpt-4.1")
  460. if err != nil {
  461. t.Fatalf("Resolve openai/gpt-4.1: %v", err)
  462. }
  463. if adapter != mockOpenAI {
  464. t.Error("expected openai adapter")
  465. }
  466. if override != "gpt-4.1" {
  467. t.Errorf("expected override 'gpt-4.1', got %q", override)
  468. }
  469. // Unknown provider → error
  470. _, _, err = registry.Resolve("unknown")
  471. if err == nil {
  472. t.Error("expected error for unknown provider")
  473. }
  474. _, _, err = registry.Resolve("unknown/model")
  475. if err == nil {
  476. t.Error("expected error for unknown/model")
  477. }
  478. }