v314_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. package workflow_test
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "context"
  6. "fmt"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "workflow"
  11. )
  12. // ── helpers ──────────────────────────────────────────────────────────────────
  13. // makeZip builds an in-memory zip archive from a map of filename→content.
  14. func makeZip(files map[string][]byte) []byte {
  15. var buf bytes.Buffer
  16. w := zip.NewWriter(&buf)
  17. for name, data := range files {
  18. f, _ := w.Create(name)
  19. f.Write(data)
  20. }
  21. w.Close()
  22. return buf.Bytes()
  23. }
  24. // v314Registry returns a standard registry allowing .tmp/**, artifacts/**,
  25. // ExtComponents/**, Services/**, and Sections/** writes.
  26. func v314Registry() workflow.Registry {
  27. return workflow.Registry{
  28. Services: []string{},
  29. Components: []string{},
  30. Vars: []string{},
  31. Files: workflow.FilesRegistry{
  32. Artifacts: []string{
  33. ".tmp/**",
  34. "artifacts/**",
  35. "ExtComponents/**",
  36. "Services/**",
  37. "Sections/**",
  38. },
  39. },
  40. }
  41. }
  42. // v314Adapters wraps a FileAdapter in the full Adapters struct.
  43. func v314Adapters(fa workflow.FileAdapter) *workflow.Adapters {
  44. return &workflow.Adapters{
  45. Service: workflow.NewDefaultServiceAdapter(),
  46. Component: workflow.NewDefaultComponentAdapter(),
  47. LLM: workflow.NewDefaultLLMAdapter(),
  48. File: fa,
  49. }
  50. }
  51. func mustEngineV314(t *testing.T, wf *workflow.Workflow) *workflow.Engine {
  52. t.Helper()
  53. eng, err := workflow.NewEngine(wf)
  54. if err != nil {
  55. t.Fatalf("NewEngine: %v", err)
  56. }
  57. return eng
  58. }
  59. // mustExecuteV314 starts execution and blocks until the workflow finishes.
  60. func mustExecuteV314(t *testing.T, eng *workflow.Engine, adapters *workflow.Adapters) *workflow.ExecutionResult {
  61. t.Helper()
  62. result, err := eng.Execute(context.Background(), map[string]interface{}{}, adapters)
  63. if err != nil {
  64. t.Fatalf("Execute: %v", err)
  65. }
  66. drainEvents(result.RunEventStream) // wait for goroutine to finish
  67. return result
  68. }
  69. // ── Write_* prepend (v3.14) ───────────────────────────────────────────────
  70. func TestV314_WritePrepend(t *testing.T) {
  71. fa := workflow.NewDefaultFileAdapter()
  72. fa.SetFile("artifacts/file.txt", []byte("world"))
  73. wf := &workflow.Workflow{
  74. Version: "3.14",
  75. Name: "Prepend Test",
  76. Registry: v314Registry(),
  77. Steps: []workflow.Step{
  78. {
  79. ID: "Write_Prepend",
  80. Target: "artifacts/file.txt",
  81. Value: "hello ",
  82. Mode: "prepend",
  83. Next: "Stop_Done",
  84. },
  85. {ID: "Stop_Done"},
  86. },
  87. }
  88. mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))
  89. got := string(fa.GetFile("artifacts/file.txt"))
  90. if want := "hello world"; got != want {
  91. t.Errorf("prepend: got %q, want %q", got, want)
  92. }
  93. }
  94. // ── .tmp/ path isolation (v3.14) ─────────────────────────────────────────
  95. func TestV314_TmpPathIsolation(t *testing.T) {
  96. fa := workflow.NewDefaultFileAdapter()
  97. wf := &workflow.Workflow{
  98. Version: "3.14",
  99. Name: "Tmp Path Test",
  100. Registry: v314Registry(),
  101. Steps: []workflow.Step{
  102. {
  103. ID: "Write_Tmp",
  104. Target: ".tmp/bundle.zip",
  105. Value: "data",
  106. Next: "Stop_Done",
  107. },
  108. {ID: "Stop_Done"},
  109. },
  110. }
  111. eng := mustEngineV314(t, wf)
  112. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  113. if err != nil {
  114. t.Fatalf("Execute: %v", err)
  115. }
  116. drainEvents(result.RunEventStream) // wait for workflow to finish
  117. runID := result.Context.WorkflowID
  118. expectedPath := ".tmp/" + runID + "/bundle.zip"
  119. if fa.GetFile(expectedPath) == nil {
  120. t.Errorf(".tmp/ not isolated: expected file at %s", expectedPath)
  121. }
  122. // Bare unresolved path must NOT exist
  123. if fa.GetFile(".tmp/bundle.zip") != nil {
  124. t.Errorf(".tmp/ isolation: bare path should not be written")
  125. }
  126. }
  127. // ── _iterDir injection in parallel loop (v3.14) ──────────────────────────
  128. func TestV314_IterDirParallel(t *testing.T) {
  129. fa := workflow.NewDefaultFileAdapter()
  130. wf := &workflow.Workflow{
  131. Version: "3.14",
  132. Name: "IterDir Parallel Test",
  133. Registry: workflow.Registry{
  134. Services: []string{},
  135. Components: []string{},
  136. Vars: []string{"$items([STRING])"},
  137. Files: workflow.FilesRegistry{Artifacts: []string{".tmp/**"}},
  138. },
  139. Steps: []workflow.Step{
  140. {ID: "Set_Items", Target: "$items", Value: `=["a","b","c"]`, Next: "Loop_Process"},
  141. {
  142. ID: "Loop_Process",
  143. Source: "=$items",
  144. Mode: "parallel",
  145. Children: []string{"Write_TmpFile"},
  146. Next: "Stop_Done",
  147. },
  148. {ID: "Write_TmpFile", Target: ".tmp/{_iterDir}/item.txt", Value: "=_item", Next: "RETURN"},
  149. {ID: "Stop_Done"},
  150. },
  151. }
  152. eng := mustEngineV314(t, wf)
  153. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  154. if err != nil {
  155. t.Fatalf("Execute: %v", err)
  156. }
  157. drainEvents(result.RunEventStream) // wait for all parallel goroutines to finish
  158. runID := result.Context.WorkflowID
  159. for i, want := range []string{"a", "b", "c"} {
  160. iterDir := fmt.Sprintf("Loop_Process_%d", i)
  161. path := ".tmp/" + runID + "/" + iterDir + "/item.txt"
  162. got := fa.GetFile(path)
  163. if got == nil {
  164. t.Errorf("missing file for iteration %d at %s", i, path)
  165. continue
  166. }
  167. if string(got) != want {
  168. t.Errorf("iteration %d: got %q, want %q", i, string(got), want)
  169. }
  170. }
  171. }
  172. // ── _iterDir injection in serial loop (v3.14) ────────────────────────────
  173. func TestV314_IterDirSerial(t *testing.T) {
  174. fa := workflow.NewDefaultFileAdapter()
  175. wf := &workflow.Workflow{
  176. Version: "3.14",
  177. Name: "IterDir Serial Test",
  178. Registry: workflow.Registry{
  179. Services: []string{},
  180. Components: []string{},
  181. Vars: []string{"$items([STRING])"},
  182. Files: workflow.FilesRegistry{Artifacts: []string{".tmp/**"}},
  183. },
  184. Steps: []workflow.Step{
  185. {ID: "Set_Items", Target: "$items", Value: `=["x","y"]`, Next: "Loop_Serial"},
  186. {
  187. ID: "Loop_Serial",
  188. Source: "=$items",
  189. Mode: "serial",
  190. Children: []string{"Write_TmpFile"},
  191. Next: "Stop_Done",
  192. },
  193. {ID: "Write_TmpFile", Target: ".tmp/{_iterDir}/item.txt", Value: "=_item", Next: "RETURN"},
  194. {ID: "Stop_Done"},
  195. },
  196. }
  197. eng := mustEngineV314(t, wf)
  198. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  199. if err != nil {
  200. t.Fatalf("Execute: %v", err)
  201. }
  202. drainEvents(result.RunEventStream) // wait for workflow to finish
  203. runID := result.Context.WorkflowID
  204. for i, want := range []string{"x", "y"} {
  205. iterDir := fmt.Sprintf("Loop_Serial_%d", i)
  206. path := ".tmp/" + runID + "/" + iterDir + "/item.txt"
  207. got := fa.GetFile(path)
  208. if got == nil {
  209. t.Errorf("missing file for serial iteration %d at %s", i, path)
  210. continue
  211. }
  212. if string(got) != want {
  213. t.Errorf("serial iteration %d: got %q, want %q", i, string(got), want)
  214. }
  215. }
  216. }
  217. // ── Download_* with explicit target (v3.14) ──────────────────────────────
  218. func TestV314_DownloadTarget(t *testing.T) {
  219. content := []byte("file-content-abc")
  220. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  221. w.Write(content)
  222. }))
  223. defer srv.Close()
  224. fa := workflow.NewDefaultFileAdapter()
  225. wf := &workflow.Workflow{
  226. Version: "3.14",
  227. Name: "Download Target Test",
  228. Registry: v314Registry(),
  229. Steps: []workflow.Step{
  230. {
  231. ID: "Download_File",
  232. Source: fmt.Sprintf("%s/file.cp", srv.URL),
  233. Target: "artifacts/downloaded.cp",
  234. Next: "Stop_Done",
  235. },
  236. {ID: "Stop_Done"},
  237. },
  238. }
  239. mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))
  240. got := fa.GetFile("artifacts/downloaded.cp")
  241. if !bytes.Equal(got, content) {
  242. t.Errorf("download content mismatch: got %q, want %q", got, content)
  243. }
  244. }
  245. // ── Download_* with routeByExt (v3.14) ───────────────────────────────────
  246. func TestV314_DownloadRouteByExt(t *testing.T) {
  247. content := []byte("component-data")
  248. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  249. w.Write(content)
  250. }))
  251. defer srv.Close()
  252. fa := workflow.NewDefaultFileAdapter()
  253. wf := &workflow.Workflow{
  254. Version: "3.14",
  255. Name: "Download RouteByExt Test",
  256. Registry: v314Registry(),
  257. Steps: []workflow.Step{
  258. {
  259. ID: "Download_Cp",
  260. Source: fmt.Sprintf("%s/MyWidget.cp", srv.URL),
  261. RouteByExt: map[string]string{
  262. ".cp": "ExtComponents/",
  263. ".vs": "Services/",
  264. },
  265. DefaultDir: "artifacts/misc/",
  266. Next: "Stop_Done",
  267. },
  268. {ID: "Stop_Done"},
  269. },
  270. }
  271. mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))
  272. got := fa.GetFile("ExtComponents/MyWidget.cp")
  273. if !bytes.Equal(got, content) {
  274. t.Errorf("routeByExt content mismatch: got %q, want %q", got, content)
  275. }
  276. }
  277. // ── Unzip_* with routeByExt (v3.14) ──────────────────────────────────────
  278. func TestV314_UnzipRouteByExt(t *testing.T) {
  279. zipData := makeZip(map[string][]byte{
  280. "Widget.cp": []byte("cp-content"),
  281. "Service.vs": []byte("vs-content"),
  282. "Readme.txt": []byte("readme"),
  283. })
  284. fa := workflow.NewDefaultFileAdapter()
  285. fa.SetFile("artifacts/bundle.zip", zipData)
  286. wf := &workflow.Workflow{
  287. Version: "3.14",
  288. Name: "Unzip RouteByExt Test",
  289. Registry: v314Registry(),
  290. Steps: []workflow.Step{
  291. {
  292. ID: "Unzip_Bundle",
  293. Source: "artifacts/bundle.zip",
  294. RouteByExt: map[string]string{
  295. ".cp": "ExtComponents/",
  296. ".vs": "Services/",
  297. },
  298. DefaultDir: "artifacts/misc/",
  299. Next: "Stop_Done",
  300. },
  301. {ID: "Stop_Done"},
  302. },
  303. }
  304. mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))
  305. cases := []struct{ path, want string }{
  306. {"ExtComponents/Widget.cp", "cp-content"},
  307. {"Services/Service.vs", "vs-content"},
  308. {"artifacts/misc/Readme.txt", "readme"},
  309. }
  310. for _, c := range cases {
  311. got := string(fa.GetFile(c.path))
  312. if got != c.want {
  313. t.Errorf("%s: got %q, want %q", c.path, got, c.want)
  314. }
  315. }
  316. }
  317. // ── Unzip_* zip-slip protection (v3.14) ──────────────────────────────────
  318. func TestV314_UnzipZipSlipRejected(t *testing.T) {
  319. var buf bytes.Buffer
  320. zw := zip.NewWriter(&buf)
  321. f, _ := zw.Create("../../etc/passwd")
  322. f.Write([]byte("root:x:0:0"))
  323. zw.Close()
  324. fa := workflow.NewDefaultFileAdapter()
  325. fa.SetFile("artifacts/evil.zip", buf.Bytes())
  326. wf := &workflow.Workflow{
  327. Version: "3.14",
  328. Name: "ZipSlip Test",
  329. Registry: v314Registry(),
  330. Steps: []workflow.Step{
  331. {
  332. ID: "Unzip_Evil",
  333. Source: "artifacts/evil.zip",
  334. RouteByExt: map[string]string{},
  335. DefaultDir: "artifacts/out/",
  336. Next: "Stop_Done",
  337. },
  338. {ID: "Stop_Done"},
  339. },
  340. }
  341. eng := mustEngineV314(t, wf)
  342. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  343. if err != nil {
  344. t.Fatalf("Execute (start): %v", err)
  345. }
  346. drainEvents(result.RunEventStream) // wait for workflow to finish
  347. if result.Context.Status != workflow.StatusFailed {
  348. t.Errorf("expected workflow to fail for zip-slip entry, got status %q", result.Context.Status)
  349. }
  350. }
  351. // ── Unzip_* overwrite=false (v3.14) ──────────────────────────────────────
  352. func TestV314_UnzipNoOverwrite(t *testing.T) {
  353. zipData := makeZip(map[string][]byte{
  354. "file.cp": []byte("new-content"),
  355. })
  356. fa := workflow.NewDefaultFileAdapter()
  357. fa.SetFile("artifacts/bundle.zip", zipData)
  358. fa.SetFile("ExtComponents/file.cp", []byte("original"))
  359. overwriteFalse := false
  360. wf := &workflow.Workflow{
  361. Version: "3.14",
  362. Name: "Unzip No Overwrite Test",
  363. Registry: v314Registry(),
  364. Steps: []workflow.Step{
  365. {
  366. ID: "Unzip_Bundle",
  367. Source: "artifacts/bundle.zip",
  368. RouteByExt: map[string]string{".cp": "ExtComponents/"},
  369. Overwrite: &overwriteFalse,
  370. Next: "Stop_Done",
  371. },
  372. {ID: "Stop_Done"},
  373. },
  374. }
  375. eng := mustEngineV314(t, wf)
  376. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  377. if err != nil {
  378. t.Fatalf("Execute (start): %v", err)
  379. }
  380. drainEvents(result.RunEventStream) // wait for workflow to finish
  381. if result.Context.Status != workflow.StatusFailed {
  382. t.Errorf("expected workflow to fail when overwrite=false on existing file, got status %q", result.Context.Status)
  383. }
  384. if got := string(fa.GetFile("ExtComponents/file.cp")); got != "original" {
  385. t.Errorf("original file overwritten despite overwrite=false, got %q", got)
  386. }
  387. }
  388. // ── Download_* → Unzip_* full pipeline (v3.14) ───────────────────────────
  389. func TestV314_DownloadThenUnzip(t *testing.T) {
  390. zipData := makeZip(map[string][]byte{
  391. "Alpha.cp": []byte("alpha"),
  392. "Beta.vs": []byte("beta"),
  393. })
  394. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  395. w.Write(zipData)
  396. }))
  397. defer srv.Close()
  398. fa := workflow.NewDefaultFileAdapter()
  399. wf := &workflow.Workflow{
  400. Version: "3.14",
  401. Name: "Download+Unzip Pipeline Test",
  402. Registry: v314Registry(),
  403. Steps: []workflow.Step{
  404. {
  405. ID: "Download_Bundle",
  406. Source: fmt.Sprintf("%s/bundle.zip", srv.URL),
  407. Target: ".tmp/bundle.zip",
  408. Next: "Unzip_Bundle",
  409. },
  410. {
  411. ID: "Unzip_Bundle",
  412. Source: ".tmp/bundle.zip",
  413. RouteByExt: map[string]string{
  414. ".cp": "ExtComponents/",
  415. ".vs": "Services/",
  416. },
  417. Next: "Stop_Done",
  418. },
  419. {ID: "Stop_Done"},
  420. },
  421. }
  422. eng := mustEngineV314(t, wf)
  423. result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  424. if err != nil {
  425. t.Fatalf("Execute: %v", err)
  426. }
  427. drainEvents(result.RunEventStream) // wait for workflow to finish
  428. runID := result.Context.WorkflowID
  429. tmpZip := ".tmp/" + runID + "/bundle.zip"
  430. if fa.GetFile(tmpZip) == nil {
  431. t.Errorf("expected zip at isolated path %s", tmpZip)
  432. }
  433. if got := fa.GetFile("ExtComponents/Alpha.cp"); !bytes.Equal(got, []byte("alpha")) {
  434. t.Errorf("Alpha.cp: got %q, want %q", got, "alpha")
  435. }
  436. if got := fa.GetFile("Services/Beta.vs"); !bytes.Equal(got, []byte("beta")) {
  437. t.Errorf("Beta.vs: got %q, want %q", got, "beta")
  438. }
  439. }
  440. // ── Version 3.14 accepted by validator ───────────────────────────────────
  441. func TestV314_VersionAccepted(t *testing.T) {
  442. wf := &workflow.Workflow{
  443. Version: "3.14",
  444. Name: "Version Test",
  445. Registry: v314Registry(),
  446. Steps: []workflow.Step{{ID: "Stop_Done"}},
  447. }
  448. if err := wf.Validate(); err != nil {
  449. t.Errorf("version 3.14 should be accepted: %v", err)
  450. }
  451. }
  452. // ── Validation: Download_* missing source ────────────────────────────────
  453. func TestV314_Validate_DownloadMissingSource(t *testing.T) {
  454. wf := &workflow.Workflow{
  455. Version: "3.14",
  456. Name: "Validate Download No Source",
  457. Registry: v314Registry(),
  458. Steps: []workflow.Step{
  459. {ID: "Download_File", Target: "artifacts/file.txt", Next: "Stop_Done"}, // no Source
  460. {ID: "Stop_Done"},
  461. },
  462. }
  463. if err := wf.Validate(); err == nil {
  464. t.Error("expected validation error for Download_* without source, got nil")
  465. }
  466. }
  467. // ── Validation: Download_* missing target and routeByExt ─────────────────
  468. func TestV314_Validate_DownloadMissingRoute(t *testing.T) {
  469. wf := &workflow.Workflow{
  470. Version: "3.14",
  471. Name: "Validate Download No Route",
  472. Registry: v314Registry(),
  473. Steps: []workflow.Step{
  474. // Has source but neither target nor routeByExt/defaultDir
  475. {ID: "Download_File", Source: "http://example.com/file.zip", Next: "Stop_Done"},
  476. {ID: "Stop_Done"},
  477. },
  478. }
  479. if err := wf.Validate(); err == nil {
  480. t.Error("expected validation error for Download_* without target/routeByExt, got nil")
  481. }
  482. }
  483. // ── Validation: Download_* both target and routeByExt (mutually exclusive) ─
  484. func TestV314_Validate_DownloadBothTargetAndRoute(t *testing.T) {
  485. wf := &workflow.Workflow{
  486. Version: "3.14",
  487. Name: "Validate Download Both Target and Route",
  488. Registry: v314Registry(),
  489. Steps: []workflow.Step{
  490. {
  491. ID: "Download_File",
  492. Source: "http://example.com/file.zip",
  493. Target: "artifacts/file.zip",
  494. RouteByExt: map[string]string{".zip": "artifacts/"},
  495. Next: "Stop_Done",
  496. },
  497. {ID: "Stop_Done"},
  498. },
  499. }
  500. if err := wf.Validate(); err == nil {
  501. t.Error("expected validation error when both target and routeByExt are set, got nil")
  502. }
  503. }
  504. // ── Validation: Unzip_* missing source ───────────────────────────────────
  505. func TestV314_Validate_UnzipMissingSource(t *testing.T) {
  506. wf := &workflow.Workflow{
  507. Version: "3.14",
  508. Name: "Validate Unzip No Source",
  509. Registry: v314Registry(),
  510. Steps: []workflow.Step{
  511. {
  512. ID: "Unzip_Bundle",
  513. RouteByExt: map[string]string{".cp": "ExtComponents/"},
  514. Next: "Stop_Done",
  515. }, // no Source
  516. {ID: "Stop_Done"},
  517. },
  518. }
  519. if err := wf.Validate(); err == nil {
  520. t.Error("expected validation error for Unzip_* without source, got nil")
  521. }
  522. }
  523. // ── Validation: Unzip_* missing routeByExt ───────────────────────────────
  524. func TestV314_Validate_UnzipMissingRouteByExt(t *testing.T) {
  525. wf := &workflow.Workflow{
  526. Version: "3.14",
  527. Name: "Validate Unzip No RouteByExt",
  528. Registry: v314Registry(),
  529. Steps: []workflow.Step{
  530. {
  531. ID: "Unzip_Bundle",
  532. Source: "artifacts/bundle.zip",
  533. // RouteByExt not set (nil)
  534. Next: "Stop_Done",
  535. },
  536. {ID: "Stop_Done"},
  537. },
  538. }
  539. if err := wf.Validate(); err == nil {
  540. t.Error("expected validation error for Unzip_* without routeByExt, got nil")
  541. }
  542. }
  543. // ── Unzip_* out mapping captures extraction summary ──────────────────────
  544. func TestV314_UnzipOutMapping(t *testing.T) {
  545. zipData := makeZip(map[string][]byte{
  546. "A.cp": []byte("a-content"),
  547. "B.cp": []byte("b-content"),
  548. })
  549. fa := workflow.NewDefaultFileAdapter()
  550. fa.SetFile("artifacts/pkg.zip", zipData)
  551. wf := &workflow.Workflow{
  552. Version: "3.14",
  553. Name: "Unzip Out Mapping Test",
  554. Registry: workflow.Registry{
  555. Services: []string{},
  556. Components: []string{},
  557. Vars: []string{"$unzipCount(NUMBER)", "$unzipFiles([STRING])"},
  558. Files: workflow.FilesRegistry{
  559. Artifacts: []string{"artifacts/**", "ExtComponents/**"},
  560. },
  561. },
  562. Steps: []workflow.Step{
  563. {
  564. ID: "Unzip_Bundle",
  565. Source: "artifacts/pkg.zip",
  566. RouteByExt: map[string]string{
  567. ".cp": "ExtComponents/",
  568. },
  569. Out: workflow.StepOutput{
  570. "$unzipCount": "=_result.count",
  571. "$unzipFiles": "=_result.files",
  572. },
  573. Next: "Stop_Done",
  574. },
  575. {ID: "Stop_Done"},
  576. },
  577. }
  578. eng := mustEngineV314(t, wf)
  579. res, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  580. if err != nil {
  581. t.Fatalf("Execute: %v", err)
  582. }
  583. drainEvents(res.RunEventStream)
  584. // Verify count was mapped (extractedPaths has 2 entries)
  585. count := res.Context.Variables["$unzipCount"]
  586. if count == nil {
  587. t.Error("$unzipCount should be set after out mapping")
  588. }
  589. // Verify files list was mapped (non-nil, and the correct count)
  590. files := res.Context.Variables["$unzipFiles"]
  591. if files == nil {
  592. t.Error("$unzipFiles should be set after out mapping")
  593. }
  594. }
  595. // ── Download_* out mapping captures written path ──────────────────────────
  596. func TestV314_DownloadOutMapping(t *testing.T) {
  597. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  598. w.Write([]byte("download-content"))
  599. }))
  600. defer srv.Close()
  601. fa := workflow.NewDefaultFileAdapter()
  602. wf := &workflow.Workflow{
  603. Version: "3.14",
  604. Name: "Download Out Mapping Test",
  605. Registry: workflow.Registry{
  606. Services: []string{},
  607. Components: []string{},
  608. Vars: []string{"$downloadedPath(STRING)"},
  609. Files: workflow.FilesRegistry{
  610. Artifacts: []string{"artifacts/**"},
  611. },
  612. },
  613. Steps: []workflow.Step{
  614. {
  615. ID: "Download_File",
  616. Source: fmt.Sprintf("%s/widget.cp", srv.URL),
  617. Target: "artifacts/widget.cp",
  618. Out: workflow.StepOutput{
  619. "$downloadedPath": "=_result.path",
  620. },
  621. Next: "Stop_Done",
  622. },
  623. {ID: "Stop_Done"},
  624. },
  625. }
  626. eng := mustEngineV314(t, wf)
  627. res, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
  628. if err != nil {
  629. t.Fatalf("Execute: %v", err)
  630. }
  631. drainEvents(res.RunEventStream)
  632. got, ok := res.Context.Variables["$downloadedPath"].(string)
  633. if !ok || got == "" {
  634. t.Errorf("$downloadedPath: got %v (%T), want non-empty string",
  635. res.Context.Variables["$downloadedPath"], res.Context.Variables["$downloadedPath"])
  636. }
  637. if got != "artifacts/widget.cp" {
  638. t.Errorf("$downloadedPath: got %q, want %q", got, "artifacts/widget.cp")
  639. }
  640. }