opencastle 0.26.1 → 0.27.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 (226) hide show
  1. package/README.md +7 -1
  2. package/bin/cli.mjs +10 -0
  3. package/dist/cli/agents.d.ts +3 -0
  4. package/dist/cli/agents.d.ts.map +1 -0
  5. package/dist/cli/agents.js +161 -0
  6. package/dist/cli/agents.js.map +1 -0
  7. package/dist/cli/baselines.d.ts +3 -0
  8. package/dist/cli/baselines.d.ts.map +1 -0
  9. package/dist/cli/baselines.js +128 -0
  10. package/dist/cli/baselines.js.map +1 -0
  11. package/dist/cli/convoy/engine.d.ts +68 -2
  12. package/dist/cli/convoy/engine.d.ts.map +1 -1
  13. package/dist/cli/convoy/engine.js +2102 -26
  14. package/dist/cli/convoy/engine.js.map +1 -1
  15. package/dist/cli/convoy/engine.test.js +1572 -70
  16. package/dist/cli/convoy/engine.test.js.map +1 -1
  17. package/dist/cli/convoy/events.d.ts +4 -1
  18. package/dist/cli/convoy/events.d.ts.map +1 -1
  19. package/dist/cli/convoy/events.js +74 -13
  20. package/dist/cli/convoy/events.js.map +1 -1
  21. package/dist/cli/convoy/events.test.js +154 -27
  22. package/dist/cli/convoy/events.test.js.map +1 -1
  23. package/dist/cli/convoy/expertise.d.ts +16 -0
  24. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  25. package/dist/cli/convoy/expertise.js +121 -0
  26. package/dist/cli/convoy/expertise.js.map +1 -0
  27. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  28. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  29. package/dist/cli/convoy/expertise.test.js +96 -0
  30. package/dist/cli/convoy/expertise.test.js.map +1 -0
  31. package/dist/cli/convoy/export.test.js +1 -0
  32. package/dist/cli/convoy/export.test.js.map +1 -1
  33. package/dist/cli/convoy/formula.d.ts +19 -0
  34. package/dist/cli/convoy/formula.d.ts.map +1 -0
  35. package/dist/cli/convoy/formula.js +142 -0
  36. package/dist/cli/convoy/formula.js.map +1 -0
  37. package/dist/cli/convoy/formula.test.d.ts +2 -0
  38. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  39. package/dist/cli/convoy/formula.test.js +342 -0
  40. package/dist/cli/convoy/formula.test.js.map +1 -0
  41. package/dist/cli/convoy/gates.d.ts +128 -0
  42. package/dist/cli/convoy/gates.d.ts.map +1 -0
  43. package/dist/cli/convoy/gates.js +606 -0
  44. package/dist/cli/convoy/gates.js.map +1 -0
  45. package/dist/cli/convoy/gates.test.d.ts +2 -0
  46. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  47. package/dist/cli/convoy/gates.test.js +976 -0
  48. package/dist/cli/convoy/gates.test.js.map +1 -0
  49. package/dist/cli/convoy/health.d.ts +11 -0
  50. package/dist/cli/convoy/health.d.ts.map +1 -1
  51. package/dist/cli/convoy/health.js +54 -0
  52. package/dist/cli/convoy/health.js.map +1 -1
  53. package/dist/cli/convoy/health.test.js +56 -1
  54. package/dist/cli/convoy/health.test.js.map +1 -1
  55. package/dist/cli/convoy/issues.d.ts +8 -0
  56. package/dist/cli/convoy/issues.d.ts.map +1 -0
  57. package/dist/cli/convoy/issues.js +98 -0
  58. package/dist/cli/convoy/issues.js.map +1 -0
  59. package/dist/cli/convoy/issues.test.d.ts +2 -0
  60. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  61. package/dist/cli/convoy/issues.test.js +107 -0
  62. package/dist/cli/convoy/issues.test.js.map +1 -0
  63. package/dist/cli/convoy/knowledge.d.ts +5 -0
  64. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  65. package/dist/cli/convoy/knowledge.js +116 -0
  66. package/dist/cli/convoy/knowledge.js.map +1 -0
  67. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  68. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  69. package/dist/cli/convoy/knowledge.test.js +87 -0
  70. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  71. package/dist/cli/convoy/lessons.d.ts +17 -0
  72. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  73. package/dist/cli/convoy/lessons.js +149 -0
  74. package/dist/cli/convoy/lessons.js.map +1 -0
  75. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  76. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  77. package/dist/cli/convoy/lessons.test.js +135 -0
  78. package/dist/cli/convoy/lessons.test.js.map +1 -0
  79. package/dist/cli/convoy/lock.d.ts +13 -0
  80. package/dist/cli/convoy/lock.d.ts.map +1 -0
  81. package/dist/cli/convoy/lock.js +88 -0
  82. package/dist/cli/convoy/lock.js.map +1 -0
  83. package/dist/cli/convoy/lock.test.d.ts +2 -0
  84. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  85. package/dist/cli/convoy/lock.test.js +136 -0
  86. package/dist/cli/convoy/lock.test.js.map +1 -0
  87. package/dist/cli/convoy/merge.d.ts +4 -0
  88. package/dist/cli/convoy/merge.d.ts.map +1 -1
  89. package/dist/cli/convoy/merge.js +18 -1
  90. package/dist/cli/convoy/merge.js.map +1 -1
  91. package/dist/cli/convoy/merge.test.js +6 -7
  92. package/dist/cli/convoy/merge.test.js.map +1 -1
  93. package/dist/cli/convoy/partition.d.ts +51 -0
  94. package/dist/cli/convoy/partition.d.ts.map +1 -0
  95. package/dist/cli/convoy/partition.js +186 -0
  96. package/dist/cli/convoy/partition.js.map +1 -0
  97. package/dist/cli/convoy/partition.test.d.ts +2 -0
  98. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  99. package/dist/cli/convoy/partition.test.js +315 -0
  100. package/dist/cli/convoy/partition.test.js.map +1 -0
  101. package/dist/cli/convoy/pipeline.test.js +6 -0
  102. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  103. package/dist/cli/convoy/store.d.ts +47 -5
  104. package/dist/cli/convoy/store.d.ts.map +1 -1
  105. package/dist/cli/convoy/store.js +525 -19
  106. package/dist/cli/convoy/store.js.map +1 -1
  107. package/dist/cli/convoy/store.test.js +1345 -12
  108. package/dist/cli/convoy/store.test.js.map +1 -1
  109. package/dist/cli/convoy/types.d.ts +156 -2
  110. package/dist/cli/convoy/types.d.ts.map +1 -1
  111. package/dist/cli/destroy.d.ts +3 -0
  112. package/dist/cli/destroy.d.ts.map +1 -0
  113. package/dist/cli/destroy.js +69 -0
  114. package/dist/cli/destroy.js.map +1 -0
  115. package/dist/cli/destroy.test.d.ts +2 -0
  116. package/dist/cli/destroy.test.d.ts.map +1 -0
  117. package/dist/cli/destroy.test.js +116 -0
  118. package/dist/cli/destroy.test.js.map +1 -0
  119. package/dist/cli/gitignore.d.ts +9 -0
  120. package/dist/cli/gitignore.d.ts.map +1 -1
  121. package/dist/cli/gitignore.js +29 -0
  122. package/dist/cli/gitignore.js.map +1 -1
  123. package/dist/cli/plan.d.ts +3 -0
  124. package/dist/cli/plan.d.ts.map +1 -0
  125. package/dist/cli/plan.js +288 -0
  126. package/dist/cli/plan.js.map +1 -0
  127. package/dist/cli/run/adapters/claude.d.ts +2 -0
  128. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  129. package/dist/cli/run/adapters/claude.js +89 -49
  130. package/dist/cli/run/adapters/claude.js.map +1 -1
  131. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  132. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  133. package/dist/cli/run/adapters/claude.test.js +205 -0
  134. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  135. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  137. package/dist/cli/run/adapters/copilot.js +84 -46
  138. package/dist/cli/run/adapters/copilot.js.map +1 -1
  139. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  140. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  141. package/dist/cli/run/adapters/copilot.test.js +195 -0
  142. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  143. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  145. package/dist/cli/run/adapters/cursor.js +83 -47
  146. package/dist/cli/run/adapters/cursor.js.map +1 -1
  147. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  148. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  149. package/dist/cli/run/adapters/cursor.test.js +129 -0
  150. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  151. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  153. package/dist/cli/run/adapters/opencode.js +81 -47
  154. package/dist/cli/run/adapters/opencode.js.map +1 -1
  155. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  156. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  157. package/dist/cli/run/adapters/opencode.test.js +119 -0
  158. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  159. package/dist/cli/run/executor.js +1 -1
  160. package/dist/cli/run/executor.js.map +1 -1
  161. package/dist/cli/run/schema.d.ts.map +1 -1
  162. package/dist/cli/run/schema.js +245 -4
  163. package/dist/cli/run/schema.js.map +1 -1
  164. package/dist/cli/run/schema.test.js +669 -0
  165. package/dist/cli/run/schema.test.js.map +1 -1
  166. package/dist/cli/run.d.ts.map +1 -1
  167. package/dist/cli/run.js +362 -22
  168. package/dist/cli/run.js.map +1 -1
  169. package/dist/cli/types.d.ts +85 -2
  170. package/dist/cli/types.d.ts.map +1 -1
  171. package/dist/cli/types.js.map +1 -1
  172. package/dist/cli/watch.d.ts +15 -0
  173. package/dist/cli/watch.d.ts.map +1 -0
  174. package/dist/cli/watch.js +279 -0
  175. package/dist/cli/watch.js.map +1 -0
  176. package/package.json +1 -1
  177. package/src/cli/agents.ts +177 -0
  178. package/src/cli/baselines.ts +143 -0
  179. package/src/cli/convoy/engine.test.ts +1839 -70
  180. package/src/cli/convoy/engine.ts +2417 -38
  181. package/src/cli/convoy/events.test.ts +179 -38
  182. package/src/cli/convoy/events.ts +88 -16
  183. package/src/cli/convoy/expertise.test.ts +128 -0
  184. package/src/cli/convoy/expertise.ts +163 -0
  185. package/src/cli/convoy/export.test.ts +1 -0
  186. package/src/cli/convoy/formula.test.ts +405 -0
  187. package/src/cli/convoy/formula.ts +174 -0
  188. package/src/cli/convoy/gates.test.ts +1169 -0
  189. package/src/cli/convoy/gates.ts +774 -0
  190. package/src/cli/convoy/health.test.ts +64 -2
  191. package/src/cli/convoy/health.ts +80 -2
  192. package/src/cli/convoy/issues.test.ts +143 -0
  193. package/src/cli/convoy/issues.ts +136 -0
  194. package/src/cli/convoy/knowledge.test.ts +101 -0
  195. package/src/cli/convoy/knowledge.ts +132 -0
  196. package/src/cli/convoy/lessons.test.ts +188 -0
  197. package/src/cli/convoy/lessons.ts +164 -0
  198. package/src/cli/convoy/lock.test.ts +181 -0
  199. package/src/cli/convoy/lock.ts +103 -0
  200. package/src/cli/convoy/merge.test.ts +6 -7
  201. package/src/cli/convoy/merge.ts +19 -1
  202. package/src/cli/convoy/partition.test.ts +423 -0
  203. package/src/cli/convoy/partition.ts +232 -0
  204. package/src/cli/convoy/pipeline.test.ts +6 -0
  205. package/src/cli/convoy/store.test.ts +1512 -14
  206. package/src/cli/convoy/store.ts +676 -30
  207. package/src/cli/convoy/types.ts +170 -1
  208. package/src/cli/destroy.test.ts +141 -0
  209. package/src/cli/destroy.ts +88 -0
  210. package/src/cli/gitignore.ts +36 -0
  211. package/src/cli/plan.ts +316 -0
  212. package/src/cli/run/adapters/claude.test.ts +234 -0
  213. package/src/cli/run/adapters/claude.ts +45 -5
  214. package/src/cli/run/adapters/copilot.test.ts +224 -0
  215. package/src/cli/run/adapters/copilot.ts +34 -4
  216. package/src/cli/run/adapters/cursor.test.ts +144 -0
  217. package/src/cli/run/adapters/cursor.ts +33 -2
  218. package/src/cli/run/adapters/opencode.test.ts +135 -0
  219. package/src/cli/run/adapters/opencode.ts +30 -2
  220. package/src/cli/run/executor.ts +1 -1
  221. package/src/cli/run/schema.test.ts +758 -0
  222. package/src/cli/run/schema.ts +300 -25
  223. package/src/cli/run.ts +341 -21
  224. package/src/cli/types.ts +86 -1
  225. package/src/cli/watch.ts +298 -0
  226. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
@@ -0,0 +1,232 @@
1
+ import { statSync, realpathSync, lstatSync, writeFileSync, rmSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { normalize, join, resolve } from 'node:path'
4
+ import type { Task } from '../types.js'
5
+
6
+ // ── Path normalization ────────────────────────────────────────────────────────
7
+
8
+ /**
9
+ * Normalize a file path for partition comparison.
10
+ * - Rejects glob patterns (* or ?)
11
+ * - Strips leading ./ and /
12
+ * - Replaces backslashes with forward slashes
13
+ * - Resolves . and .. via path.normalize()
14
+ * - Preserves trailing slash for directories
15
+ */
16
+ export function normalizePath(p: string): string {
17
+ if (p.includes('*') || p.includes('?')) {
18
+ throw new Error(`Glob patterns are not allowed in file paths: "${p}"`)
19
+ }
20
+
21
+ // Record whether the path indicates a directory (trailing slash)
22
+ const hasTrailingSlash = p.endsWith('/') || p.endsWith('\\')
23
+
24
+ // Normalize separators to forward slash
25
+ let result = p.replace(/\\/g, '/')
26
+
27
+ // Strip trailing slashes before further processing
28
+ result = result.replace(/\/+$/, '')
29
+
30
+ // Strip leading './' (may be multiple, e.g. '././')
31
+ result = result.replace(/^(\.\/)+/, '')
32
+
33
+ // Strip leading '/'
34
+ result = result.replace(/^\/+/, '')
35
+
36
+ // Reject any .. path segment — even those that would not escape the root.
37
+ // All usage of .. is rejected for safety, not just escaping traversals.
38
+ if (/(^|\/)\.\.(\/|$)/.test(result)) {
39
+ throw new Error(`Path traversal detected: "${p}" resolves to a path containing ".." segments`)
40
+ }
41
+
42
+ // Resolve '.' and '..' segments
43
+ result = normalize(result).replace(/\\/g, '/')
44
+
45
+ // normalize can introduce leading './' (e.g. for '.') — strip it again
46
+ result = result.replace(/^(\.\/)+/, '')
47
+ result = result.replace(/^\/+/, '')
48
+
49
+ // Restore trailing slash for directories (but not when result is '.' or empty)
50
+ if (hasTrailingSlash && result !== '.' && result !== '') {
51
+ result += '/'
52
+ }
53
+
54
+ return result
55
+ }
56
+
57
+ // ── Overlap detection ─────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Returns true if path a and path b overlap (exact match or prefix containment).
61
+ * Example: 'src/auth/' overlaps 'src/auth/service.ts' in both directions.
62
+ */
63
+ export function pathsOverlap(a: string, b: string): boolean {
64
+ if (a === b) return true
65
+ // Treat each path as a potential directory prefix
66
+ const aDir = a.endsWith('/') ? a : a + '/'
67
+ const bDir = b.endsWith('/') ? b : b + '/'
68
+ return b.startsWith(aDir) || a.startsWith(bDir)
69
+ }
70
+
71
+ // ── Partition validation ──────────────────────────────────────────────────────
72
+
73
+ export interface PartitionConflict {
74
+ phase: number
75
+ taskA: string
76
+ taskB: string
77
+ overlapping: string[]
78
+ }
79
+
80
+ export interface PartitionValidationResult {
81
+ valid: boolean
82
+ conflicts: PartitionConflict[]
83
+ }
84
+
85
+ /**
86
+ * Validate that tasks within the same parallel phase do not have overlapping file partitions.
87
+ * Tasks in different phases (sequential) are allowed to share files.
88
+ */
89
+ export function validateFilePartitions(
90
+ _tasks: Task[],
91
+ phases: Task[][],
92
+ ): PartitionValidationResult {
93
+ const isCaseSensitive = determineFsCaseSensitivity()
94
+ const conflicts: PartitionConflict[] = []
95
+
96
+ for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
97
+ const phaseTasks = phases[phaseIdx]
98
+ for (let i = 0; i < phaseTasks.length; i++) {
99
+ for (let j = i + 1; j < phaseTasks.length; j++) {
100
+ const taskA = phaseTasks[i]
101
+ const taskB = phaseTasks[j]
102
+
103
+ // Empty files arrays are not partitioned — skip
104
+ if (!taskA.files.length || !taskB.files.length) continue
105
+
106
+ const normalizedA = taskA.files.map(normalizePath)
107
+ const normalizedB = taskB.files.map(normalizePath)
108
+ const overlapping: string[] = []
109
+
110
+ for (const fileA of normalizedA) {
111
+ for (const fileB of normalizedB) {
112
+ const directOverlap = pathsOverlap(fileA, fileB)
113
+ // On case-insensitive filesystems, also check lowercased paths
114
+ const ciOverlap =
115
+ !isCaseSensitive && pathsOverlap(fileA.toLowerCase(), fileB.toLowerCase())
116
+ if ((directOverlap || ciOverlap) && !overlapping.includes(fileA)) {
117
+ overlapping.push(fileA)
118
+ }
119
+ }
120
+ }
121
+
122
+ if (overlapping.length > 0) {
123
+ conflicts.push({ phase: phaseIdx, taskA: taskA.id, taskB: taskB.id, overlapping })
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return { valid: conflicts.length === 0, conflicts }
130
+ }
131
+
132
+ // ── Filesystem case-sensitivity probe ────────────────────────────────────────
133
+
134
+ /**
135
+ * Probe whether the filesystem is case-sensitive by creating a mixed-case temp file
136
+ * and checking if the lowercase path resolves to the same inode.
137
+ *
138
+ * Uses realpathSync per LES-003: on macOS, os.tmpdir() returns /var/... which is a
139
+ * symlink to /private/var/... — realpathSync resolves this to the canonical path.
140
+ *
141
+ * Returns true if case-sensitive (git-compatible default), false if case-insensitive.
142
+ */
143
+ export function determineFsCaseSensitivity(): boolean {
144
+ const base = realpathSync(tmpdir())
145
+ const mixedCase = join(base, `OpenCastle_CaseSensitivity_${Date.now()}`)
146
+ const lowerCase = mixedCase.toLowerCase()
147
+
148
+ try {
149
+ writeFileSync(mixedCase, '')
150
+ try {
151
+ const statMixed = statSync(mixedCase)
152
+ const statLower = statSync(lowerCase)
153
+ // Same inode → same file → case-insensitive
154
+ return statMixed.ino !== statLower.ino
155
+ } catch {
156
+ // stat(lowerCase) threw → file not found at lowercase path → case-sensitive
157
+ return true
158
+ }
159
+ } finally {
160
+ try { rmSync(mixedCase) } catch { /* ignore cleanup errors */ }
161
+ }
162
+ }
163
+
164
+ // ── Symlink security scan ─────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Before task execution: scan each file in the task's files[] partition.
168
+ * If any resolved symlink target escapes the basePath directory, throw symlink_escape.
169
+ */
170
+ export function scanSymlinks(files: string[], basePath: string): void {
171
+ const realBase = realpathSync(resolve(basePath))
172
+
173
+ for (const file of files) {
174
+ const absPath = join(realBase, normalizePath(file))
175
+ let stat: ReturnType<typeof lstatSync>
176
+ try {
177
+ stat = lstatSync(absPath)
178
+ } catch {
179
+ continue // file doesn't exist yet — skip
180
+ }
181
+
182
+ if (stat.isSymbolicLink()) {
183
+ let realTarget: string
184
+ try {
185
+ realTarget = realpathSync(absPath)
186
+ } catch {
187
+ throw new Error(`symlink_escape: symlink at "${file}" could not be resolved`)
188
+ }
189
+
190
+ if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
191
+ throw new Error(
192
+ `symlink_escape: "${file}" is a symlink that resolves outside the partition`,
193
+ )
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * After task execution: scan files[] in the worktree for new symlinks that escape the partition.
201
+ * Throws symlink_escape_post_task if any symlink target is outside worktreePath.
202
+ */
203
+ export function scanNewSymlinks(worktreePath: string, files: string[]): void {
204
+ const realBase = realpathSync(resolve(worktreePath))
205
+
206
+ for (const file of files) {
207
+ const absPath = join(realBase, normalizePath(file))
208
+ let stat: ReturnType<typeof lstatSync>
209
+ try {
210
+ stat = lstatSync(absPath)
211
+ } catch {
212
+ continue
213
+ }
214
+
215
+ if (stat.isSymbolicLink()) {
216
+ let realTarget: string
217
+ try {
218
+ realTarget = realpathSync(absPath)
219
+ } catch {
220
+ throw new Error(
221
+ `symlink_escape_post_task: "${file}" is a new symlink that cannot be resolved`,
222
+ )
223
+ }
224
+
225
+ if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
226
+ throw new Error(
227
+ `symlink_escape_post_task: "${file}" is a new symlink that escapes the partition`,
228
+ )
229
+ }
230
+ }
231
+ }
232
+ }
@@ -92,6 +92,8 @@ function makeEngineFactory(runResults: ConvoyResult[]) {
92
92
  return {
93
93
  run: vi.fn().mockResolvedValue(result),
94
94
  resume: vi.fn().mockResolvedValue(makeConvoyResult()),
95
+ retryFailed: vi.fn(),
96
+ injectTask: vi.fn(),
95
97
  }
96
98
  })
97
99
  }
@@ -443,6 +445,8 @@ describe('pipeline record persistence', () => {
443
445
  return makeConvoyResult()
444
446
  }),
445
447
  resume: vi.fn(),
448
+ retryFailed: vi.fn(),
449
+ injectTask: vi.fn(),
446
450
  }))
447
451
 
448
452
  const result = await createPipelineOrchestrator({
@@ -653,6 +657,8 @@ describe('pipeline resume', () => {
653
657
  const mockEngine: ConvoyEngine = {
654
658
  run: vi.fn().mockResolvedValue(makeConvoyResult()),
655
659
  resume: vi.fn().mockResolvedValue(resumedResult),
660
+ retryFailed: vi.fn(),
661
+ injectTask: vi.fn(),
656
662
  }
657
663
  const factory = vi.fn().mockReturnValue(mockEngine)
658
664