v310_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. package workflow_test
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "workflow"
  7. )
  8. // TestV310TextOutputResultIsString verifies that in v3.10, _result for text output
  9. // is a string (content only), not the full result map
  10. func TestV310TextOutputResultIsString(t *testing.T) {
  11. wf := &workflow.Workflow{
  12. Version: "3.10",
  13. Name: "V310 Text Output Test",
  14. Registry: workflow.Registry{
  15. Services: []string{},
  16. Components: []string{},
  17. Vars: []string{"$content(STRING)", "$tokens(NUMBER)"},
  18. Files: workflow.FilesRegistry{},
  19. },
  20. Steps: []workflow.Step{
  21. {
  22. ID: "LLM_Generate",
  23. In: workflow.StepInput{
  24. "messages": []interface{}{
  25. map[string]interface{}{
  26. "role": "user",
  27. "content": "Hello",
  28. },
  29. },
  30. },
  31. Out: workflow.StepOutput{
  32. "$content": "=_result",
  33. "$tokens": "=_meta.usage.total_tokens",
  34. },
  35. Next: "Stop_End",
  36. },
  37. {
  38. ID: "Stop_End",
  39. },
  40. },
  41. }
  42. adapters := createTestAdapters()
  43. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  44. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  45. return map[string]interface{}{
  46. "content": "Hello! How can I help?",
  47. "model": "gpt-4",
  48. "finish_reason": "stop",
  49. "response_id": "chatcmpl-123",
  50. "usage": map[string]interface{}{
  51. "prompt_tokens": 10,
  52. "completion_tokens": 8,
  53. "total_tokens": 18,
  54. },
  55. }, nil
  56. })
  57. engine, err := workflow.NewEngine(wf)
  58. if err != nil {
  59. t.Fatalf("Failed to create engine: %v", err)
  60. }
  61. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  62. if err != nil {
  63. t.Fatalf("Failed to execute workflow: %v", err)
  64. }
  65. drainEvents(result.RunEventStream)
  66. // v3.10: _result should be the content string, not the full map
  67. content := result.Context.Variables["$content"]
  68. contentStr, ok := content.(string)
  69. if !ok {
  70. t.Fatalf("Expected $content to be string, got %T: %v", content, content)
  71. }
  72. if contentStr != "Hello! How can I help?" {
  73. t.Errorf("Expected content 'Hello! How can I help?', got '%s'", contentStr)
  74. }
  75. // _meta.usage.total_tokens should be accessible
  76. tokens := result.Context.Variables["$tokens"]
  77. if tokens == nil {
  78. t.Fatal("Expected $tokens to be set from _meta.usage.total_tokens")
  79. }
  80. if fmt.Sprintf("%v", tokens) != "18" {
  81. t.Errorf("Expected tokens 18, got %v", tokens)
  82. }
  83. }
  84. // TestV310StructuredOutputResultIsObject verifies that in v3.10, _result for
  85. // json_schema output is the parsed JSON object (same as v3.9 for structured)
  86. func TestV310StructuredOutputResultIsObject(t *testing.T) {
  87. wf := &workflow.Workflow{
  88. Version: "3.10",
  89. Name: "V310 Structured Output Test",
  90. Registry: workflow.Registry{
  91. Services: []string{},
  92. Components: []string{},
  93. Vars: []string{"$spec(OBJECT)", "$title(STRING)"},
  94. Files: workflow.FilesRegistry{},
  95. },
  96. Steps: []workflow.Step{
  97. {
  98. ID: "LLM_GenSpec",
  99. In: workflow.StepInput{
  100. "messages": []interface{}{
  101. map[string]interface{}{
  102. "role": "user",
  103. "content": "Generate a spec",
  104. },
  105. },
  106. "output_config": map[string]interface{}{
  107. "format": map[string]interface{}{
  108. "type": "json_schema",
  109. "schema": map[string]interface{}{
  110. "type": "object",
  111. "properties": map[string]interface{}{
  112. "title": map[string]interface{}{"type": "string"},
  113. "score": map[string]interface{}{"type": "number"},
  114. },
  115. },
  116. },
  117. },
  118. },
  119. Out: workflow.StepOutput{
  120. "$spec": "=_result",
  121. "$title": "=_result.title",
  122. },
  123. Next: "Stop_End",
  124. },
  125. {
  126. ID: "Stop_End",
  127. },
  128. },
  129. }
  130. adapters := createTestAdapters()
  131. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  132. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  133. // Adapter returns parsed JSON content (json_schema mode parses in adapter)
  134. return map[string]interface{}{
  135. "content": map[string]interface{}{
  136. "title": "My Spec",
  137. "score": 95,
  138. },
  139. "model": "gpt-4",
  140. "finish_reason": "stop",
  141. "response_id": "chatcmpl-456",
  142. "usage": map[string]interface{}{
  143. "prompt_tokens": 20,
  144. "completion_tokens": 15,
  145. "total_tokens": 35,
  146. },
  147. }, nil
  148. })
  149. engine, err := workflow.NewEngine(wf)
  150. if err != nil {
  151. t.Fatalf("Failed to create engine: %v", err)
  152. }
  153. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  154. if err != nil {
  155. t.Fatalf("Failed to execute workflow: %v", err)
  156. }
  157. drainEvents(result.RunEventStream)
  158. // _result should be the parsed object
  159. spec, ok := result.Context.Variables["$spec"].(map[string]interface{})
  160. if !ok {
  161. t.Fatalf("Expected $spec to be map, got %T: %v", result.Context.Variables["$spec"], result.Context.Variables["$spec"])
  162. }
  163. if spec["title"] != "My Spec" {
  164. t.Errorf("Expected title 'My Spec', got '%v'", spec["title"])
  165. }
  166. // _result.title should also work
  167. title := result.Context.Variables["$title"]
  168. if title != "My Spec" {
  169. t.Errorf("Expected $title 'My Spec', got '%v'", title)
  170. }
  171. }
  172. // TestV310MetaFields verifies all _meta fields are populated correctly
  173. func TestV310MetaFields(t *testing.T) {
  174. var capturedMeta interface{}
  175. wf := &workflow.Workflow{
  176. Version: "3.10",
  177. Name: "V310 Meta Fields Test",
  178. Registry: workflow.Registry{
  179. Services: []string{},
  180. Components: []string{},
  181. Vars: []string{"$meta(OBJECT)"},
  182. Files: workflow.FilesRegistry{},
  183. },
  184. Steps: []workflow.Step{
  185. {
  186. ID: "LLM_Test",
  187. In: workflow.StepInput{
  188. "messages": []interface{}{
  189. map[string]interface{}{
  190. "role": "user",
  191. "content": "Test",
  192. },
  193. },
  194. },
  195. Out: workflow.StepOutput{
  196. "$meta": "=_meta",
  197. },
  198. Next: "Stop_End",
  199. },
  200. {
  201. ID: "Stop_End",
  202. },
  203. },
  204. }
  205. adapters := createTestAdapters()
  206. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  207. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  208. return map[string]interface{}{
  209. "content": "response",
  210. "model": "claude-3-opus",
  211. "finish_reason": "stop",
  212. "response_id": "msg_abc123",
  213. "usage": map[string]interface{}{
  214. "prompt_tokens": 100,
  215. "completion_tokens": 50,
  216. "total_tokens": 150,
  217. },
  218. }, nil
  219. })
  220. engine, err := workflow.NewEngine(wf)
  221. if err != nil {
  222. t.Fatalf("Failed to create engine: %v", err)
  223. }
  224. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  225. if err != nil {
  226. t.Fatalf("Failed to execute workflow: %v", err)
  227. }
  228. drainEvents(result.RunEventStream)
  229. capturedMeta = result.Context.Variables["$meta"]
  230. meta, ok := capturedMeta.(map[string]interface{})
  231. if !ok {
  232. t.Fatalf("Expected $meta to be map, got %T", capturedMeta)
  233. }
  234. // Check model
  235. if meta["model"] != "claude-3-opus" {
  236. t.Errorf("Expected model 'claude-3-opus', got '%v'", meta["model"])
  237. }
  238. // Check provider inference
  239. if meta["provider"] != "anthropic" {
  240. t.Errorf("Expected provider 'anthropic', got '%v'", meta["provider"])
  241. }
  242. // Check finish_reason
  243. if meta["finish_reason"] != "stop" {
  244. t.Errorf("Expected finish_reason 'stop', got '%v'", meta["finish_reason"])
  245. }
  246. // Check response_id
  247. if meta["response_id"] != "msg_abc123" {
  248. t.Errorf("Expected response_id 'msg_abc123', got '%v'", meta["response_id"])
  249. }
  250. // Check latency_ms exists and is > 0
  251. latencyMs, ok := meta["latency_ms"].(int64)
  252. if !ok {
  253. t.Errorf("Expected latency_ms to be int64, got %T", meta["latency_ms"])
  254. } else if latencyMs < 0 {
  255. t.Errorf("Expected latency_ms >= 0, got %d", latencyMs)
  256. }
  257. // Check usage with normalized field names
  258. usage, ok := meta["usage"].(map[string]interface{})
  259. if !ok {
  260. t.Fatalf("Expected usage to be map, got %T", meta["usage"])
  261. }
  262. if usage["input_tokens"] != 100 {
  263. t.Errorf("Expected input_tokens 100, got %v", usage["input_tokens"])
  264. }
  265. if usage["output_tokens"] != 50 {
  266. t.Errorf("Expected output_tokens 50, got %v", usage["output_tokens"])
  267. }
  268. if usage["total_tokens"] != 150 {
  269. t.Errorf("Expected total_tokens 150, got %v", usage["total_tokens"])
  270. }
  271. // Check raw usage is preserved
  272. if usage["raw"] == nil {
  273. t.Error("Expected usage.raw to be set")
  274. }
  275. }
  276. // TestV310OnErrorJumpsToHandler verifies that when an LLM call fails and the step
  277. // has onError set, the engine jumps to the error handler step
  278. func TestV310OnErrorJumpsToHandler(t *testing.T) {
  279. wf := &workflow.Workflow{
  280. Version: "3.10",
  281. Name: "V310 OnError Test",
  282. Registry: workflow.Registry{
  283. Services: []string{},
  284. Components: []string{},
  285. Vars: []string{"$lastError(OBJECT)", "$success(STRING)"},
  286. Files: workflow.FilesRegistry{},
  287. },
  288. Steps: []workflow.Step{
  289. {
  290. ID: "LLM_WillFail",
  291. In: workflow.StepInput{
  292. "messages": []interface{}{
  293. map[string]interface{}{
  294. "role": "user",
  295. "content": "This will fail",
  296. },
  297. },
  298. },
  299. Out: workflow.StepOutput{
  300. "$success": "=_result",
  301. },
  302. OnError: "Set_LogError",
  303. Next: "Stop_End",
  304. },
  305. {
  306. ID: "Set_LogError",
  307. Target: "$lastError",
  308. Value: "=_error",
  309. Next: "Stop_End",
  310. },
  311. {
  312. ID: "Stop_End",
  313. },
  314. },
  315. }
  316. adapters := createTestAdapters()
  317. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  318. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  319. return nil, &workflow.LLMError{
  320. Type: "rate_limit",
  321. Code: "RATE_LIMIT",
  322. Message: "Rate limit exceeded",
  323. Retryable: true,
  324. StatusCode: 429,
  325. Provider: "openai",
  326. Model: "gpt-4",
  327. }
  328. })
  329. engine, err := workflow.NewEngine(wf)
  330. if err != nil {
  331. t.Fatalf("Failed to create engine: %v", err)
  332. }
  333. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  334. if err != nil {
  335. t.Fatalf("Failed to execute workflow: %v", err)
  336. }
  337. drainEvents(result.RunEventStream)
  338. // Workflow should NOT have failed - onError handled it
  339. if result.Context.Status == workflow.StatusFailed {
  340. t.Fatal("Expected workflow to succeed (onError should handle the error)")
  341. }
  342. // $success should NOT be set (out mapping is skipped on error)
  343. if result.Context.Variables["$success"] != nil {
  344. t.Errorf("Expected $success to be nil (out mapping not applied on error), got %v", result.Context.Variables["$success"])
  345. }
  346. // $lastError should be set by the error handler
  347. lastError, ok := result.Context.Variables["$lastError"].(map[string]interface{})
  348. if !ok {
  349. t.Fatalf("Expected $lastError to be map, got %T: %v", result.Context.Variables["$lastError"], result.Context.Variables["$lastError"])
  350. }
  351. if lastError["type"] != "rate_limit" {
  352. t.Errorf("Expected error type 'rate_limit', got '%v'", lastError["type"])
  353. }
  354. if lastError["code"] != "RATE_LIMIT" {
  355. t.Errorf("Expected error code 'RATE_LIMIT', got '%v'", lastError["code"])
  356. }
  357. if lastError["message"] != "Rate limit exceeded" {
  358. t.Errorf("Expected error message 'Rate limit exceeded', got '%v'", lastError["message"])
  359. }
  360. if lastError["retryable"] != true {
  361. t.Errorf("Expected error retryable true, got %v", lastError["retryable"])
  362. }
  363. if lastError["status_code"] != 429 {
  364. t.Errorf("Expected status_code 429, got %v", lastError["status_code"])
  365. }
  366. }
  367. // TestV310ErrorWithoutOnErrorFails verifies that in v3.10, when an LLM call fails
  368. // and the step has no onError, the workflow still fails (backward-compatible)
  369. func TestV310ErrorWithoutOnErrorFails(t *testing.T) {
  370. wf := &workflow.Workflow{
  371. Version: "3.10",
  372. Name: "V310 Error Without OnError Test",
  373. Registry: workflow.Registry{
  374. Services: []string{},
  375. Components: []string{},
  376. Vars: []string{"$content(STRING)"},
  377. Files: workflow.FilesRegistry{},
  378. },
  379. Steps: []workflow.Step{
  380. {
  381. ID: "LLM_WillFail",
  382. In: workflow.StepInput{
  383. "messages": []interface{}{
  384. map[string]interface{}{
  385. "role": "user",
  386. "content": "This will fail",
  387. },
  388. },
  389. },
  390. Out: workflow.StepOutput{
  391. "$content": "=_result",
  392. },
  393. // No OnError - error should propagate
  394. Next: "Stop_End",
  395. },
  396. {
  397. ID: "Stop_End",
  398. },
  399. },
  400. }
  401. adapters := createTestAdapters()
  402. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  403. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  404. return nil, fmt.Errorf("connection refused")
  405. })
  406. engine, err := workflow.NewEngine(wf)
  407. if err != nil {
  408. t.Fatalf("Failed to create engine: %v", err)
  409. }
  410. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  411. if err != nil {
  412. t.Fatalf("Failed to execute workflow: %v", err)
  413. }
  414. drainEvents(result.RunEventStream)
  415. // Workflow should have failed
  416. if result.Context.Status != workflow.StatusFailed {
  417. t.Errorf("Expected workflow to fail, got status %s", result.Context.Status)
  418. }
  419. }
  420. // TestV310BackwardCompatV39 verifies that a v3.9 workflow still works with old _result semantics
  421. func TestV310BackwardCompatV39(t *testing.T) {
  422. wf := &workflow.Workflow{
  423. Version: "3.9",
  424. Name: "V39 Backward Compat Test",
  425. Registry: workflow.Registry{
  426. Services: []string{},
  427. Components: []string{},
  428. Vars: []string{"$content(STRING)", "$model(STRING)", "$tokens(NUMBER)"},
  429. Files: workflow.FilesRegistry{},
  430. },
  431. Steps: []workflow.Step{
  432. {
  433. ID: "LLM_Generate",
  434. In: workflow.StepInput{
  435. "messages": []interface{}{
  436. map[string]interface{}{
  437. "role": "user",
  438. "content": "Hello",
  439. },
  440. },
  441. },
  442. Out: workflow.StepOutput{
  443. // v3.9 semantics: _result is the full map for text output
  444. "$content": "=_result.content",
  445. "$model": "=_result.model",
  446. "$tokens": "=_result.usage.total_tokens",
  447. },
  448. Next: "Stop_End",
  449. },
  450. {
  451. ID: "Stop_End",
  452. },
  453. },
  454. }
  455. adapters := createTestAdapters()
  456. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  457. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  458. return map[string]interface{}{
  459. "content": "Hello there!",
  460. "model": "gpt-4",
  461. "finish_reason": "stop",
  462. "usage": map[string]interface{}{
  463. "prompt_tokens": 5,
  464. "completion_tokens": 3,
  465. "total_tokens": 8,
  466. },
  467. }, nil
  468. })
  469. engine, err := workflow.NewEngine(wf)
  470. if err != nil {
  471. t.Fatalf("Failed to create engine: %v", err)
  472. }
  473. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  474. if err != nil {
  475. t.Fatalf("Failed to execute workflow: %v", err)
  476. }
  477. drainEvents(result.RunEventStream)
  478. // v3.9: _result.content should work (full map semantics)
  479. if result.Context.Variables["$content"] != "Hello there!" {
  480. t.Errorf("Expected content 'Hello there!', got '%v'", result.Context.Variables["$content"])
  481. }
  482. if result.Context.Variables["$model"] != "gpt-4" {
  483. t.Errorf("Expected model 'gpt-4', got '%v'", result.Context.Variables["$model"])
  484. }
  485. if fmt.Sprintf("%v", result.Context.Variables["$tokens"]) != "8" {
  486. t.Errorf("Expected tokens 8, got %v", result.Context.Variables["$tokens"])
  487. }
  488. }
  489. // TestV310ErrorMetaPartiallyPopulated verifies that _meta is set (with latency)
  490. // even on LLM failure when onError is used
  491. func TestV310ErrorMetaPartiallyPopulated(t *testing.T) {
  492. wf := &workflow.Workflow{
  493. Version: "3.10",
  494. Name: "V310 Error Meta Test",
  495. Registry: workflow.Registry{
  496. Services: []string{},
  497. Components: []string{},
  498. Vars: []string{"$errorMeta(OBJECT)", "$errorInfo(OBJECT)"},
  499. Files: workflow.FilesRegistry{},
  500. },
  501. Steps: []workflow.Step{
  502. {
  503. ID: "LLM_WillFail",
  504. In: workflow.StepInput{
  505. "messages": []interface{}{
  506. map[string]interface{}{
  507. "role": "user",
  508. "content": "This will fail",
  509. },
  510. },
  511. },
  512. OnError: "Set_CaptureError",
  513. Next: "Stop_End",
  514. },
  515. {
  516. ID: "Set_CaptureError",
  517. Target: "$errorInfo",
  518. Value: "=_error",
  519. Next: "Stop_End",
  520. },
  521. {
  522. ID: "Stop_End",
  523. },
  524. },
  525. }
  526. adapters := createTestAdapters()
  527. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  528. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  529. return nil, &workflow.LLMError{
  530. Type: "timeout",
  531. Code: "TIMEOUT",
  532. Message: "Request timed out",
  533. Retryable: true,
  534. StatusCode: 408,
  535. Model: "gpt-4",
  536. }
  537. })
  538. engine, err := workflow.NewEngine(wf)
  539. if err != nil {
  540. t.Fatalf("Failed to create engine: %v", err)
  541. }
  542. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  543. if err != nil {
  544. t.Fatalf("Failed to execute workflow: %v", err)
  545. }
  546. drainEvents(result.RunEventStream)
  547. // Workflow should succeed (onError handled)
  548. if result.Context.Status == workflow.StatusFailed {
  549. t.Fatal("Expected workflow to succeed with onError handler")
  550. }
  551. // $errorInfo should have the error details
  552. errorInfo, ok := result.Context.Variables["$errorInfo"].(map[string]interface{})
  553. if !ok {
  554. t.Fatalf("Expected $errorInfo to be map, got %T: %v", result.Context.Variables["$errorInfo"], result.Context.Variables["$errorInfo"])
  555. }
  556. if errorInfo["type"] != "timeout" {
  557. t.Errorf("Expected error type 'timeout', got '%v'", errorInfo["type"])
  558. }
  559. if errorInfo["retryable"] != true {
  560. t.Errorf("Expected retryable true, got %v", errorInfo["retryable"])
  561. }
  562. }
  563. // TestV310ResultCleanup verifies that _result, _meta, _error are all cleaned up
  564. // after out evaluation
  565. func TestV310ResultCleanup(t *testing.T) {
  566. wf := &workflow.Workflow{
  567. Version: "3.10",
  568. Name: "V310 Cleanup Test",
  569. Registry: workflow.Registry{
  570. Services: []string{},
  571. Components: []string{},
  572. Vars: []string{"$content(STRING)"},
  573. Files: workflow.FilesRegistry{},
  574. },
  575. Steps: []workflow.Step{
  576. {
  577. ID: "LLM_First",
  578. In: workflow.StepInput{
  579. "messages": []interface{}{
  580. map[string]interface{}{
  581. "role": "user",
  582. "content": "Hello",
  583. },
  584. },
  585. },
  586. Out: workflow.StepOutput{
  587. "$content": "=_result",
  588. },
  589. Next: "Stop_End",
  590. },
  591. {
  592. ID: "Stop_End",
  593. },
  594. },
  595. }
  596. adapters := createTestAdapters()
  597. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  598. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  599. return map[string]interface{}{
  600. "content": "response",
  601. "model": "gpt-4",
  602. "finish_reason": "stop",
  603. }, nil
  604. })
  605. engine, err := workflow.NewEngine(wf)
  606. if err != nil {
  607. t.Fatalf("Failed to create engine: %v", err)
  608. }
  609. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  610. if err != nil {
  611. t.Fatalf("Failed to execute workflow: %v", err)
  612. }
  613. drainEvents(result.RunEventStream)
  614. // After workflow completes, local vars should be cleaned up
  615. if _, ok := result.Context.LocalVars["_result"]; ok {
  616. t.Error("Expected _result to be cleaned up after step execution")
  617. }
  618. if _, ok := result.Context.LocalVars["_meta"]; ok {
  619. t.Error("Expected _meta to be cleaned up after step execution")
  620. }
  621. if _, ok := result.Context.LocalVars["_error"]; ok {
  622. t.Error("Expected _error to be cleaned up after step execution")
  623. }
  624. }
  625. // TestV310OnErrorValidation verifies that onError referencing a non-existent step fails validation
  626. func TestV310OnErrorValidation(t *testing.T) {
  627. wf := &workflow.Workflow{
  628. Version: "3.10",
  629. Name: "V310 OnError Validation Test",
  630. Registry: workflow.Registry{
  631. Services: []string{},
  632. Components: []string{},
  633. Vars: []string{},
  634. Files: workflow.FilesRegistry{},
  635. },
  636. Steps: []workflow.Step{
  637. {
  638. ID: "LLM_Test",
  639. In: workflow.StepInput{
  640. "messages": []interface{}{},
  641. },
  642. OnError: "NonExistent_Step",
  643. Next: "Stop_End",
  644. },
  645. {
  646. ID: "Stop_End",
  647. },
  648. },
  649. }
  650. _, err := workflow.NewEngine(wf)
  651. if err == nil {
  652. t.Fatal("Expected validation error for non-existent onError step")
  653. }
  654. t.Logf("Got expected error: %v", err)
  655. }
  656. // TestV310ProviderInference verifies provider inference from model names
  657. func TestV310ProviderInference(t *testing.T) {
  658. tests := []struct {
  659. model string
  660. expected string
  661. }{
  662. {"gpt-4", "openai"},
  663. {"claude-3-opus", "anthropic"},
  664. {"gemini-pro", "google"},
  665. {"mistral-large", "mistral"},
  666. {"some-custom-model", "unknown"},
  667. }
  668. for _, tt := range tests {
  669. t.Run(tt.model, func(t *testing.T) {
  670. wf := &workflow.Workflow{
  671. Version: "3.10",
  672. Name: "Provider Test",
  673. Registry: workflow.Registry{
  674. Services: []string{},
  675. Components: []string{},
  676. Vars: []string{"$provider(STRING)"},
  677. Files: workflow.FilesRegistry{},
  678. },
  679. Steps: []workflow.Step{
  680. {
  681. ID: "LLM_Test",
  682. In: workflow.StepInput{
  683. "messages": []interface{}{
  684. map[string]interface{}{
  685. "role": "user",
  686. "content": "test",
  687. },
  688. },
  689. },
  690. Out: workflow.StepOutput{
  691. "$provider": "=_meta.provider",
  692. },
  693. Next: "Stop_End",
  694. },
  695. {
  696. ID: "Stop_End",
  697. },
  698. },
  699. }
  700. adapters := createTestAdapters()
  701. llmAdapter := adapters.LLM.(*workflow.DefaultLLMAdapter)
  702. llmAdapter.SetHandler(func(ctx context.Context, params map[string]interface{}, stream chan<- string) (map[string]interface{}, error) {
  703. return map[string]interface{}{
  704. "content": "ok",
  705. "model": tt.model,
  706. }, nil
  707. })
  708. engine, err := workflow.NewEngine(wf)
  709. if err != nil {
  710. t.Fatalf("Failed to create engine: %v", err)
  711. }
  712. result, err := engine.Execute(context.Background(), map[string]interface{}{}, adapters)
  713. if err != nil {
  714. t.Fatalf("Failed to execute: %v", err)
  715. }
  716. drainEvents(result.RunEventStream)
  717. provider := result.Context.Variables["$provider"]
  718. if provider != tt.expected {
  719. t.Errorf("Expected provider '%s' for model '%s', got '%v'", tt.expected, tt.model, provider)
  720. }
  721. })
  722. }
  723. }