@diff-review-system/drs 4.0.0-rc.4 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.pi/agents/describe/pr-describer.md +14 -0
  2. package/.pi/agents/review/unified-reviewer.md +31 -1
  3. package/.pi/agents/task/agents-md-updater.md +3 -1
  4. package/.pi/agents/task/review-issue-fixer.md +18 -1
  5. package/.pi/agents/visual/pr-explainer.md +205 -0
  6. package/.pi/workflows/github-pr-describe.yaml +10 -7
  7. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +148 -0
  8. package/.pi/workflows/github-pr-post-comment.yaml +10 -10
  9. package/.pi/workflows/github-pr-review-post.yaml +19 -8
  10. package/.pi/workflows/github-pr-review.yaml +348 -7
  11. package/.pi/workflows/github-pr-show-changes.yaml +8 -8
  12. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +103 -0
  13. package/.pi/workflows/github-pr-visual-explain.yaml +35 -0
  14. package/.pi/workflows/gitlab-mr-describe.yaml +8 -5
  15. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +144 -0
  16. package/.pi/workflows/gitlab-mr-post-comment.yaml +8 -8
  17. package/.pi/workflows/gitlab-mr-review.yaml +348 -5
  18. package/.pi/workflows/gitlab-mr-show-changes.yaml +6 -6
  19. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +100 -0
  20. package/.pi/workflows/gitlab-mr-visual-explain.yaml +33 -0
  21. package/.pi/workflows/local-fix-review-issues.yaml +82 -13
  22. package/.pi/workflows/local-review.yaml +9 -2
  23. package/.pi/workflows/local-update-agents-md.yaml +1 -1
  24. package/.pi/workflows/local-visual-explain.yaml +31 -0
  25. package/.pi/workflows/release-changelog-finalize.yaml +47 -0
  26. package/.pi/workflows/tag-changelog-update.yaml +4 -4
  27. package/README.md +91 -27
  28. package/dist/ci/runner.d.ts.map +1 -1
  29. package/dist/ci/runner.js +3 -1
  30. package/dist/ci/runner.js.map +1 -1
  31. package/dist/cli/index.js +48 -6
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/run-agent.d.ts +2 -0
  34. package/dist/cli/run-agent.d.ts.map +1 -1
  35. package/dist/cli/run-agent.js +4 -0
  36. package/dist/cli/run-agent.js.map +1 -1
  37. package/dist/cli/workflow.d.ts +56 -2
  38. package/dist/cli/workflow.d.ts.map +1 -1
  39. package/dist/cli/workflow.js +2165 -85
  40. package/dist/cli/workflow.js.map +1 -1
  41. package/dist/github/client.d.ts +12 -0
  42. package/dist/github/client.d.ts.map +1 -1
  43. package/dist/github/client.js +27 -0
  44. package/dist/github/client.js.map +1 -1
  45. package/dist/github/platform-adapter.d.ts +6 -1
  46. package/dist/github/platform-adapter.d.ts.map +1 -1
  47. package/dist/github/platform-adapter.js +84 -8
  48. package/dist/github/platform-adapter.js.map +1 -1
  49. package/dist/gitlab/client.d.ts +11 -0
  50. package/dist/gitlab/client.d.ts.map +1 -1
  51. package/dist/gitlab/client.js +11 -0
  52. package/dist/gitlab/client.js.map +1 -1
  53. package/dist/gitlab/platform-adapter.d.ts +3 -1
  54. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  55. package/dist/gitlab/platform-adapter.js +32 -1
  56. package/dist/gitlab/platform-adapter.js.map +1 -1
  57. package/dist/lib/comment-formatter.d.ts +8 -0
  58. package/dist/lib/comment-formatter.d.ts.map +1 -1
  59. package/dist/lib/comment-formatter.js +12 -4
  60. package/dist/lib/comment-formatter.js.map +1 -1
  61. package/dist/lib/comment-poster.d.ts.map +1 -1
  62. package/dist/lib/comment-poster.js +28 -1
  63. package/dist/lib/comment-poster.js.map +1 -1
  64. package/dist/lib/config.d.ts +50 -11
  65. package/dist/lib/config.d.ts.map +1 -1
  66. package/dist/lib/config.js +163 -28
  67. package/dist/lib/config.js.map +1 -1
  68. package/dist/lib/context-compression.d.ts +10 -0
  69. package/dist/lib/context-compression.d.ts.map +1 -1
  70. package/dist/lib/context-compression.js +101 -13
  71. package/dist/lib/context-compression.js.map +1 -1
  72. package/dist/lib/context-loader.d.ts +2 -1
  73. package/dist/lib/context-loader.d.ts.map +1 -1
  74. package/dist/lib/context-loader.js +70 -1
  75. package/dist/lib/context-loader.js.map +1 -1
  76. package/dist/lib/describe-core.d.ts.map +1 -1
  77. package/dist/lib/describe-core.js +3 -2
  78. package/dist/lib/describe-core.js.map +1 -1
  79. package/dist/lib/diff-lines.d.ts +9 -0
  80. package/dist/lib/diff-lines.d.ts.map +1 -1
  81. package/dist/lib/diff-lines.js +17 -9
  82. package/dist/lib/diff-lines.js.map +1 -1
  83. package/dist/lib/exit.js +4 -4
  84. package/dist/lib/exit.js.map +1 -1
  85. package/dist/lib/html-artifact.d.ts +14 -0
  86. package/dist/lib/html-artifact.d.ts.map +1 -0
  87. package/dist/lib/html-artifact.js +59 -0
  88. package/dist/lib/html-artifact.js.map +1 -0
  89. package/dist/lib/issue-parser.js +3 -3
  90. package/dist/lib/issue-parser.js.map +1 -1
  91. package/dist/lib/json-output-schema.d.ts +70 -0
  92. package/dist/lib/json-output-schema.d.ts.map +1 -1
  93. package/dist/lib/json-output-schema.js +40 -0
  94. package/dist/lib/json-output-schema.js.map +1 -1
  95. package/dist/lib/platform-client.d.ts +26 -0
  96. package/dist/lib/platform-client.d.ts.map +1 -1
  97. package/dist/lib/review-artifact.d.ts +69 -0
  98. package/dist/lib/review-artifact.d.ts.map +1 -0
  99. package/dist/lib/review-artifact.js +171 -0
  100. package/dist/lib/review-artifact.js.map +1 -0
  101. package/dist/lib/review-core.d.ts +6 -4
  102. package/dist/lib/review-core.d.ts.map +1 -1
  103. package/dist/lib/review-core.js +71 -151
  104. package/dist/lib/review-core.js.map +1 -1
  105. package/dist/lib/review-orchestrator.d.ts +23 -0
  106. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  107. package/dist/lib/review-orchestrator.js +20 -13
  108. package/dist/lib/review-orchestrator.js.map +1 -1
  109. package/dist/lib/review-usage.d.ts +4 -0
  110. package/dist/lib/review-usage.d.ts.map +1 -1
  111. package/dist/lib/review-usage.js +25 -0
  112. package/dist/lib/review-usage.js.map +1 -1
  113. package/dist/lib/trace-collector.d.ts +105 -0
  114. package/dist/lib/trace-collector.d.ts.map +1 -0
  115. package/dist/lib/trace-collector.js +255 -0
  116. package/dist/lib/trace-collector.js.map +1 -0
  117. package/dist/lib/trace-html.d.ts +3 -0
  118. package/dist/lib/trace-html.d.ts.map +1 -0
  119. package/dist/lib/trace-html.js +349 -0
  120. package/dist/lib/trace-html.js.map +1 -0
  121. package/dist/lib/workflow-artifacts.d.ts +54 -0
  122. package/dist/lib/workflow-artifacts.d.ts.map +1 -0
  123. package/dist/lib/workflow-artifacts.js +150 -0
  124. package/dist/lib/workflow-artifacts.js.map +1 -0
  125. package/dist/pi/sdk.d.ts.map +1 -1
  126. package/dist/pi/sdk.js +570 -6
  127. package/dist/pi/sdk.js.map +1 -1
  128. package/dist/runtime/agent-loader.js +2 -2
  129. package/dist/runtime/client.d.ts +2 -0
  130. package/dist/runtime/client.d.ts.map +1 -1
  131. package/dist/runtime/client.js +11 -5
  132. package/dist/runtime/client.js.map +1 -1
  133. package/package.json +21 -15
  134. package/.pi/workflows/github-pr-describe-post.yaml +0 -24
  135. package/.pi/workflows/gitlab-mr-describe-post.yaml +0 -22
  136. package/.pi/workflows/gitlab-mr-review-code-quality.yaml +0 -31
  137. package/.pi/workflows/gitlab-mr-review-post-code-quality.yaml +0 -40
  138. package/.pi/workflows/gitlab-mr-review-post.yaml +0 -30
  139. package/.pi/workflows/local-staged-review.yaml +0 -17
  140. package/dist/cli/run-agent.test.d.ts +0 -2
  141. package/dist/cli/run-agent.test.d.ts.map +0 -1
  142. package/dist/cli/run-agent.test.js +0 -204
  143. package/dist/cli/run-agent.test.js.map +0 -1
  144. package/dist/cli/workflow.test.d.ts +0 -2
  145. package/dist/cli/workflow.test.d.ts.map +0 -1
  146. package/dist/cli/workflow.test.js +0 -1410
  147. package/dist/cli/workflow.test.js.map +0 -1
  148. package/dist/github/client.test.d.ts +0 -2
  149. package/dist/github/client.test.d.ts.map +0 -1
  150. package/dist/github/client.test.js +0 -206
  151. package/dist/github/client.test.js.map +0 -1
  152. package/dist/github/platform-adapter.test.d.ts +0 -2
  153. package/dist/github/platform-adapter.test.d.ts.map +0 -1
  154. package/dist/github/platform-adapter.test.js +0 -40
  155. package/dist/github/platform-adapter.test.js.map +0 -1
  156. package/dist/gitlab/diff-parser.test.d.ts +0 -2
  157. package/dist/gitlab/diff-parser.test.d.ts.map +0 -1
  158. package/dist/gitlab/diff-parser.test.js +0 -315
  159. package/dist/gitlab/diff-parser.test.js.map +0 -1
  160. package/dist/gitlab/platform-adapter.test.d.ts +0 -2
  161. package/dist/gitlab/platform-adapter.test.d.ts.map +0 -1
  162. package/dist/gitlab/platform-adapter.test.js +0 -21
  163. package/dist/gitlab/platform-adapter.test.js.map +0 -1
  164. package/dist/index.test.d.ts +0 -2
  165. package/dist/index.test.d.ts.map +0 -1
  166. package/dist/index.test.js +0 -7
  167. package/dist/index.test.js.map +0 -1
  168. package/dist/lib/code-quality-report.test.d.ts +0 -2
  169. package/dist/lib/code-quality-report.test.d.ts.map +0 -1
  170. package/dist/lib/code-quality-report.test.js +0 -327
  171. package/dist/lib/code-quality-report.test.js.map +0 -1
  172. package/dist/lib/comment-formatter.test.d.ts +0 -2
  173. package/dist/lib/comment-formatter.test.d.ts.map +0 -1
  174. package/dist/lib/comment-formatter.test.js +0 -727
  175. package/dist/lib/comment-formatter.test.js.map +0 -1
  176. package/dist/lib/comment-manager.test.d.ts +0 -2
  177. package/dist/lib/comment-manager.test.d.ts.map +0 -1
  178. package/dist/lib/comment-manager.test.js +0 -680
  179. package/dist/lib/comment-manager.test.js.map +0 -1
  180. package/dist/lib/comment-poster.test.d.ts +0 -5
  181. package/dist/lib/comment-poster.test.d.ts.map +0 -1
  182. package/dist/lib/comment-poster.test.js +0 -255
  183. package/dist/lib/comment-poster.test.js.map +0 -1
  184. package/dist/lib/config-model-overrides.test.d.ts +0 -2
  185. package/dist/lib/config-model-overrides.test.d.ts.map +0 -1
  186. package/dist/lib/config-model-overrides.test.js +0 -218
  187. package/dist/lib/config-model-overrides.test.js.map +0 -1
  188. package/dist/lib/config.test.d.ts +0 -2
  189. package/dist/lib/config.test.d.ts.map +0 -1
  190. package/dist/lib/config.test.js +0 -353
  191. package/dist/lib/config.test.js.map +0 -1
  192. package/dist/lib/context-compression.test.d.ts +0 -2
  193. package/dist/lib/context-compression.test.d.ts.map +0 -1
  194. package/dist/lib/context-compression.test.js +0 -337
  195. package/dist/lib/context-compression.test.js.map +0 -1
  196. package/dist/lib/context-loader.test.d.ts +0 -2
  197. package/dist/lib/context-loader.test.d.ts.map +0 -1
  198. package/dist/lib/context-loader.test.js +0 -212
  199. package/dist/lib/context-loader.test.js.map +0 -1
  200. package/dist/lib/cursor-fix-link.test.d.ts +0 -2
  201. package/dist/lib/cursor-fix-link.test.d.ts.map +0 -1
  202. package/dist/lib/cursor-fix-link.test.js +0 -70
  203. package/dist/lib/cursor-fix-link.test.js.map +0 -1
  204. package/dist/lib/describe-core.test.d.ts +0 -2
  205. package/dist/lib/describe-core.test.d.ts.map +0 -1
  206. package/dist/lib/describe-core.test.js +0 -208
  207. package/dist/lib/describe-core.test.js.map +0 -1
  208. package/dist/lib/describe-output-path.test.d.ts +0 -2
  209. package/dist/lib/describe-output-path.test.d.ts.map +0 -1
  210. package/dist/lib/describe-output-path.test.js +0 -51
  211. package/dist/lib/describe-output-path.test.js.map +0 -1
  212. package/dist/lib/describe-parser.test.d.ts +0 -2
  213. package/dist/lib/describe-parser.test.d.ts.map +0 -1
  214. package/dist/lib/describe-parser.test.js +0 -282
  215. package/dist/lib/describe-parser.test.js.map +0 -1
  216. package/dist/lib/description-executor.test.d.ts +0 -2
  217. package/dist/lib/description-executor.test.d.ts.map +0 -1
  218. package/dist/lib/description-executor.test.js +0 -135
  219. package/dist/lib/description-executor.test.js.map +0 -1
  220. package/dist/lib/description-formatter.test.d.ts +0 -2
  221. package/dist/lib/description-formatter.test.d.ts.map +0 -1
  222. package/dist/lib/description-formatter.test.js +0 -57
  223. package/dist/lib/description-formatter.test.js.map +0 -1
  224. package/dist/lib/diff-lines.test.d.ts +0 -2
  225. package/dist/lib/diff-lines.test.d.ts.map +0 -1
  226. package/dist/lib/diff-lines.test.js +0 -13
  227. package/dist/lib/diff-lines.test.js.map +0 -1
  228. package/dist/lib/diff-parser.test.d.ts +0 -2
  229. package/dist/lib/diff-parser.test.d.ts.map +0 -1
  230. package/dist/lib/diff-parser.test.js +0 -335
  231. package/dist/lib/diff-parser.test.js.map +0 -1
  232. package/dist/lib/error-comment-poster.test.d.ts +0 -2
  233. package/dist/lib/error-comment-poster.test.d.ts.map +0 -1
  234. package/dist/lib/error-comment-poster.test.js +0 -128
  235. package/dist/lib/error-comment-poster.test.js.map +0 -1
  236. package/dist/lib/exit.test.d.ts +0 -2
  237. package/dist/lib/exit.test.d.ts.map +0 -1
  238. package/dist/lib/exit.test.js +0 -120
  239. package/dist/lib/exit.test.js.map +0 -1
  240. package/dist/lib/issue-parser.test.d.ts +0 -2
  241. package/dist/lib/issue-parser.test.d.ts.map +0 -1
  242. package/dist/lib/issue-parser.test.js +0 -281
  243. package/dist/lib/issue-parser.test.js.map +0 -1
  244. package/dist/lib/json-output-schema.test.d.ts +0 -2
  245. package/dist/lib/json-output-schema.test.d.ts.map +0 -1
  246. package/dist/lib/json-output-schema.test.js +0 -92
  247. package/dist/lib/json-output-schema.test.js.map +0 -1
  248. package/dist/lib/json-output.test.d.ts +0 -2
  249. package/dist/lib/json-output.test.d.ts.map +0 -1
  250. package/dist/lib/json-output.test.js +0 -141
  251. package/dist/lib/json-output.test.js.map +0 -1
  252. package/dist/lib/logger.test.d.ts +0 -2
  253. package/dist/lib/logger.test.d.ts.map +0 -1
  254. package/dist/lib/logger.test.js +0 -324
  255. package/dist/lib/logger.test.js.map +0 -1
  256. package/dist/lib/position-validator.test.d.ts +0 -2
  257. package/dist/lib/position-validator.test.d.ts.map +0 -1
  258. package/dist/lib/position-validator.test.js +0 -128
  259. package/dist/lib/position-validator.test.js.map +0 -1
  260. package/dist/lib/prompt-budget.test.d.ts +0 -2
  261. package/dist/lib/prompt-budget.test.d.ts.map +0 -1
  262. package/dist/lib/prompt-budget.test.js +0 -55
  263. package/dist/lib/prompt-budget.test.js.map +0 -1
  264. package/dist/lib/repository-validator.test.d.ts +0 -5
  265. package/dist/lib/repository-validator.test.d.ts.map +0 -1
  266. package/dist/lib/repository-validator.test.js +0 -341
  267. package/dist/lib/repository-validator.test.js.map +0 -1
  268. package/dist/lib/review-core.test.d.ts +0 -2
  269. package/dist/lib/review-core.test.d.ts.map +0 -1
  270. package/dist/lib/review-core.test.js +0 -614
  271. package/dist/lib/review-core.test.js.map +0 -1
  272. package/dist/lib/review-orchestrator.test.d.ts +0 -2
  273. package/dist/lib/review-orchestrator.test.d.ts.map +0 -1
  274. package/dist/lib/review-orchestrator.test.js +0 -552
  275. package/dist/lib/review-orchestrator.test.js.map +0 -1
  276. package/dist/lib/review-output-path.test.d.ts +0 -2
  277. package/dist/lib/review-output-path.test.d.ts.map +0 -1
  278. package/dist/lib/review-output-path.test.js +0 -83
  279. package/dist/lib/review-output-path.test.js.map +0 -1
  280. package/dist/lib/review-parser.test.d.ts +0 -2
  281. package/dist/lib/review-parser.test.d.ts.map +0 -1
  282. package/dist/lib/review-parser.test.js +0 -130
  283. package/dist/lib/review-parser.test.js.map +0 -1
  284. package/dist/lib/review-usage.test.d.ts +0 -2
  285. package/dist/lib/review-usage.test.d.ts.map +0 -1
  286. package/dist/lib/review-usage.test.js +0 -83
  287. package/dist/lib/review-usage.test.js.map +0 -1
  288. package/dist/lib/unified-review-executor.d.ts +0 -58
  289. package/dist/lib/unified-review-executor.d.ts.map +0 -1
  290. package/dist/lib/unified-review-executor.js +0 -201
  291. package/dist/lib/unified-review-executor.js.map +0 -1
  292. package/dist/lib/unified-review-executor.test.d.ts +0 -5
  293. package/dist/lib/unified-review-executor.test.d.ts.map +0 -1
  294. package/dist/lib/unified-review-executor.test.js +0 -472
  295. package/dist/lib/unified-review-executor.test.js.map +0 -1
  296. package/dist/lib/write-json-output.test.d.ts +0 -2
  297. package/dist/lib/write-json-output.test.d.ts.map +0 -1
  298. package/dist/lib/write-json-output.test.js +0 -259
  299. package/dist/lib/write-json-output.test.js.map +0 -1
  300. package/dist/pi/sdk.test.d.ts +0 -2
  301. package/dist/pi/sdk.test.d.ts.map +0 -1
  302. package/dist/pi/sdk.test.js +0 -488
  303. package/dist/pi/sdk.test.js.map +0 -1
  304. package/dist/runtime/agent-loader.test.d.ts +0 -2
  305. package/dist/runtime/agent-loader.test.d.ts.map +0 -1
  306. package/dist/runtime/agent-loader.test.js +0 -277
  307. package/dist/runtime/agent-loader.test.js.map +0 -1
  308. package/dist/runtime/client.test.d.ts +0 -2
  309. package/dist/runtime/client.test.d.ts.map +0 -1
  310. package/dist/runtime/client.test.js +0 -772
  311. package/dist/runtime/client.test.js.map +0 -1
  312. package/dist/runtime/path-config.test.d.ts +0 -2
  313. package/dist/runtime/path-config.test.d.ts.map +0 -1
  314. package/dist/runtime/path-config.test.js +0 -112
  315. package/dist/runtime/path-config.test.js.map +0 -1
@@ -1,1410 +0,0 @@
1
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
2
- import { tmpdir } from 'os';
3
- import { join } from 'path';
4
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { loadConfig } from '../lib/config.js';
6
- import { exitProcess } from '../lib/exit.js';
7
- import { runWorkflow, listWorkflows } from './workflow.js';
8
- const mocks = vi.hoisted(() => {
9
- const githubAdapter = {
10
- getPullRequest: vi.fn(),
11
- getChangedFiles: vi.fn(),
12
- getComments: vi.fn(),
13
- getInlineComments: vi.fn(),
14
- createComment: vi.fn(),
15
- updateComment: vi.fn(),
16
- deleteComment: vi.fn(),
17
- createBulkInlineComments: vi.fn(),
18
- addLabels: vi.fn(),
19
- };
20
- const gitlabAdapter = {
21
- getPullRequest: vi.fn(),
22
- getChangedFiles: vi.fn(),
23
- getComments: vi.fn(),
24
- getInlineComments: vi.fn(),
25
- createComment: vi.fn(),
26
- updateComment: vi.fn(),
27
- deleteComment: vi.fn(),
28
- createBulkInlineComments: vi.fn(),
29
- addLabels: vi.fn(),
30
- };
31
- return {
32
- git: {
33
- checkIsRepo: vi.fn(async () => true),
34
- diff: vi.fn(async () => 'diff --git a/src/app.ts b/src/app.ts'),
35
- raw: vi.fn(async (_args) => ''),
36
- add: vi.fn(async () => ''),
37
- commit: vi.fn(async () => ({
38
- commit: 'abc1234',
39
- summary: {
40
- changes: 1,
41
- insertions: 2,
42
- deletions: 0,
43
- },
44
- })),
45
- },
46
- simpleGit: vi.fn(),
47
- createGitHubClient: vi.fn(() => ({ platform: 'github' })),
48
- GitHubPlatformAdapter: vi.fn(() => githubAdapter),
49
- githubAdapter,
50
- createGitLabClient: vi.fn(() => ({ platform: 'gitlab' })),
51
- GitLabPlatformAdapter: vi.fn(() => gitlabAdapter),
52
- gitlabAdapter,
53
- parseDiff: vi.fn(() => [{ filename: 'src/app.ts', patch: '@@ +1 @@\n+change' }]),
54
- getChangedFiles: vi.fn(() => ['src/app.ts']),
55
- getFilesWithDiffs: vi.fn(() => [{ filename: 'src/app.ts', patch: '@@ +1 @@\n+change' }]),
56
- runtimeClient: {
57
- shutdown: vi.fn(async () => undefined),
58
- },
59
- connectToRuntime: vi.fn(),
60
- runDescribeIfEnabled: vi.fn(),
61
- enforceRepoBranchMatch: vi.fn(async () => undefined),
62
- executeReview: vi.fn(async (_config, source) => ({
63
- issues: [],
64
- summary: {
65
- filesReviewed: source.files.length,
66
- issuesFound: 0,
67
- bySeverity: {},
68
- byCategory: {},
69
- },
70
- filesReviewed: source.files.length,
71
- })),
72
- runAgent: vi.fn(async (_config, agent, options) => ({
73
- timestamp: '2026-06-16T00:00:00.000Z',
74
- agent,
75
- response: `${agent}: ${options.prompt ?? 'configured prompt'}`,
76
- usage: {
77
- agent,
78
- success: true,
79
- inputTokens: 1,
80
- outputTokens: 1,
81
- cacheReadTokens: 0,
82
- cacheWriteTokens: 0,
83
- totalTokens: 2,
84
- cost: 0,
85
- messages: 1,
86
- },
87
- })),
88
- };
89
- });
90
- mocks.simpleGit.mockReturnValue(mocks.git);
91
- vi.mock('simple-git', () => ({
92
- default: mocks.simpleGit,
93
- }));
94
- vi.mock('../github/client.js', () => ({
95
- createGitHubClient: mocks.createGitHubClient,
96
- }));
97
- vi.mock('../github/platform-adapter.js', () => ({
98
- GitHubPlatformAdapter: mocks.GitHubPlatformAdapter,
99
- }));
100
- vi.mock('../gitlab/client.js', () => ({
101
- createGitLabClient: mocks.createGitLabClient,
102
- }));
103
- vi.mock('../gitlab/platform-adapter.js', () => ({
104
- GitLabPlatformAdapter: mocks.GitLabPlatformAdapter,
105
- }));
106
- vi.mock('./run-agent.js', () => ({
107
- runAgent: mocks.runAgent,
108
- }));
109
- vi.mock('../lib/diff-parser.js', () => ({
110
- parseDiff: mocks.parseDiff,
111
- getChangedFiles: mocks.getChangedFiles,
112
- getFilesWithDiffs: mocks.getFilesWithDiffs,
113
- }));
114
- vi.mock('../lib/review-orchestrator.js', () => ({
115
- connectToRuntime: mocks.connectToRuntime,
116
- executeReview: mocks.executeReview,
117
- filterIgnoredFiles: (files, config) => files.filter((file) => !(config.review.ignorePatterns ?? []).includes(file)),
118
- }));
119
- vi.mock('../lib/description-executor.js', () => ({
120
- runDescribeIfEnabled: mocks.runDescribeIfEnabled,
121
- }));
122
- vi.mock('../lib/repository-validator.js', () => ({
123
- enforceRepoBranchMatch: mocks.enforceRepoBranchMatch,
124
- resolveBaseBranch: (_baseBranch, targetBranch) => ({
125
- resolvedBaseBranch: targetBranch ? `origin/${targetBranch}` : undefined,
126
- source: targetBranch ? 'pr:targetBranch' : undefined,
127
- }),
128
- getCanonicalDiffCommand: () => 'git diff origin/main origin/feature -- <file>',
129
- }));
130
- const baseConfig = {
131
- pi: {},
132
- agents: { default: { model: 'provider/default-model', skills: [] } },
133
- gitlab: { url: '', token: '' },
134
- github: { token: '' },
135
- review: {
136
- agents: ['review/security'],
137
- ignorePatterns: [],
138
- },
139
- };
140
- describe('workflow runner', () => {
141
- const tempDirs = [];
142
- function createTempDir(prefix) {
143
- const dir = mkdtempSync(join(tmpdir(), prefix));
144
- tempDirs.push(dir);
145
- return dir;
146
- }
147
- function createMockAgentResult(agent, response) {
148
- return {
149
- timestamp: '2026-06-16T00:00:00.000Z',
150
- agent,
151
- response,
152
- usage: {
153
- agent,
154
- success: true,
155
- inputTokens: 1,
156
- outputTokens: 1,
157
- cacheReadTokens: 0,
158
- cacheWriteTokens: 0,
159
- totalTokens: 2,
160
- cost: 0,
161
- messages: 1,
162
- },
163
- };
164
- }
165
- function timeoutAfter(ms) {
166
- return new Promise((_, reject) => {
167
- setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms);
168
- });
169
- }
170
- beforeEach(() => {
171
- vi.clearAllMocks();
172
- mocks.simpleGit.mockReturnValue(mocks.git);
173
- mocks.git.checkIsRepo.mockResolvedValue(true);
174
- mocks.git.diff.mockResolvedValue('diff --git a/src/app.ts b/src/app.ts');
175
- mocks.git.raw.mockResolvedValue('');
176
- mocks.git.add.mockResolvedValue('');
177
- mocks.git.commit.mockResolvedValue({
178
- commit: 'abc1234',
179
- summary: {
180
- changes: 1,
181
- insertions: 2,
182
- deletions: 0,
183
- },
184
- });
185
- mocks.parseDiff.mockReturnValue([{ filename: 'src/app.ts', patch: '@@ +1 @@\n+change' }]);
186
- mocks.getChangedFiles.mockReturnValue(['src/app.ts']);
187
- mocks.getFilesWithDiffs.mockReturnValue([
188
- { filename: 'src/app.ts', patch: '@@ +1 @@\n+change' },
189
- ]);
190
- mocks.runtimeClient.shutdown.mockResolvedValue(undefined);
191
- mocks.connectToRuntime.mockResolvedValue(mocks.runtimeClient);
192
- mocks.runDescribeIfEnabled.mockResolvedValue({
193
- type: 'feature',
194
- title: 'Generated description',
195
- summary: ['Describe the change'],
196
- });
197
- mocks.enforceRepoBranchMatch.mockResolvedValue(undefined);
198
- mocks.createGitHubClient.mockReturnValue({ platform: 'github' });
199
- mocks.GitHubPlatformAdapter.mockReturnValue(mocks.githubAdapter);
200
- mocks.githubAdapter.getPullRequest.mockResolvedValue({
201
- number: 7,
202
- title: 'GitHub PR',
203
- author: 'octocat',
204
- sourceBranch: 'feature',
205
- targetBranch: 'main',
206
- headSha: 'abc123',
207
- });
208
- mocks.githubAdapter.getChangedFiles.mockResolvedValue([
209
- {
210
- filename: 'src/github.ts',
211
- status: 'modified',
212
- additions: 2,
213
- deletions: 1,
214
- patch: '@@ +1 @@\n+github',
215
- },
216
- ]);
217
- mocks.githubAdapter.getComments.mockResolvedValue([]);
218
- mocks.githubAdapter.getInlineComments.mockResolvedValue([]);
219
- mocks.githubAdapter.createComment.mockResolvedValue(undefined);
220
- mocks.githubAdapter.updateComment.mockResolvedValue(undefined);
221
- mocks.githubAdapter.deleteComment.mockResolvedValue(undefined);
222
- mocks.githubAdapter.createBulkInlineComments.mockResolvedValue(undefined);
223
- mocks.githubAdapter.addLabels.mockResolvedValue(undefined);
224
- mocks.createGitLabClient.mockReturnValue({ platform: 'gitlab' });
225
- mocks.GitLabPlatformAdapter.mockReturnValue(mocks.gitlabAdapter);
226
- mocks.gitlabAdapter.getPullRequest.mockResolvedValue({
227
- number: 8,
228
- title: 'GitLab MR',
229
- author: 'gitlab-user',
230
- sourceBranch: 'feature',
231
- targetBranch: 'main',
232
- headSha: 'def456',
233
- });
234
- mocks.gitlabAdapter.getChangedFiles.mockResolvedValue([
235
- {
236
- filename: 'src/gitlab.ts',
237
- status: 'modified',
238
- additions: 3,
239
- deletions: 1,
240
- patch: '@@ +1 @@\n+gitlab',
241
- },
242
- ]);
243
- mocks.gitlabAdapter.getComments.mockResolvedValue([]);
244
- mocks.gitlabAdapter.getInlineComments.mockResolvedValue([]);
245
- mocks.gitlabAdapter.createComment.mockResolvedValue(undefined);
246
- mocks.gitlabAdapter.updateComment.mockResolvedValue(undefined);
247
- mocks.gitlabAdapter.deleteComment.mockResolvedValue(undefined);
248
- mocks.gitlabAdapter.createBulkInlineComments.mockResolvedValue(undefined);
249
- mocks.gitlabAdapter.addLabels.mockResolvedValue(undefined);
250
- mocks.executeReview.mockImplementation(async (_config, source) => ({
251
- issues: [],
252
- summary: {
253
- filesReviewed: source.files.length,
254
- issuesFound: 0,
255
- bySeverity: {},
256
- byCategory: {},
257
- },
258
- filesReviewed: source.files.length,
259
- }));
260
- vi.spyOn(console, 'log').mockImplementation(() => { });
261
- });
262
- afterEach(() => {
263
- vi.restoreAllMocks();
264
- for (const dir of tempDirs.splice(0, tempDirs.length)) {
265
- rmSync(dir, { recursive: true, force: true });
266
- }
267
- });
268
- it('runs agent and write nodes in dependency order', async () => {
269
- const projectRoot = createTempDir('drs-workflow-');
270
- const config = {
271
- ...baseConfig,
272
- workflows: {
273
- release: {
274
- inputs: {
275
- diff: 'Diff text',
276
- },
277
- nodes: {
278
- summarize: {
279
- agent: 'task/summarizer',
280
- input: 'Summarize {{inputs.diff}}',
281
- output: 'summary',
282
- },
283
- writeSummary: {
284
- action: 'write',
285
- needs: ['summarize'],
286
- input: 'Summary:\n{{artifacts.summary}}',
287
- writes: 'out/summary.md',
288
- output: 'written',
289
- },
290
- },
291
- },
292
- },
293
- };
294
- const result = await runWorkflow(config, 'release', {
295
- workingDir: projectRoot,
296
- });
297
- expect(mocks.runAgent).toHaveBeenCalledWith(config, 'task/summarizer', expect.objectContaining({
298
- prompt: 'Summarize Diff text',
299
- quiet: true,
300
- allowImplicitStdin: false,
301
- ignoreConfiguredOutput: true,
302
- }));
303
- expect(readFileSync(join(projectRoot, 'out/summary.md'), 'utf-8')).toBe('Summary:\ntask/summarizer: Summarize Diff text');
304
- expect(result.output).toBe('Summary:\ntask/summarizer: Summarize Diff text');
305
- });
306
- it('rejects writes paths that render to empty strings', async () => {
307
- const config = {
308
- ...baseConfig,
309
- workflows: {
310
- emptyWrite: {
311
- inputs: {
312
- outputPath: '',
313
- },
314
- nodes: {
315
- writeSummary: {
316
- action: 'write',
317
- input: 'content',
318
- writes: '{{inputs.outputPath}}',
319
- },
320
- },
321
- },
322
- },
323
- };
324
- await expect(runWorkflow(config, 'emptyWrite')).rejects.toThrow('Workflow node "writeSummary" writes resolved to an empty path.');
325
- });
326
- it('uses strict boolean checks for node JSON writes', async () => {
327
- const projectRoot = createTempDir('drs-workflow-json-');
328
- const config = {
329
- ...baseConfig,
330
- workflows: {
331
- jsonFlag: {
332
- nodes: {
333
- summarize: {
334
- agent: 'task/summarizer',
335
- input: 'Summarize',
336
- writes: 'summary.txt',
337
- json: 'false',
338
- },
339
- },
340
- },
341
- },
342
- };
343
- await runWorkflow(config, 'jsonFlag', { workingDir: projectRoot });
344
- expect(readFileSync(join(projectRoot, 'summary.txt'), 'utf-8')).toBe('task/summarizer: Summarize');
345
- });
346
- it('lets CLI-style inputs override configured inputs', async () => {
347
- const projectRoot = createTempDir('drs-workflow-inputs-');
348
- writeFileSync(join(projectRoot, 'diff.md'), 'File diff');
349
- const config = {
350
- ...baseConfig,
351
- workflows: {
352
- describe: {
353
- inputs: {
354
- diff: 'Configured diff',
355
- title: 'Configured title',
356
- },
357
- nodes: {
358
- summarize: {
359
- agent: 'task/summarizer',
360
- input: '{{inputs.title}}\n{{inputs.diff}}',
361
- },
362
- },
363
- },
364
- },
365
- };
366
- await runWorkflow(config, 'describe', {
367
- inputs: { title: 'CLI title' },
368
- inputFiles: { diff: 'diff.md' },
369
- workingDir: projectRoot,
370
- });
371
- expect(mocks.runAgent).toHaveBeenCalledWith(config, 'task/summarizer', expect.objectContaining({
372
- prompt: 'CLI title\nFile diff',
373
- }));
374
- });
375
- it('runs agentsFrom review.agents as a multi-agent node', async () => {
376
- const config = {
377
- ...baseConfig,
378
- review: {
379
- agents: ['review/security', { name: 'review/quality' }],
380
- ignorePatterns: [],
381
- },
382
- workflows: {
383
- review: {
384
- inputs: {
385
- diff: 'Diff text',
386
- },
387
- nodes: {
388
- reviewers: {
389
- agentsFrom: 'review.agents',
390
- input: 'Review {{inputs.diff}}',
391
- output: 'reviewResult',
392
- },
393
- },
394
- },
395
- },
396
- };
397
- const result = await runWorkflow(config, 'review', {
398
- workingDir: process.cwd(),
399
- });
400
- expect(mocks.runAgent).toHaveBeenNthCalledWith(1, config, 'review/security', expect.objectContaining({ prompt: 'Review Diff text' }));
401
- expect(mocks.runAgent).toHaveBeenNthCalledWith(2, config, 'review/quality', expect.objectContaining({ prompt: 'Review Diff text' }));
402
- expect(result.artifacts.reviewResult).toContain('## review/security');
403
- expect(result.artifacts.reviewResult).toContain('## review/quality');
404
- });
405
- it('runs agentsFrom agents concurrently', async () => {
406
- let resolveSecurity = () => { };
407
- let resolveQuality = () => { };
408
- let resolveBothStarted = () => { };
409
- const starts = [];
410
- const securityDone = new Promise((resolve) => {
411
- resolveSecurity = resolve;
412
- });
413
- const qualityDone = new Promise((resolve) => {
414
- resolveQuality = resolve;
415
- });
416
- const bothStarted = new Promise((resolve) => {
417
- resolveBothStarted = resolve;
418
- });
419
- mocks.runAgent.mockImplementation(async (_config, agent) => {
420
- starts.push(agent);
421
- if (starts.length === 2) {
422
- resolveBothStarted();
423
- }
424
- await (agent === 'review/security' ? securityDone : qualityDone);
425
- return createMockAgentResult(agent, `${agent} done`);
426
- });
427
- const config = {
428
- ...baseConfig,
429
- review: {
430
- agents: ['review/security', 'review/quality'],
431
- ignorePatterns: [],
432
- },
433
- workflows: {
434
- review: {
435
- inputs: {
436
- diff: 'Diff text',
437
- },
438
- nodes: {
439
- reviewers: {
440
- agentsFrom: 'review.agents',
441
- input: 'Review {{inputs.diff}}',
442
- output: 'reviewResult',
443
- },
444
- },
445
- },
446
- },
447
- };
448
- const runPromise = runWorkflow(config, 'review', { workingDir: process.cwd() });
449
- await Promise.race([bothStarted, timeoutAfter(250)]);
450
- resolveSecurity();
451
- resolveQuality();
452
- const result = await runPromise;
453
- expect(starts).toEqual(['review/security', 'review/quality']);
454
- expect(result.artifacts.reviewResult).toContain('review/security done');
455
- expect(result.artifacts.reviewResult).toContain('review/quality done');
456
- });
457
- it('runs independent workflow nodes concurrently', async () => {
458
- let resolveOne = () => { };
459
- let resolveTwo = () => { };
460
- let resolveBothStarted = () => { };
461
- const starts = [];
462
- const oneDone = new Promise((resolve) => {
463
- resolveOne = resolve;
464
- });
465
- const twoDone = new Promise((resolve) => {
466
- resolveTwo = resolve;
467
- });
468
- const bothStarted = new Promise((resolve) => {
469
- resolveBothStarted = resolve;
470
- });
471
- mocks.runAgent.mockImplementation(async (_config, agent) => {
472
- starts.push(agent);
473
- if (starts.length === 2) {
474
- resolveBothStarted();
475
- }
476
- await (agent === 'task/one' ? oneDone : twoDone);
477
- return createMockAgentResult(agent, `${agent} done`);
478
- });
479
- const config = {
480
- ...baseConfig,
481
- workflows: {
482
- parallel: {
483
- nodes: {
484
- one: { agent: 'task/one', input: 'one', output: 'one' },
485
- two: { agent: 'task/two', input: 'two', output: 'two' },
486
- join: {
487
- action: 'write',
488
- needs: ['one', 'two'],
489
- input: '{{artifacts.one}}\n{{artifacts.two}}',
490
- writes: 'joined.txt',
491
- },
492
- },
493
- },
494
- },
495
- };
496
- const projectRoot = createTempDir('drs-workflow-parallel-');
497
- const runPromise = runWorkflow(config, 'parallel', { workingDir: projectRoot });
498
- await Promise.race([bothStarted, timeoutAfter(250)]);
499
- resolveOne();
500
- resolveTwo();
501
- await runPromise;
502
- expect(starts).toEqual(['task/one', 'task/two']);
503
- expect(readFileSync(join(projectRoot, 'joined.txt'), 'utf-8')).toBe('task/one done\ntask/two done');
504
- });
505
- it('loads local git diff as an action artifact', async () => {
506
- const projectRoot = createTempDir('drs-workflow-git-diff-');
507
- const config = {
508
- ...baseConfig,
509
- workflows: {
510
- localReview: {
511
- nodes: {
512
- diff: {
513
- action: 'git-diff',
514
- with: { staged: true },
515
- output: 'localDiff',
516
- },
517
- summarize: {
518
- agent: 'task/summarizer',
519
- needs: ['diff'],
520
- input: 'Summarize {{artifacts.localDiff}}',
521
- },
522
- },
523
- },
524
- },
525
- };
526
- await runWorkflow(config, 'localReview', {
527
- workingDir: projectRoot,
528
- });
529
- expect(mocks.simpleGit).toHaveBeenCalledWith({ baseDir: projectRoot });
530
- expect(mocks.git.diff).toHaveBeenCalledWith(['--cached']);
531
- expect(mocks.runAgent).toHaveBeenCalledWith(config, 'task/summarizer', expect.objectContaining({
532
- prompt: 'Summarize diff --git a/src/app.ts b/src/app.ts',
533
- }));
534
- });
535
- it('stages paths with a git-add action', async () => {
536
- const projectRoot = createTempDir('drs-workflow-git-add-');
537
- const config = {
538
- ...baseConfig,
539
- workflows: {
540
- stageChangelog: {
541
- nodes: {
542
- stage: {
543
- action: 'git-add',
544
- with: { paths: 'CHANGELOG.md, README.md' },
545
- output: 'stagedPaths',
546
- },
547
- },
548
- },
549
- },
550
- };
551
- const result = await runWorkflow(config, 'stageChangelog', {
552
- workingDir: projectRoot,
553
- });
554
- expect(mocks.git.add).toHaveBeenCalledWith(['CHANGELOG.md', 'README.md']);
555
- expect(result.artifacts.stagedPaths).toEqual(['CHANGELOG.md', 'README.md']);
556
- });
557
- it('commits only configured paths with a git-commit action', async () => {
558
- const projectRoot = createTempDir('drs-workflow-git-commit-');
559
- const config = {
560
- ...baseConfig,
561
- workflows: {
562
- commitChangelog: {
563
- nodes: {
564
- commit: {
565
- action: 'git-commit',
566
- with: {
567
- paths: 'CHANGELOG.md',
568
- message: 'docs: update changelog',
569
- },
570
- output: 'commitResult',
571
- },
572
- },
573
- },
574
- },
575
- };
576
- const result = await runWorkflow(config, 'commitChangelog', {
577
- workingDir: projectRoot,
578
- });
579
- expect(mocks.git.add).toHaveBeenCalledWith(['CHANGELOG.md']);
580
- expect(mocks.git.commit).toHaveBeenCalledWith('docs: update changelog', ['CHANGELOG.md']);
581
- expect(result.artifacts.commitResult).toMatchObject({
582
- commit: 'abc1234',
583
- message: 'docs: update changelog',
584
- paths: ['CHANGELOG.md'],
585
- });
586
- });
587
- it('rejects git action paths outside the working directory', async () => {
588
- const projectRoot = createTempDir('drs-workflow-git-path-');
589
- const config = {
590
- ...baseConfig,
591
- workflows: {
592
- unsafeStage: {
593
- nodes: {
594
- stage: {
595
- action: 'git-add',
596
- with: { path: '../outside.md' },
597
- },
598
- },
599
- },
600
- },
601
- };
602
- await expect(runWorkflow(config, 'unsafeStage', {
603
- workingDir: projectRoot,
604
- })).rejects.toThrow('Refusing to access outside working directory');
605
- expect(mocks.git.add).not.toHaveBeenCalled();
606
- });
607
- it('loads a local change source and reviews it', async () => {
608
- const projectRoot = createTempDir('drs-workflow-review-');
609
- const config = {
610
- ...baseConfig,
611
- workflows: {
612
- localReview: {
613
- nodes: {
614
- change: {
615
- action: 'change-source',
616
- with: { type: 'local', staged: true },
617
- output: 'change',
618
- },
619
- review: {
620
- action: 'review',
621
- needs: ['change'],
622
- with: { source: 'change' },
623
- output: 'reviewResult',
624
- writes: '.drs/review-result.json',
625
- },
626
- },
627
- },
628
- },
629
- };
630
- const result = await runWorkflow(config, 'localReview', {
631
- workingDir: projectRoot,
632
- debug: true,
633
- thinkingLevel: 'high',
634
- });
635
- expect(mocks.git.diff).toHaveBeenCalledWith(['--cached']);
636
- expect(mocks.parseDiff).toHaveBeenCalledWith('diff --git a/src/app.ts b/src/app.ts');
637
- expect(mocks.executeReview).toHaveBeenCalledWith(config, expect.objectContaining({
638
- name: 'Local staged diff',
639
- files: ['src/app.ts'],
640
- filesWithDiffs: [{ filename: 'src/app.ts', patch: '@@ +1 @@\n+change' }],
641
- workingDir: projectRoot,
642
- staged: true,
643
- debug: true,
644
- thinkingLevel: 'high',
645
- }));
646
- expect(result.artifacts.reviewResult).toMatchObject({ filesReviewed: 1 });
647
- expect(JSON.parse(readFileSync(join(projectRoot, '.drs/review-result.json'), 'utf-8'))).toMatchObject({
648
- filesReviewed: 1,
649
- });
650
- });
651
- it('loads a git range change source from explicit refs', async () => {
652
- const projectRoot = createTempDir('drs-workflow-git-range-');
653
- const config = {
654
- ...baseConfig,
655
- workflows: {
656
- releaseChanges: {
657
- nodes: {
658
- change: {
659
- action: 'change-source',
660
- with: { type: 'git-range', from: 'v3.3.1', to: 'v4.0.0-rc.1' },
661
- output: 'change',
662
- },
663
- },
664
- },
665
- },
666
- };
667
- mocks.git.raw.mockResolvedValue('abc123\x1fAda Lovelace\x1f2026-06-16T00:00:00Z\x1fAdd workflow runtime\n');
668
- const result = await runWorkflow(config, 'releaseChanges', {
669
- workingDir: projectRoot,
670
- });
671
- expect(mocks.git.diff).toHaveBeenCalledWith(['v3.3.1..v4.0.0-rc.1']);
672
- expect(mocks.git.raw).toHaveBeenCalledWith([
673
- 'log',
674
- '--format=%H%x1f%an%x1f%aI%x1f%s',
675
- '--no-merges',
676
- 'v3.3.1..v4.0.0-rc.1',
677
- ]);
678
- expect(result.artifacts.change).toMatchObject({
679
- name: 'Git range v3.3.1..v4.0.0-rc.1',
680
- files: ['src/app.ts'],
681
- context: {
682
- sourceType: 'git-range',
683
- fromRef: 'v3.3.1',
684
- toRef: 'v4.0.0-rc.1',
685
- range: 'v3.3.1..v4.0.0-rc.1',
686
- commits: [
687
- {
688
- sha: 'abc123',
689
- author: 'Ada Lovelace',
690
- date: '2026-06-16T00:00:00Z',
691
- subject: 'Add workflow runtime',
692
- },
693
- ],
694
- },
695
- });
696
- });
697
- it('infers git range refs from a GitHub Actions tag event', async () => {
698
- const previousRefType = process.env.GITHUB_REF_TYPE;
699
- const previousRefName = process.env.GITHUB_REF_NAME;
700
- process.env.GITHUB_REF_TYPE = 'tag';
701
- process.env.GITHUB_REF_NAME = 'v4.0.0-rc.1';
702
- const projectRoot = createTempDir('drs-workflow-github-tag-range-');
703
- const config = {
704
- ...baseConfig,
705
- workflows: {
706
- releaseChanges: {
707
- nodes: {
708
- change: {
709
- action: 'change-source',
710
- with: { type: 'git-range' },
711
- output: 'change',
712
- },
713
- },
714
- },
715
- },
716
- };
717
- mocks.git.raw.mockImplementation(async (args) => {
718
- if (args?.[0] === 'tag') {
719
- return 'v4.0.0-rc.1\nv3.3.1\nv3.3.0\n';
720
- }
721
- return 'def456\x1fGrace Hopper\x1f2026-06-16T00:00:00Z\x1fPrepare 4.0\n';
722
- });
723
- try {
724
- const result = await runWorkflow(config, 'releaseChanges', {
725
- workingDir: projectRoot,
726
- });
727
- expect(mocks.git.raw).toHaveBeenCalledWith([
728
- 'tag',
729
- '--merged',
730
- 'v4.0.0-rc.1',
731
- '--sort=-v:refname',
732
- ]);
733
- expect(mocks.git.diff).toHaveBeenCalledWith(['v3.3.1..v4.0.0-rc.1']);
734
- expect(result.artifacts.change).toMatchObject({
735
- name: 'Git range v3.3.1..v4.0.0-rc.1',
736
- context: {
737
- fromRef: 'v3.3.1',
738
- toRef: 'v4.0.0-rc.1',
739
- },
740
- });
741
- }
742
- finally {
743
- if (previousRefType === undefined) {
744
- delete process.env.GITHUB_REF_TYPE;
745
- }
746
- else {
747
- process.env.GITHUB_REF_TYPE = previousRefType;
748
- }
749
- if (previousRefName === undefined) {
750
- delete process.env.GITHUB_REF_NAME;
751
- }
752
- else {
753
- process.env.GITHUB_REF_NAME = previousRefName;
754
- }
755
- }
756
- });
757
- it('suppresses review action logs when workflow JSON output is enabled', async () => {
758
- const logSpy = vi.mocked(console.log);
759
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
760
- mocks.executeReview.mockImplementation(async (_config, source) => {
761
- console.log('review progress');
762
- console.warn('review warning');
763
- return {
764
- issues: [],
765
- summary: {
766
- filesReviewed: source.files.length,
767
- issuesFound: 0,
768
- bySeverity: {},
769
- byCategory: {},
770
- },
771
- filesReviewed: source.files.length,
772
- };
773
- });
774
- const config = {
775
- ...baseConfig,
776
- workflows: {
777
- localReview: {
778
- nodes: {
779
- change: {
780
- action: 'change-source',
781
- output: 'change',
782
- },
783
- review: {
784
- action: 'review',
785
- needs: ['change'],
786
- with: { source: 'change' },
787
- output: 'reviewResult',
788
- },
789
- },
790
- },
791
- },
792
- };
793
- await runWorkflow(config, 'localReview', {
794
- jsonOutput: true,
795
- workingDir: process.cwd(),
796
- });
797
- expect(warnSpy).not.toHaveBeenCalled();
798
- expect(logSpy).toHaveBeenCalledTimes(1);
799
- expect(() => JSON.parse(String(logSpy.mock.calls[0][0]))).not.toThrow();
800
- });
801
- it('keeps review action log suppression isolated for concurrent JSON nodes', async () => {
802
- const logSpy = vi.mocked(console.log);
803
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
804
- let resolveFirstStarted = () => { };
805
- let resolveFirstCanReturn = () => { };
806
- const firstStarted = new Promise((resolve) => {
807
- resolveFirstStarted = resolve;
808
- });
809
- const firstCanReturn = new Promise((resolve) => {
810
- resolveFirstCanReturn = resolve;
811
- });
812
- let reviewCalls = 0;
813
- mocks.executeReview.mockImplementation(async (_config, source) => {
814
- const callNumber = ++reviewCalls;
815
- if (callNumber === 1) {
816
- console.log('first review progress');
817
- resolveFirstStarted();
818
- await firstCanReturn;
819
- }
820
- else {
821
- await new Promise((resolve) => {
822
- setTimeout(resolve, 0);
823
- });
824
- console.log('second review progress');
825
- console.warn('second review warning');
826
- }
827
- return {
828
- issues: [],
829
- summary: {
830
- filesReviewed: source.files.length,
831
- issuesFound: 0,
832
- bySeverity: {},
833
- byCategory: {},
834
- },
835
- filesReviewed: source.files.length,
836
- };
837
- });
838
- const config = {
839
- ...baseConfig,
840
- workflows: {
841
- concurrentReview: {
842
- nodes: {
843
- change: {
844
- action: 'change-source',
845
- output: 'change',
846
- },
847
- reviewOne: {
848
- action: 'review',
849
- needs: ['change'],
850
- with: { source: 'change' },
851
- output: 'reviewOneResult',
852
- },
853
- reviewTwo: {
854
- action: 'review',
855
- needs: ['change'],
856
- with: { source: 'change' },
857
- output: 'reviewTwoResult',
858
- },
859
- },
860
- },
861
- },
862
- };
863
- const runPromise = runWorkflow(config, 'concurrentReview', {
864
- jsonOutput: true,
865
- workingDir: process.cwd(),
866
- });
867
- await Promise.race([firstStarted, timeoutAfter(250)]);
868
- resolveFirstCanReturn();
869
- await runPromise;
870
- expect(reviewCalls).toBe(2);
871
- expect(warnSpy).not.toHaveBeenCalled();
872
- expect(logSpy).toHaveBeenCalledTimes(1);
873
- expect(() => JSON.parse(String(logSpy.mock.calls[0][0]))).not.toThrow();
874
- expect(console.log).toBe(logSpy);
875
- });
876
- it('converts review action process exits into workflow errors', async () => {
877
- mocks.executeReview.mockImplementation(async () => {
878
- exitProcess(1);
879
- });
880
- const config = {
881
- ...baseConfig,
882
- workflows: {
883
- localReview: {
884
- nodes: {
885
- change: {
886
- action: 'change-source',
887
- output: 'change',
888
- },
889
- review: {
890
- action: 'review',
891
- needs: ['change'],
892
- with: { source: 'change' },
893
- },
894
- },
895
- },
896
- },
897
- };
898
- await expect(runWorkflow(config, 'localReview')).rejects.toThrow('Workflow review node "review" failed: all review agents failed.');
899
- });
900
- it('loads a GitHub PR change source and reviews it', async () => {
901
- const config = {
902
- ...baseConfig,
903
- workflows: {
904
- githubReview: {
905
- inputs: {
906
- owner: '',
907
- repo: '',
908
- pr: '',
909
- },
910
- nodes: {
911
- change: {
912
- action: 'change-source',
913
- with: {
914
- type: 'github-pr',
915
- owner: '{{inputs.owner}}',
916
- repo: '{{inputs.repo}}',
917
- pr: '{{inputs.pr}}',
918
- },
919
- output: 'change',
920
- },
921
- review: {
922
- action: 'review',
923
- needs: ['change'],
924
- with: { source: 'change' },
925
- output: 'reviewResult',
926
- },
927
- },
928
- },
929
- },
930
- };
931
- await runWorkflow(config, 'githubReview', {
932
- inputs: { owner: 'octocat', repo: 'hello-world', pr: '7' },
933
- workingDir: process.cwd(),
934
- });
935
- expect(mocks.createGitHubClient).toHaveBeenCalled();
936
- expect(mocks.GitHubPlatformAdapter).toHaveBeenCalled();
937
- expect(mocks.githubAdapter.getPullRequest).toHaveBeenCalledWith('octocat/hello-world', 7);
938
- expect(mocks.githubAdapter.getChangedFiles).toHaveBeenCalledWith('octocat/hello-world', 7);
939
- expect(mocks.enforceRepoBranchMatch).toHaveBeenCalledWith(process.cwd(), 'octocat/hello-world', expect.objectContaining({ number: 7 }), {
940
- skipRepoCheck: undefined,
941
- skipBranchCheck: undefined,
942
- });
943
- expect(mocks.executeReview).toHaveBeenCalledWith(config, expect.objectContaining({
944
- name: 'GitHub PR octocat/hello-world#7',
945
- files: ['src/github.ts'],
946
- filesWithDiffs: [{ filename: 'src/github.ts', patch: '@@ +1 @@\n+github' }],
947
- context: expect.objectContaining({
948
- platform: 'github',
949
- projectId: 'octocat/hello-world',
950
- }),
951
- }));
952
- });
953
- it('shows GitHub PR review context with embedded diff content', async () => {
954
- const config = {
955
- ...baseConfig,
956
- workflows: {
957
- githubContext: {
958
- nodes: {
959
- change: {
960
- action: 'change-source',
961
- with: {
962
- type: 'github-pr',
963
- owner: 'octocat',
964
- repo: 'hello-world',
965
- pr: 7,
966
- },
967
- output: 'change',
968
- },
969
- context: {
970
- action: 'review-context',
971
- needs: ['change'],
972
- with: { source: 'change' },
973
- output: 'reviewContext',
974
- },
975
- },
976
- },
977
- },
978
- };
979
- const result = await runWorkflow(config, 'githubContext', { workingDir: process.cwd() });
980
- expect(result.output).toEqual(expect.stringContaining('## Diff Content'));
981
- expect(result.output).toEqual(expect.stringContaining('### src/github.ts'));
982
- expect(result.output).toEqual(expect.stringContaining('+github'));
983
- });
984
- it('filters review context to a requested file', async () => {
985
- mocks.gitlabAdapter.getChangedFiles.mockResolvedValue([
986
- {
987
- filename: 'src/one.ts',
988
- status: 'modified',
989
- additions: 1,
990
- deletions: 0,
991
- patch: '@@ +1 @@\n+one',
992
- },
993
- {
994
- filename: 'src/two.ts',
995
- status: 'modified',
996
- additions: 1,
997
- deletions: 0,
998
- patch: '@@ +1 @@\n+two',
999
- },
1000
- ]);
1001
- const config = {
1002
- ...baseConfig,
1003
- workflows: {
1004
- gitlabContext: {
1005
- nodes: {
1006
- change: {
1007
- action: 'change-source',
1008
- with: {
1009
- type: 'gitlab-mr',
1010
- project: 'group/repo',
1011
- mr: 8,
1012
- },
1013
- output: 'change',
1014
- },
1015
- context: {
1016
- action: 'review-context',
1017
- needs: ['change'],
1018
- with: { source: 'change', file: 'src/two.ts' },
1019
- output: 'reviewContext',
1020
- },
1021
- },
1022
- },
1023
- },
1024
- };
1025
- const result = await runWorkflow(config, 'gitlabContext', { workingDir: process.cwd() });
1026
- expect(result.output).toEqual(expect.stringContaining('### src/two.ts'));
1027
- expect(result.output).toEqual(expect.stringContaining('+two'));
1028
- expect(result.output).not.toEqual(expect.stringContaining('### src/one.ts'));
1029
- });
1030
- it('generates and posts a GitHub PR description from workflow artifacts', async () => {
1031
- const projectRoot = createTempDir('drs-workflow-describe-');
1032
- const config = {
1033
- ...baseConfig,
1034
- workflows: {
1035
- githubDescribe: {
1036
- nodes: {
1037
- change: {
1038
- action: 'change-source',
1039
- with: {
1040
- type: 'github-pr',
1041
- owner: 'octocat',
1042
- repo: 'hello-world',
1043
- pr: 7,
1044
- },
1045
- output: 'change',
1046
- },
1047
- describe: {
1048
- action: 'describe',
1049
- needs: ['change'],
1050
- with: { source: 'change', post: true },
1051
- output: 'description',
1052
- writes: '.drs/description.json',
1053
- },
1054
- },
1055
- },
1056
- },
1057
- };
1058
- const result = await runWorkflow(config, 'githubDescribe', {
1059
- workingDir: projectRoot,
1060
- debug: true,
1061
- thinkingLevel: 'high',
1062
- });
1063
- expect(mocks.connectToRuntime).toHaveBeenCalledWith(config, projectRoot, expect.objectContaining({
1064
- debug: true,
1065
- thinkingLevel: 'high',
1066
- }));
1067
- expect(mocks.runDescribeIfEnabled).toHaveBeenCalledWith(mocks.runtimeClient, config, mocks.githubAdapter, 'octocat/hello-world', expect.objectContaining({ number: 7 }), [{ filename: 'src/github.ts', patch: '@@ +1 @@\n+github' }], true, projectRoot, true);
1068
- expect(mocks.runtimeClient.shutdown).toHaveBeenCalled();
1069
- expect(result.artifacts.description).toMatchObject({ title: 'Generated description' });
1070
- expect(JSON.parse(readFileSync(join(projectRoot, '.drs/description.json'), 'utf-8'))).toEqual(result.artifacts.description);
1071
- });
1072
- it('updates an existing marked platform comment', async () => {
1073
- mocks.githubAdapter.getComments.mockResolvedValue([
1074
- { id: 9, body: '<!-- drs-comment-id: release-notes -->\nold body' },
1075
- ]);
1076
- const config = {
1077
- ...baseConfig,
1078
- workflows: {
1079
- postComment: {
1080
- nodes: {
1081
- comment: {
1082
- action: 'post-comment',
1083
- input: 'new body',
1084
- with: {
1085
- platform: 'github',
1086
- owner: 'octocat',
1087
- repo: 'hello-world',
1088
- pr: 7,
1089
- marker: 'release-notes',
1090
- },
1091
- output: 'commentResult',
1092
- },
1093
- },
1094
- },
1095
- },
1096
- };
1097
- const result = await runWorkflow(config, 'postComment', { workingDir: process.cwd() });
1098
- expect(mocks.githubAdapter.updateComment).toHaveBeenCalledWith('octocat/hello-world', 7, 9, '<!-- drs-comment-id: release-notes -->\nnew body');
1099
- expect(mocks.githubAdapter.createComment).not.toHaveBeenCalled();
1100
- expect(result.artifacts.commentResult).toMatchObject({
1101
- platform: 'github',
1102
- projectId: 'octocat/hello-world',
1103
- prNumber: 7,
1104
- marker: 'release-notes',
1105
- operation: 'updated',
1106
- });
1107
- });
1108
- it('posts GitHub review comments from workflow review artifacts', async () => {
1109
- mocks.githubAdapter.getChangedFiles.mockResolvedValue([
1110
- {
1111
- filename: 'src/github.ts',
1112
- status: 'modified',
1113
- additions: 1,
1114
- deletions: 0,
1115
- patch: '@@ -0,0 +1 @@\n+github',
1116
- },
1117
- ]);
1118
- mocks.executeReview.mockImplementation(async () => ({
1119
- issues: [
1120
- {
1121
- category: 'QUALITY',
1122
- severity: 'HIGH',
1123
- title: 'Validate input',
1124
- file: 'src/github.ts',
1125
- line: 1,
1126
- problem: 'Input is not validated.',
1127
- solution: 'Validate it before use.',
1128
- agent: 'review/quality',
1129
- },
1130
- ],
1131
- summary: {
1132
- filesReviewed: 1,
1133
- issuesFound: 1,
1134
- bySeverity: { CRITICAL: 0, HIGH: 1, MEDIUM: 0, LOW: 0 },
1135
- byCategory: { SECURITY: 0, QUALITY: 1, STYLE: 0, PERFORMANCE: 0, DOCUMENTATION: 0 },
1136
- },
1137
- filesReviewed: 1,
1138
- }));
1139
- const config = {
1140
- ...baseConfig,
1141
- workflows: {
1142
- githubReview: {
1143
- nodes: {
1144
- change: {
1145
- action: 'change-source',
1146
- with: {
1147
- type: 'github-pr',
1148
- owner: 'octocat',
1149
- repo: 'hello-world',
1150
- pr: 7,
1151
- },
1152
- output: 'change',
1153
- },
1154
- review: {
1155
- action: 'review',
1156
- needs: ['change'],
1157
- with: { source: 'change' },
1158
- output: 'review',
1159
- },
1160
- post: {
1161
- action: 'post-review-comments',
1162
- needs: ['review'],
1163
- with: {
1164
- source: 'change',
1165
- review: 'review',
1166
- },
1167
- output: 'postResult',
1168
- },
1169
- },
1170
- },
1171
- },
1172
- };
1173
- const result = await runWorkflow(config, 'githubReview', { workingDir: process.cwd() });
1174
- expect(mocks.githubAdapter.deleteComment).not.toHaveBeenCalled();
1175
- expect(mocks.githubAdapter.createComment).toHaveBeenCalledWith('octocat/hello-world', 7, expect.stringContaining('<!-- drs-comment-id: drs-review-summary -->'));
1176
- expect(mocks.githubAdapter.createBulkInlineComments).toHaveBeenCalledWith('octocat/hello-world', 7, [
1177
- expect.objectContaining({
1178
- body: expect.stringContaining('<!-- issue-fp: src/github.ts:1:QUALITY:Validate input -->'),
1179
- position: {
1180
- path: 'src/github.ts',
1181
- line: 1,
1182
- commitSha: 'abc123',
1183
- },
1184
- }),
1185
- ]);
1186
- expect(mocks.githubAdapter.addLabels).toHaveBeenCalledWith('octocat/hello-world', 7, [
1187
- 'ai-reviewed',
1188
- ]);
1189
- expect(result.artifacts.postResult).toMatchObject({
1190
- platform: 'github',
1191
- projectId: 'octocat/hello-world',
1192
- prNumber: 7,
1193
- issues: 1,
1194
- });
1195
- });
1196
- it('writes a GitLab code quality report from workflow review artifacts', async () => {
1197
- const projectRoot = createTempDir('drs-workflow-code-quality-');
1198
- mocks.executeReview.mockImplementation(async () => ({
1199
- issues: [
1200
- {
1201
- category: 'QUALITY',
1202
- severity: 'HIGH',
1203
- title: 'Validate input',
1204
- file: 'src/gitlab.ts',
1205
- line: 1,
1206
- problem: 'Input is not validated.',
1207
- solution: 'Validate it before use.',
1208
- agent: 'review/quality',
1209
- },
1210
- ],
1211
- summary: {
1212
- filesReviewed: 1,
1213
- issuesFound: 1,
1214
- bySeverity: { CRITICAL: 0, HIGH: 1, MEDIUM: 0, LOW: 0 },
1215
- byCategory: { SECURITY: 0, QUALITY: 1, STYLE: 0, PERFORMANCE: 0, DOCUMENTATION: 0 },
1216
- },
1217
- filesReviewed: 1,
1218
- }));
1219
- const config = {
1220
- ...baseConfig,
1221
- workflows: {
1222
- codeQuality: {
1223
- nodes: {
1224
- change: {
1225
- action: 'change-source',
1226
- with: {
1227
- type: 'gitlab-mr',
1228
- project: 'group/repo',
1229
- mr: 8,
1230
- },
1231
- output: 'change',
1232
- },
1233
- review: {
1234
- action: 'review',
1235
- needs: ['change'],
1236
- with: { source: 'change' },
1237
- output: 'review',
1238
- },
1239
- report: {
1240
- action: 'code-quality-report',
1241
- needs: ['review'],
1242
- with: {
1243
- review: 'review',
1244
- path: 'gl-code-quality-report.json',
1245
- },
1246
- output: 'codeQualityReport',
1247
- },
1248
- },
1249
- },
1250
- },
1251
- };
1252
- const result = await runWorkflow(config, 'codeQuality', { workingDir: projectRoot });
1253
- const report = JSON.parse(readFileSync(join(projectRoot, 'gl-code-quality-report.json'), 'utf-8'));
1254
- expect(report).toEqual([
1255
- expect.objectContaining({
1256
- check_name: 'drs-quality',
1257
- severity: 'critical',
1258
- location: {
1259
- path: 'src/gitlab.ts',
1260
- lines: { begin: 1 },
1261
- },
1262
- }),
1263
- ]);
1264
- expect(result.artifacts.codeQualityReport).toMatchObject({
1265
- path: 'gl-code-quality-report.json',
1266
- issues: 1,
1267
- });
1268
- });
1269
- it('loads a GitLab MR change source and reviews it', async () => {
1270
- const config = {
1271
- ...baseConfig,
1272
- review: {
1273
- ...baseConfig.review,
1274
- skipRepoCheck: true,
1275
- skipBranchCheck: true,
1276
- },
1277
- workflows: {
1278
- gitlabReview: {
1279
- inputs: {
1280
- project: '',
1281
- mr: '',
1282
- },
1283
- nodes: {
1284
- change: {
1285
- action: 'change-source',
1286
- with: {
1287
- type: 'gitlab-mr',
1288
- project: '{{inputs.project}}',
1289
- mr: '{{inputs.mr}}',
1290
- },
1291
- output: 'change',
1292
- },
1293
- review: {
1294
- action: 'review',
1295
- needs: ['change'],
1296
- with: { source: 'change' },
1297
- output: 'reviewResult',
1298
- },
1299
- },
1300
- },
1301
- },
1302
- };
1303
- await runWorkflow(config, 'gitlabReview', {
1304
- inputs: { project: 'group/repo', mr: '8' },
1305
- workingDir: process.cwd(),
1306
- });
1307
- expect(mocks.createGitLabClient).toHaveBeenCalled();
1308
- expect(mocks.GitLabPlatformAdapter).toHaveBeenCalled();
1309
- expect(mocks.gitlabAdapter.getPullRequest).toHaveBeenCalledWith('group/repo', 8);
1310
- expect(mocks.gitlabAdapter.getChangedFiles).toHaveBeenCalledWith('group/repo', 8);
1311
- expect(mocks.enforceRepoBranchMatch).toHaveBeenCalledWith(process.cwd(), 'group/repo', expect.objectContaining({ number: 8 }), {
1312
- skipRepoCheck: true,
1313
- skipBranchCheck: true,
1314
- });
1315
- expect(mocks.executeReview).toHaveBeenCalledWith(config, expect.objectContaining({
1316
- name: 'GitLab MR group/repo!8',
1317
- files: ['src/gitlab.ts'],
1318
- filesWithDiffs: [{ filename: 'src/gitlab.ts', patch: '@@ +1 @@\n+gitlab' }],
1319
- context: expect.objectContaining({
1320
- platform: 'gitlab',
1321
- projectId: 'group/repo',
1322
- }),
1323
- }));
1324
- });
1325
- it('rejects empty GitLab MR aliases without falling through to mrIid', async () => {
1326
- const config = {
1327
- ...baseConfig,
1328
- workflows: {
1329
- gitlabReview: {
1330
- inputs: {
1331
- project: 'group/repo',
1332
- mr: '',
1333
- },
1334
- nodes: {
1335
- change: {
1336
- action: 'change-source',
1337
- with: {
1338
- type: 'gitlab-mr',
1339
- project: '{{inputs.project}}',
1340
- mr: '{{inputs.mr}}',
1341
- },
1342
- output: 'change',
1343
- },
1344
- },
1345
- },
1346
- },
1347
- };
1348
- await expect(runWorkflow(config, 'gitlabReview')).rejects.toThrow('Workflow node "change" must define with.mr.');
1349
- expect(mocks.gitlabAdapter.getPullRequest).not.toHaveBeenCalled();
1350
- });
1351
- it('rejects dependency cycles', async () => {
1352
- const config = {
1353
- ...baseConfig,
1354
- workflows: {
1355
- cyclic: {
1356
- nodes: {
1357
- first: { agent: 'task/first', input: 'first', needs: ['second'] },
1358
- second: { agent: 'task/second', input: 'second', needs: ['first'] },
1359
- },
1360
- },
1361
- },
1362
- };
1363
- await expect(runWorkflow(config, 'cyclic')).rejects.toThrow('dependency cycle');
1364
- expect(mocks.runAgent).not.toHaveBeenCalled();
1365
- });
1366
- it('rejects workflows with invalid nodes config', async () => {
1367
- const config = {
1368
- ...baseConfig,
1369
- workflows: {
1370
- invalid: {
1371
- nodes: 'not an object',
1372
- },
1373
- },
1374
- };
1375
- await expect(runWorkflow(config, 'invalid')).rejects.toThrow('Workflow "invalid" must define at least one node.');
1376
- });
1377
- it('rejects unknown template references', async () => {
1378
- const config = {
1379
- ...baseConfig,
1380
- workflows: {
1381
- badTemplate: {
1382
- nodes: {
1383
- summarize: { agent: 'task/summarizer', input: '{{inputs.missing}}' },
1384
- },
1385
- },
1386
- },
1387
- };
1388
- await expect(runWorkflow(config, 'badTemplate')).rejects.toThrow('Unknown workflow template value "{{inputs.missing}}"');
1389
- expect(mocks.runAgent).not.toHaveBeenCalled();
1390
- });
1391
- it('lists workflows with packaged source by default', () => {
1392
- const config = loadConfig(process.cwd());
1393
- const entries = listWorkflows(config, { workingDir: process.cwd() });
1394
- expect(entries.some((entry) => entry.source === 'packaged' && !entry.overridden)).toBe(true);
1395
- });
1396
- it('lists project workflows that override packaged ones', () => {
1397
- const projectRoot = createTempDir('drs-workflow-list-');
1398
- mkdirSync(join(projectRoot, '.drs', 'workflows'), { recursive: true });
1399
- writeFileSync(join(projectRoot, '.drs', 'workflows', 'local-review.yaml'), 'description: Project override\nnodes:\n step:\n action: write\n input: hi\n writes: out.txt\n', 'utf-8');
1400
- const config = loadConfig(projectRoot);
1401
- const entries = listWorkflows(config, { workingDir: projectRoot });
1402
- const localReview = entries.find((entry) => entry.name === 'local-review');
1403
- expect(localReview).toMatchObject({
1404
- source: 'project',
1405
- overridden: true,
1406
- description: 'Project override',
1407
- });
1408
- });
1409
- });
1410
- //# sourceMappingURL=workflow.test.js.map