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
@@ -1008,4 +1008,673 @@ describe('applyDefaults — gate_retries default', () => {
1008
1008
  expect(spec.gate_retries).toBe(2);
1009
1009
  });
1010
1010
  });
1011
+ // ── guard config validation ─────────────────────────────────────
1012
+ describe('guard config', () => {
1013
+ const baseSpec = {
1014
+ name: 'test-run',
1015
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1016
+ };
1017
+ it('accepts a valid guard config', () => {
1018
+ const result = validateSpec({
1019
+ ...baseSpec,
1020
+ guard: { enabled: true, agent: 'session-guard', checks: ['observability', 'cleanup'] },
1021
+ });
1022
+ expect(result.valid).toBe(true);
1023
+ expect(result.errors).toHaveLength(0);
1024
+ });
1025
+ it('accepts guard with only enabled: false', () => {
1026
+ const result = validateSpec({ ...baseSpec, guard: { enabled: false } });
1027
+ expect(result.valid).toBe(true);
1028
+ });
1029
+ it('accepts guard with no fields (all optional)', () => {
1030
+ const result = validateSpec({ ...baseSpec, guard: {} });
1031
+ expect(result.valid).toBe(true);
1032
+ });
1033
+ it('rejects guard.enabled that is not a boolean', () => {
1034
+ const result = validateSpec({ ...baseSpec, guard: { enabled: 'yes' } });
1035
+ expect(result.valid).toBe(false);
1036
+ expect(result.errors).toContainEqual(expect.stringContaining('guard.enabled'));
1037
+ });
1038
+ it('rejects guard.agent that is not a string', () => {
1039
+ const result = validateSpec({ ...baseSpec, guard: { agent: 42 } });
1040
+ expect(result.valid).toBe(false);
1041
+ expect(result.errors).toContainEqual(expect.stringContaining('guard.agent'));
1042
+ });
1043
+ it('rejects guard.checks that is not an array', () => {
1044
+ const result = validateSpec({ ...baseSpec, guard: { checks: 'observability' } });
1045
+ expect(result.valid).toBe(false);
1046
+ expect(result.errors).toContainEqual(expect.stringContaining('guard.checks'));
1047
+ });
1048
+ it('rejects guard.checks with empty string entries', () => {
1049
+ const result = validateSpec({ ...baseSpec, guard: { checks: ['valid', ''] } });
1050
+ expect(result.valid).toBe(false);
1051
+ expect(result.errors).toContainEqual(expect.stringContaining('guard.checks'));
1052
+ });
1053
+ });
1054
+ // ── review field validation ────────────────────────────────────────────────────
1055
+ describe('review defaults validation', () => {
1056
+ const baseV1Spec = {
1057
+ name: 'test',
1058
+ version: 1,
1059
+ tasks: [{ id: 't1', prompt: 'Do it' }],
1060
+ };
1061
+ it('accepts valid defaults.review values', () => {
1062
+ for (const r of ['auto', 'fast', 'panel', 'none']) {
1063
+ const result = validateSpec({ ...baseV1Spec, defaults: { review: r } });
1064
+ expect(result.valid).toBe(true);
1065
+ }
1066
+ });
1067
+ it('rejects invalid defaults.review value', () => {
1068
+ const result = validateSpec({ ...baseV1Spec, defaults: { review: 'always' } });
1069
+ expect(result.valid).toBe(false);
1070
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.review'));
1071
+ });
1072
+ it('accepts defaults.reviewer_model string', () => {
1073
+ const result = validateSpec({ ...baseV1Spec, defaults: { reviewer_model: 'gpt-4' } });
1074
+ expect(result.valid).toBe(true);
1075
+ });
1076
+ it('rejects defaults.reviewer_model non-string', () => {
1077
+ const result = validateSpec({ ...baseV1Spec, defaults: { reviewer_model: 42 } });
1078
+ expect(result.valid).toBe(false);
1079
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.reviewer_model'));
1080
+ });
1081
+ it('accepts defaults.review_budget positive integer', () => {
1082
+ const result = validateSpec({ ...baseV1Spec, defaults: { review_budget: 1000 } });
1083
+ expect(result.valid).toBe(true);
1084
+ });
1085
+ it('rejects defaults.review_budget of 0', () => {
1086
+ const result = validateSpec({ ...baseV1Spec, defaults: { review_budget: 0 } });
1087
+ expect(result.valid).toBe(false);
1088
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.review_budget'));
1089
+ });
1090
+ it('accepts valid on_review_budget_exceeded values', () => {
1091
+ for (const v of ['skip', 'downgrade', 'stop']) {
1092
+ const result = validateSpec({ ...baseV1Spec, defaults: { on_review_budget_exceeded: v } });
1093
+ expect(result.valid).toBe(true);
1094
+ }
1095
+ });
1096
+ it('rejects invalid on_review_budget_exceeded', () => {
1097
+ const result = validateSpec({ ...baseV1Spec, defaults: { on_review_budget_exceeded: 'ignore' } });
1098
+ expect(result.valid).toBe(false);
1099
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.on_review_budget_exceeded'));
1100
+ });
1101
+ it('accepts defaults.max_concurrent_reviews positive integer', () => {
1102
+ const result = validateSpec({ ...baseV1Spec, defaults: { max_concurrent_reviews: 3 } });
1103
+ expect(result.valid).toBe(true);
1104
+ });
1105
+ it('rejects defaults.max_concurrent_reviews of 0', () => {
1106
+ const result = validateSpec({ ...baseV1Spec, defaults: { max_concurrent_reviews: 0 } });
1107
+ expect(result.valid).toBe(false);
1108
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.max_concurrent_reviews'));
1109
+ });
1110
+ it('accepts valid review_heuristics object', () => {
1111
+ const result = validateSpec({
1112
+ ...baseV1Spec,
1113
+ defaults: {
1114
+ review_heuristics: {
1115
+ panel_paths: ['auth/', 'security/'],
1116
+ panel_agents: ['security-expert'],
1117
+ auto_pass_agents: ['copywriter'],
1118
+ auto_pass_max_lines: 20,
1119
+ auto_pass_max_files: 3,
1120
+ },
1121
+ },
1122
+ });
1123
+ expect(result.valid).toBe(true);
1124
+ });
1125
+ it('rejects review_heuristics as non-object', () => {
1126
+ const result = validateSpec({ ...baseV1Spec, defaults: { review_heuristics: 'fast' } });
1127
+ expect(result.valid).toBe(false);
1128
+ expect(result.errors).toContainEqual(expect.stringContaining('defaults.review_heuristics'));
1129
+ });
1130
+ it('rejects review_heuristics.panel_paths with non-string entries', () => {
1131
+ const result = validateSpec({ ...baseV1Spec, defaults: { review_heuristics: { panel_paths: [1, 2] } } });
1132
+ expect(result.valid).toBe(false);
1133
+ expect(result.errors).toContainEqual(expect.stringContaining('review_heuristics.panel_paths'));
1134
+ });
1135
+ it('rejects review_heuristics.auto_pass_max_lines of 0', () => {
1136
+ const result = validateSpec({ ...baseV1Spec, defaults: { review_heuristics: { auto_pass_max_lines: 0 } } });
1137
+ expect(result.valid).toBe(false);
1138
+ expect(result.errors).toContainEqual(expect.stringContaining('auto_pass_max_lines'));
1139
+ });
1140
+ });
1141
+ describe('per-task review field validation', () => {
1142
+ const taskBase = { name: 'test', tasks: [{ id: 't1', prompt: 'Do it' }] };
1143
+ it('accepts valid per-task review values', () => {
1144
+ for (const r of ['auto', 'fast', 'panel', 'none']) {
1145
+ const result = validateSpec({ ...taskBase, tasks: [{ id: 't1', prompt: 'Do it', review: r }] });
1146
+ expect(result.valid).toBe(true);
1147
+ }
1148
+ });
1149
+ it('rejects invalid per-task review value', () => {
1150
+ const result = validateSpec({ ...taskBase, tasks: [{ id: 't1', prompt: 'Do it', review: 'always' }] });
1151
+ expect(result.valid).toBe(false);
1152
+ expect(result.errors).toContainEqual(expect.stringContaining('tasks[0]'));
1153
+ expect(result.errors).toContainEqual(expect.stringContaining('review'));
1154
+ });
1155
+ });
1156
+ describe('applyDefaults review merge', () => {
1157
+ it('merges defaults.review into tasks when task has no review set', () => {
1158
+ const spec = applyDefaults({
1159
+ name: 'test',
1160
+ version: 1,
1161
+ tasks: [{ id: 't1', prompt: 'Do it' }],
1162
+ defaults: { review: 'panel' },
1163
+ });
1164
+ expect(spec.tasks[0].review).toBe('panel');
1165
+ });
1166
+ it('task-level review overrides defaults.review', () => {
1167
+ const spec = applyDefaults({
1168
+ name: 'test',
1169
+ version: 1,
1170
+ tasks: [{ id: 't1', prompt: 'Do it', review: 'none' }],
1171
+ defaults: { review: 'panel' },
1172
+ });
1173
+ expect(spec.tasks[0].review).toBe('none');
1174
+ });
1175
+ it('review remains undefined when not set in defaults or task', () => {
1176
+ const spec = applyDefaults({
1177
+ name: 'test',
1178
+ version: 1,
1179
+ tasks: [{ id: 't1', prompt: 'Do it' }],
1180
+ defaults: {},
1181
+ });
1182
+ expect(spec.tasks[0].review).toBeUndefined();
1183
+ });
1184
+ });
1185
+ describe('concurrency: auto (swarm mode)', () => {
1186
+ it('accepts concurrency: auto', () => {
1187
+ const result = validateSpec({
1188
+ name: 'test',
1189
+ concurrency: 'auto',
1190
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1191
+ });
1192
+ expect(result.valid).toBe(true);
1193
+ });
1194
+ it('accepts concurrency: 1', () => {
1195
+ const result = validateSpec({
1196
+ name: 'test',
1197
+ concurrency: 1,
1198
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1199
+ });
1200
+ expect(result.valid).toBe(true);
1201
+ });
1202
+ it('rejects concurrency: 0', () => {
1203
+ const result = validateSpec({
1204
+ name: 'test',
1205
+ concurrency: 0,
1206
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1207
+ });
1208
+ expect(result.valid).toBe(false);
1209
+ expect(result.errors[0]).toContain('concurrency');
1210
+ });
1211
+ it('rejects concurrency: 51', () => {
1212
+ const result = validateSpec({
1213
+ name: 'test',
1214
+ concurrency: 51,
1215
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1216
+ });
1217
+ expect(result.valid).toBe(false);
1218
+ });
1219
+ it('rejects concurrency: "invalid"', () => {
1220
+ const result = validateSpec({
1221
+ name: 'test',
1222
+ concurrency: 'invalid',
1223
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1224
+ });
1225
+ expect(result.valid).toBe(false);
1226
+ });
1227
+ });
1228
+ describe('defaults.max_swarm_concurrency', () => {
1229
+ it('accepts valid max_swarm_concurrency', () => {
1230
+ const result = validateSpec({
1231
+ name: 'test',
1232
+ version: 1,
1233
+ defaults: { max_swarm_concurrency: 8 },
1234
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1235
+ });
1236
+ expect(result.valid).toBe(true);
1237
+ });
1238
+ it('rejects max_swarm_concurrency: 0', () => {
1239
+ const result = validateSpec({
1240
+ name: 'test',
1241
+ version: 1,
1242
+ defaults: { max_swarm_concurrency: 0 },
1243
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1244
+ });
1245
+ expect(result.valid).toBe(false);
1246
+ expect(result.errors[0]).toContain('max_swarm_concurrency');
1247
+ });
1248
+ it('rejects max_swarm_concurrency: 51', () => {
1249
+ const result = validateSpec({
1250
+ name: 'test',
1251
+ version: 1,
1252
+ defaults: { max_swarm_concurrency: 51 },
1253
+ tasks: [{ id: 't1', prompt: 'do stuff' }],
1254
+ });
1255
+ expect(result.valid).toBe(false);
1256
+ });
1257
+ });
1258
+ // ── validateSpec — MCP server config in defaults ──────────────
1259
+ describe('validateSpec — MCP server config in defaults', () => {
1260
+ const validSpec = {
1261
+ name: 'test-run',
1262
+ version: 1,
1263
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1264
+ };
1265
+ it('accepts valid mcp_servers in defaults', () => {
1266
+ const result = validateSpec({
1267
+ ...validSpec,
1268
+ defaults: {
1269
+ mcp_servers: [
1270
+ { name: 'github', type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
1271
+ ],
1272
+ },
1273
+ });
1274
+ expect(result.valid).toBe(true);
1275
+ });
1276
+ it('accepts mcp_servers with all optional fields', () => {
1277
+ const result = validateSpec({
1278
+ ...validSpec,
1279
+ defaults: {
1280
+ mcp_servers: [
1281
+ { name: 'remote', type: 'sse', url: 'https://mcp.example.com', local: false, config: { key: 'val' } },
1282
+ ],
1283
+ },
1284
+ });
1285
+ expect(result.valid).toBe(true);
1286
+ });
1287
+ it('accepts empty mcp_servers array', () => {
1288
+ const result = validateSpec({
1289
+ ...validSpec,
1290
+ defaults: { mcp_servers: [] },
1291
+ });
1292
+ expect(result.valid).toBe(true);
1293
+ });
1294
+ it('rejects mcp_servers as non-array', () => {
1295
+ const result = validateSpec({
1296
+ ...validSpec,
1297
+ defaults: { mcp_servers: 'not-array' },
1298
+ });
1299
+ expect(result.valid).toBe(false);
1300
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_servers'));
1301
+ });
1302
+ it('rejects mcp_server entry without name', () => {
1303
+ const result = validateSpec({
1304
+ ...validSpec,
1305
+ defaults: {
1306
+ mcp_servers: [{ type: 'stdio' }],
1307
+ },
1308
+ });
1309
+ expect(result.valid).toBe(false);
1310
+ expect(result.errors).toContainEqual(expect.stringContaining('name'));
1311
+ });
1312
+ it('rejects mcp_server entry without type', () => {
1313
+ const result = validateSpec({
1314
+ ...validSpec,
1315
+ defaults: {
1316
+ mcp_servers: [{ name: 'github' }],
1317
+ },
1318
+ });
1319
+ expect(result.valid).toBe(false);
1320
+ expect(result.errors).toContainEqual(expect.stringContaining('type'));
1321
+ });
1322
+ it('rejects mcp_server entry with non-string name', () => {
1323
+ const result = validateSpec({
1324
+ ...validSpec,
1325
+ defaults: {
1326
+ mcp_servers: [{ name: 123, type: 'stdio' }],
1327
+ },
1328
+ });
1329
+ expect(result.valid).toBe(false);
1330
+ expect(result.errors).toContainEqual(expect.stringContaining('name'));
1331
+ });
1332
+ it('rejects mcp_server entry with non-boolean local', () => {
1333
+ const result = validateSpec({
1334
+ ...validSpec,
1335
+ defaults: {
1336
+ mcp_servers: [{ name: 'x', type: 'stdio', local: 'yes' }],
1337
+ },
1338
+ });
1339
+ expect(result.valid).toBe(false);
1340
+ expect(result.errors).toContainEqual(expect.stringContaining('local'));
1341
+ });
1342
+ it('rejects mcp_server entry with non-string command', () => {
1343
+ const result = validateSpec({
1344
+ ...validSpec,
1345
+ defaults: {
1346
+ mcp_servers: [{ name: 'x', type: 'stdio', command: 42 }],
1347
+ },
1348
+ });
1349
+ expect(result.valid).toBe(false);
1350
+ expect(result.errors).toContainEqual(expect.stringContaining('command'));
1351
+ });
1352
+ it('rejects mcp_server entry with non-array args', () => {
1353
+ const result = validateSpec({
1354
+ ...validSpec,
1355
+ defaults: {
1356
+ mcp_servers: [{ name: 'x', type: 'stdio', args: 'not-array' }],
1357
+ },
1358
+ });
1359
+ expect(result.valid).toBe(false);
1360
+ expect(result.errors).toContainEqual(expect.stringContaining('args'));
1361
+ });
1362
+ it('rejects mcp_server entry with non-string args items', () => {
1363
+ const result = validateSpec({
1364
+ ...validSpec,
1365
+ defaults: {
1366
+ mcp_servers: [{ name: 'x', type: 'stdio', args: [1, 2] }],
1367
+ },
1368
+ });
1369
+ expect(result.valid).toBe(false);
1370
+ expect(result.errors).toContainEqual(expect.stringContaining('args'));
1371
+ });
1372
+ it('rejects mcp_server entry with non-string url', () => {
1373
+ const result = validateSpec({
1374
+ ...validSpec,
1375
+ defaults: {
1376
+ mcp_servers: [{ name: 'x', type: 'sse', url: 42 }],
1377
+ },
1378
+ });
1379
+ expect(result.valid).toBe(false);
1380
+ expect(result.errors).toContainEqual(expect.stringContaining('url'));
1381
+ });
1382
+ it('rejects mcp_server entry with non-object config', () => {
1383
+ const result = validateSpec({
1384
+ ...validSpec,
1385
+ defaults: {
1386
+ mcp_servers: [{ name: 'x', type: 'stdio', config: 'not-object' }],
1387
+ },
1388
+ });
1389
+ expect(result.valid).toBe(false);
1390
+ expect(result.errors).toContainEqual(expect.stringContaining('config'));
1391
+ });
1392
+ it('rejects mcp_server entry with array config', () => {
1393
+ const result = validateSpec({
1394
+ ...validSpec,
1395
+ defaults: {
1396
+ mcp_servers: [{ name: 'x', type: 'stdio', config: [1, 2] }],
1397
+ },
1398
+ });
1399
+ expect(result.valid).toBe(false);
1400
+ expect(result.errors).toContainEqual(expect.stringContaining('config'));
1401
+ });
1402
+ it('validates multiple mcp_server entries independently', () => {
1403
+ const result = validateSpec({
1404
+ ...validSpec,
1405
+ defaults: {
1406
+ mcp_servers: [
1407
+ { name: 'valid', type: 'stdio' },
1408
+ { name: 123, type: 'stdio' },
1409
+ ],
1410
+ },
1411
+ });
1412
+ expect(result.valid).toBe(false);
1413
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_servers[1].name'));
1414
+ });
1415
+ it('rejects non-object mcp_server entry', () => {
1416
+ const result = validateSpec({
1417
+ ...validSpec,
1418
+ defaults: {
1419
+ mcp_servers: ['not-an-object'],
1420
+ },
1421
+ });
1422
+ expect(result.valid).toBe(false);
1423
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_servers[0]'));
1424
+ });
1425
+ });
1426
+ // ── validateSpec — mcp_approve_all in defaults ────────────────
1427
+ describe('validateSpec — mcp_approve_all in defaults', () => {
1428
+ const validSpec = {
1429
+ name: 'test-run',
1430
+ version: 1,
1431
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1432
+ };
1433
+ it('accepts mcp_approve_all as true', () => {
1434
+ const result = validateSpec({
1435
+ ...validSpec,
1436
+ defaults: { mcp_approve_all: true },
1437
+ });
1438
+ expect(result.valid).toBe(true);
1439
+ });
1440
+ it('accepts mcp_approve_all as false', () => {
1441
+ const result = validateSpec({
1442
+ ...validSpec,
1443
+ defaults: { mcp_approve_all: false },
1444
+ });
1445
+ expect(result.valid).toBe(true);
1446
+ });
1447
+ it('rejects mcp_approve_all as string', () => {
1448
+ const result = validateSpec({
1449
+ ...validSpec,
1450
+ defaults: { mcp_approve_all: 'yes' },
1451
+ });
1452
+ expect(result.valid).toBe(false);
1453
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_approve_all'));
1454
+ });
1455
+ it('rejects mcp_approve_all as number', () => {
1456
+ const result = validateSpec({
1457
+ ...validSpec,
1458
+ defaults: { mcp_approve_all: 1 },
1459
+ });
1460
+ expect(result.valid).toBe(false);
1461
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_approve_all'));
1462
+ });
1463
+ });
1464
+ // ── validateSpec — mcp_server_approval_timeout in defaults ────
1465
+ describe('validateSpec — mcp_server_approval_timeout in defaults', () => {
1466
+ const validSpec = {
1467
+ name: 'test-run',
1468
+ version: 1,
1469
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1470
+ };
1471
+ it('accepts valid mcp_server_approval_timeout', () => {
1472
+ const result = validateSpec({
1473
+ ...validSpec,
1474
+ defaults: { mcp_server_approval_timeout: 30 },
1475
+ });
1476
+ expect(result.valid).toBe(true);
1477
+ });
1478
+ it('rejects mcp_server_approval_timeout of 0', () => {
1479
+ const result = validateSpec({
1480
+ ...validSpec,
1481
+ defaults: { mcp_server_approval_timeout: 0 },
1482
+ });
1483
+ expect(result.valid).toBe(false);
1484
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_server_approval_timeout'));
1485
+ });
1486
+ it('rejects negative mcp_server_approval_timeout', () => {
1487
+ const result = validateSpec({
1488
+ ...validSpec,
1489
+ defaults: { mcp_server_approval_timeout: -5 },
1490
+ });
1491
+ expect(result.valid).toBe(false);
1492
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_server_approval_timeout'));
1493
+ });
1494
+ it('rejects non-number mcp_server_approval_timeout', () => {
1495
+ const result = validateSpec({
1496
+ ...validSpec,
1497
+ defaults: { mcp_server_approval_timeout: '30s' },
1498
+ });
1499
+ expect(result.valid).toBe(false);
1500
+ expect(result.errors).toContainEqual(expect.stringContaining('mcp_server_approval_timeout'));
1501
+ });
1502
+ });
1503
+ describe('validateSpec — built_in_gates config', () => {
1504
+ const validSpec = {
1505
+ name: 'test-run',
1506
+ version: 1,
1507
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1508
+ };
1509
+ it('accepts valid built_in_gates with boolean fields', () => {
1510
+ const result = validateSpec({
1511
+ ...validSpec,
1512
+ defaults: { built_in_gates: { secret_scan: true, blast_radius: false } },
1513
+ });
1514
+ expect(result.valid).toBe(true);
1515
+ });
1516
+ it('accepts built_in_gates with "auto" value', () => {
1517
+ const result = validateSpec({
1518
+ ...validSpec,
1519
+ defaults: { built_in_gates: { dependency_audit: 'auto', browser_test: 'auto' } },
1520
+ });
1521
+ expect(result.valid).toBe(true);
1522
+ });
1523
+ it('rejects built_in_gates with invalid field value', () => {
1524
+ const result = validateSpec({
1525
+ ...validSpec,
1526
+ defaults: { built_in_gates: { secret_scan: 'yes' } },
1527
+ });
1528
+ expect(result.valid).toBe(false);
1529
+ expect(result.errors).toContainEqual(expect.stringContaining('built_in_gates.secret_scan'));
1530
+ });
1531
+ it('rejects built_in_gates that is not an object', () => {
1532
+ const result = validateSpec({
1533
+ ...validSpec,
1534
+ defaults: { built_in_gates: 'enabled' },
1535
+ });
1536
+ expect(result.valid).toBe(false);
1537
+ expect(result.errors).toContainEqual(expect.stringContaining('built_in_gates'));
1538
+ });
1539
+ it('accepts valid gate_timeout', () => {
1540
+ const result = validateSpec({
1541
+ ...validSpec,
1542
+ defaults: { built_in_gates: { gate_timeout: 300 } },
1543
+ });
1544
+ expect(result.valid).toBe(true);
1545
+ });
1546
+ it('rejects gate_timeout of 0', () => {
1547
+ const result = validateSpec({
1548
+ ...validSpec,
1549
+ defaults: { built_in_gates: { gate_timeout: 0 } },
1550
+ });
1551
+ expect(result.valid).toBe(false);
1552
+ expect(result.errors).toContainEqual(expect.stringContaining('gate_timeout'));
1553
+ });
1554
+ });
1555
+ describe('validateSpec — browser_test config', () => {
1556
+ const validSpec = {
1557
+ name: 'test-run',
1558
+ version: 1,
1559
+ tasks: [{ id: 'task-1', prompt: 'Do something' }],
1560
+ };
1561
+ it('accepts valid browser_test config in defaults', () => {
1562
+ const result = validateSpec({
1563
+ ...validSpec,
1564
+ defaults: {
1565
+ browser_test: { urls: ['http://localhost:3000'] },
1566
+ },
1567
+ });
1568
+ expect(result.valid).toBe(true);
1569
+ });
1570
+ it('accepts browser_test with all optional fields', () => {
1571
+ const result = validateSpec({
1572
+ ...validSpec,
1573
+ defaults: {
1574
+ browser_test: {
1575
+ urls: ['http://localhost:3000'],
1576
+ check_console_errors: true,
1577
+ visual_diff_threshold: 0.1,
1578
+ a11y: true,
1579
+ severity_threshold: 'serious',
1580
+ },
1581
+ },
1582
+ });
1583
+ expect(result.valid).toBe(true);
1584
+ });
1585
+ it('rejects browser_test with missing urls', () => {
1586
+ const result = validateSpec({
1587
+ ...validSpec,
1588
+ defaults: { browser_test: { check_console_errors: true } },
1589
+ });
1590
+ expect(result.valid).toBe(false);
1591
+ expect(result.errors).toContainEqual(expect.stringContaining('browser_test.urls'));
1592
+ });
1593
+ it('rejects browser_test with empty urls array', () => {
1594
+ const result = validateSpec({
1595
+ ...validSpec,
1596
+ defaults: { browser_test: { urls: [] } },
1597
+ });
1598
+ expect(result.valid).toBe(false);
1599
+ expect(result.errors).toContainEqual(expect.stringContaining('browser_test.urls'));
1600
+ });
1601
+ it('rejects browser_test with non-string urls elements', () => {
1602
+ const result = validateSpec({
1603
+ ...validSpec,
1604
+ defaults: { browser_test: { urls: [42, 'http://localhost:3000'] } },
1605
+ });
1606
+ expect(result.valid).toBe(false);
1607
+ expect(result.errors).toContainEqual(expect.stringContaining('browser_test.urls'));
1608
+ });
1609
+ it('rejects browser_test with invalid severity_threshold', () => {
1610
+ const result = validateSpec({
1611
+ ...validSpec,
1612
+ defaults: { browser_test: { urls: ['http://localhost:3000'], severity_threshold: 'fatal' } },
1613
+ });
1614
+ expect(result.valid).toBe(false);
1615
+ expect(result.errors).toContainEqual(expect.stringContaining('severity_threshold'));
1616
+ });
1617
+ it('rejects browser_test that is not an object', () => {
1618
+ const result = validateSpec({
1619
+ ...validSpec,
1620
+ defaults: { browser_test: 'http://localhost:3000' },
1621
+ });
1622
+ expect(result.valid).toBe(false);
1623
+ expect(result.errors).toContainEqual(expect.stringContaining('browser_test'));
1624
+ });
1625
+ it('accepts valid task-level browser_test config', () => {
1626
+ const result = validateSpec({
1627
+ ...validSpec,
1628
+ tasks: [{
1629
+ id: 'task-1',
1630
+ prompt: 'Do something',
1631
+ browser_test: { urls: ['http://localhost:4000'] },
1632
+ }],
1633
+ });
1634
+ expect(result.valid).toBe(true);
1635
+ });
1636
+ it('rejects invalid task-level browser_test config', () => {
1637
+ const result = validateSpec({
1638
+ ...validSpec,
1639
+ tasks: [{
1640
+ id: 'task-1',
1641
+ prompt: 'Do something',
1642
+ browser_test: { urls: [] },
1643
+ }],
1644
+ });
1645
+ expect(result.valid).toBe(false);
1646
+ expect(result.errors).toContainEqual(expect.stringContaining('browser_test.urls'));
1647
+ });
1648
+ it('accepts valid task-level built_in_gates', () => {
1649
+ const result = validateSpec({
1650
+ ...validSpec,
1651
+ tasks: [{
1652
+ id: 'task-1',
1653
+ prompt: 'Do something',
1654
+ built_in_gates: { browser_test: true },
1655
+ }],
1656
+ });
1657
+ expect(result.valid).toBe(true);
1658
+ });
1659
+ it('rejects invalid task-level built_in_gates', () => {
1660
+ const result = validateSpec({
1661
+ ...validSpec,
1662
+ tasks: [{
1663
+ id: 'task-1',
1664
+ prompt: 'Do something',
1665
+ built_in_gates: { browser_test: 'enable' },
1666
+ }],
1667
+ });
1668
+ expect(result.valid).toBe(false);
1669
+ expect(result.errors).toContainEqual(expect.stringContaining('built_in_gates.browser_test'));
1670
+ });
1671
+ it('rejects browser_test with non-string baselines_dir', () => {
1672
+ const result = validateSpec({
1673
+ ...validSpec,
1674
+ defaults: { browser_test: { urls: ['http://localhost:3000'], baselines_dir: 123 } },
1675
+ });
1676
+ expect(result.valid).toBe(false);
1677
+ expect(result.errors).toContainEqual(expect.stringContaining('baselines_dir'));
1678
+ });
1679
+ });
1011
1680
  //# sourceMappingURL=schema.test.js.map