@recombine-ai/bosun 0.15.0 → 0.16.0

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 (185) hide show
  1. package/build/client/_app/immutable/assets/{0.b0jhAoSC.css → 0.FwR3B_mV.css} +1 -1
  2. package/build/client/_app/immutable/assets/0.FwR3B_mV.css.br +0 -0
  3. package/build/client/_app/immutable/assets/{0.b0jhAoSC.css.gz → 0.FwR3B_mV.css.gz} +0 -0
  4. package/build/client/_app/immutable/chunks/BD9x_mnu.js +1 -0
  5. package/build/client/_app/immutable/chunks/BD9x_mnu.js.br +0 -0
  6. package/build/client/_app/immutable/chunks/BD9x_mnu.js.gz +0 -0
  7. package/build/client/_app/immutable/chunks/{lmnC3bvv.js → Bj8jfBTi.js} +1 -1
  8. package/build/client/_app/immutable/chunks/Bj8jfBTi.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/Bj8jfBTi.js.gz +0 -0
  10. package/build/client/_app/immutable/chunks/CX0Ekq8t.js +1 -0
  11. package/build/client/_app/immutable/chunks/CX0Ekq8t.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/CX0Ekq8t.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/{ChVBNPUb.js → CXESoUel.js} +1 -1
  14. package/build/client/_app/immutable/chunks/CXESoUel.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/CXESoUel.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/{E9rdsnHb.js → DCuEIs0S.js} +1 -1
  17. package/build/client/_app/immutable/chunks/DCuEIs0S.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/DCuEIs0S.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/DgGeUDDh.js +1 -0
  20. package/build/client/_app/immutable/chunks/DgGeUDDh.js.br +0 -0
  21. package/build/client/_app/immutable/chunks/DgGeUDDh.js.gz +0 -0
  22. package/build/client/_app/immutable/chunks/_U9sB4BM.js +3 -0
  23. package/build/client/_app/immutable/chunks/_U9sB4BM.js.br +0 -0
  24. package/build/client/_app/immutable/chunks/_U9sB4BM.js.gz +0 -0
  25. package/build/client/_app/immutable/entry/{app.BY5DP2K_.js → app.Du7JgOF0.js} +2 -2
  26. package/build/client/_app/immutable/entry/app.Du7JgOF0.js.br +0 -0
  27. package/build/client/_app/immutable/entry/app.Du7JgOF0.js.gz +0 -0
  28. package/build/client/_app/immutable/entry/start.BssSvo4L.js +1 -0
  29. package/build/client/_app/immutable/entry/start.BssSvo4L.js.br +2 -0
  30. package/build/client/_app/immutable/entry/start.BssSvo4L.js.gz +0 -0
  31. package/build/client/_app/immutable/nodes/{0.Cao0LJwW.js → 0.EJrjlOP2.js} +1 -1
  32. package/build/client/_app/immutable/nodes/0.EJrjlOP2.js.br +0 -0
  33. package/build/client/_app/immutable/nodes/0.EJrjlOP2.js.gz +0 -0
  34. package/build/client/_app/immutable/nodes/{1.Kjgu6yxL.js → 1.BX3yW9Ki.js} +1 -1
  35. package/build/client/_app/immutable/nodes/1.BX3yW9Ki.js.br +0 -0
  36. package/build/client/_app/immutable/nodes/1.BX3yW9Ki.js.gz +0 -0
  37. package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js +13 -0
  38. package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js.br +0 -0
  39. package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js.gz +0 -0
  40. package/build/client/_app/immutable/nodes/{17.COHkD98p.js → 17.DQQN2LTu.js} +3 -3
  41. package/build/client/_app/immutable/nodes/17.DQQN2LTu.js.br +0 -0
  42. package/build/client/_app/immutable/nodes/17.DQQN2LTu.js.gz +0 -0
  43. package/build/client/_app/immutable/nodes/{18.DQVit7Md.js → 18.Dx1VeGYs.js} +1 -1
  44. package/build/client/_app/immutable/nodes/18.Dx1VeGYs.js.br +0 -0
  45. package/build/client/_app/immutable/nodes/18.Dx1VeGYs.js.gz +0 -0
  46. package/build/client/_app/immutable/nodes/{2.DVwYttKv.js → 2.BSZUitGm.js} +1 -1
  47. package/build/client/_app/immutable/nodes/2.BSZUitGm.js.br +0 -0
  48. package/build/client/_app/immutable/nodes/{2.DVwYttKv.js.gz → 2.BSZUitGm.js.gz} +0 -0
  49. package/build/client/_app/immutable/nodes/{3.BhmyTb3R.js → 3.D2P9M-PP.js} +1 -1
  50. package/build/client/_app/immutable/nodes/3.D2P9M-PP.js.br +0 -0
  51. package/build/client/_app/immutable/nodes/3.D2P9M-PP.js.gz +0 -0
  52. package/build/client/_app/immutable/nodes/{4.CxATX0ci.js → 4.Cx97sxk5.js} +1 -1
  53. package/build/client/_app/immutable/nodes/4.Cx97sxk5.js.br +0 -0
  54. package/build/client/_app/immutable/nodes/4.Cx97sxk5.js.gz +0 -0
  55. package/build/client/_app/immutable/nodes/{5.Dal1qxB1.js → 5.BAliV6Wc.js} +1 -1
  56. package/build/client/_app/immutable/nodes/5.BAliV6Wc.js.br +0 -0
  57. package/build/client/_app/immutable/nodes/5.BAliV6Wc.js.gz +0 -0
  58. package/build/client/_app/immutable/nodes/{6.iAOxBhJw.js → 6.CmUnus-n.js} +1 -1
  59. package/build/client/_app/immutable/nodes/6.CmUnus-n.js.br +0 -0
  60. package/build/client/_app/immutable/nodes/6.CmUnus-n.js.gz +0 -0
  61. package/build/client/_app/immutable/nodes/{8.DFhKu3dp.js → 8.DLYtZXzs.js} +1 -1
  62. package/build/client/_app/immutable/nodes/8.DLYtZXzs.js.br +0 -0
  63. package/build/client/_app/immutable/nodes/8.DLYtZXzs.js.gz +0 -0
  64. package/build/client/_app/immutable/nodes/{9.DRIEv5m9.js → 9.DQ0jyQBU.js} +1 -1
  65. package/build/client/_app/immutable/nodes/9.DQ0jyQBU.js.br +0 -0
  66. package/build/client/_app/immutable/nodes/9.DQ0jyQBU.js.gz +0 -0
  67. package/build/client/_app/version.json +1 -1
  68. package/build/client/_app/version.json.br +0 -0
  69. package/build/client/_app/version.json.gz +0 -0
  70. package/build/server/chunks/{0-CIzAMtA3.js → 0-DB4bFp_f.js} +3 -3
  71. package/build/server/chunks/{0-CIzAMtA3.js.map → 0-DB4bFp_f.js.map} +1 -1
  72. package/build/server/chunks/{1-D1heuNbf.js → 1-kigiZghR.js} +2 -2
  73. package/build/server/chunks/{1-D1heuNbf.js.map → 1-kigiZghR.js.map} +1 -1
  74. package/build/server/chunks/{10-CjLtjXyZ.js → 10-C49_L5kn.js} +3 -3
  75. package/build/server/chunks/10-C49_L5kn.js.map +1 -0
  76. package/build/server/chunks/{17-DklEmmOx.js → 17-Dbb76ckK.js} +3 -3
  77. package/build/server/chunks/17-Dbb76ckK.js.map +1 -0
  78. package/build/server/chunks/18-CvqluQBN.js +9 -0
  79. package/build/server/chunks/18-CvqluQBN.js.map +1 -0
  80. package/build/server/chunks/{2-DCrAVYXs.js → 2-JGarRt-z.js} +4 -4
  81. package/build/server/chunks/2-JGarRt-z.js.map +1 -0
  82. package/build/server/chunks/{3-BpTqa4DU.js → 3-CnXV_OO3.js} +2 -2
  83. package/build/server/chunks/{3-BpTqa4DU.js.map → 3-CnXV_OO3.js.map} +1 -1
  84. package/build/server/chunks/{4-B7GjwQzI.js → 4-B-XLuotH.js} +2 -2
  85. package/build/server/chunks/{4-B7GjwQzI.js.map → 4-B-XLuotH.js.map} +1 -1
  86. package/build/server/chunks/{5-XjS__bcp.js → 5-Cumig7Am.js} +2 -2
  87. package/build/server/chunks/{5-XjS__bcp.js.map → 5-Cumig7Am.js.map} +1 -1
  88. package/build/server/chunks/{6-DWkWptmB.js → 6-Ez7v6PpX.js} +2 -2
  89. package/build/server/chunks/{6-DWkWptmB.js.map → 6-Ez7v6PpX.js.map} +1 -1
  90. package/build/server/chunks/{8-_swAx6gL.js → 8-COGeKy3N.js} +3 -3
  91. package/build/server/chunks/{8-_swAx6gL.js.map → 8-COGeKy3N.js.map} +1 -1
  92. package/build/server/chunks/9-DlMHnRdZ.js +9 -0
  93. package/build/server/chunks/9-DlMHnRdZ.js.map +1 -0
  94. package/build/server/chunks/Panel-D0IY1AzO.js +11 -0
  95. package/build/server/chunks/Panel-D0IY1AzO.js.map +1 -0
  96. package/build/server/chunks/{_layout.svelte-PtdpfgwJ.js → _layout.svelte-Blfto3jh.js} +82 -12
  97. package/build/server/chunks/_layout.svelte-Blfto3jh.js.map +1 -0
  98. package/build/server/chunks/{_page.svelte-Cu8a_kUO.js → _page.svelte-B1erTzoN.js} +3 -2
  99. package/build/server/chunks/_page.svelte-B1erTzoN.js.map +1 -0
  100. package/build/server/chunks/{_page.svelte-BTKQMuSz.js → _page.svelte-DhhqK6z3.js} +12 -85
  101. package/build/server/chunks/_page.svelte-DhhqK6z3.js.map +1 -0
  102. package/build/server/chunks/{_page.svelte-C3gizoSD.js → _page.svelte-xf-0yXQM.js} +3 -2
  103. package/build/server/chunks/_page.svelte-xf-0yXQM.js.map +1 -0
  104. package/build/server/chunks/edit-workspace-context-biaZqjOf.js +6 -0
  105. package/build/server/chunks/edit-workspace-context-biaZqjOf.js.map +1 -0
  106. package/build/server/index.js +1 -1
  107. package/build/server/index.js.map +1 -1
  108. package/build/server/manifest.js +13 -13
  109. package/build/server/manifest.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/backend/lib/project-api.ts +6 -0
  112. package/src/backend/roleplay/agents.ts +1 -1
  113. package/src/backend/tests/academy.ts +35 -39
  114. package/src/backend/tests/context-utils.ts +0 -12
  115. package/src/backend/tests/data.spec.ts +21 -14
  116. package/src/backend/tests/data.ts +21 -124
  117. package/src/backend/tests/pii-sanitized-test-drafts.spec.ts +6 -5
  118. package/src/backend/tests/pii-sanitized-test-drafts.ts +2 -2
  119. package/src/backend/tests/router.ts +26 -99
  120. package/src/backend/tests/suite.spec.ts +82 -34
  121. package/src/backend/tests/suite.ts +145 -84
  122. package/build/client/_app/immutable/assets/0.b0jhAoSC.css.br +0 -0
  123. package/build/client/_app/immutable/chunks/BTX1tKMW.js +0 -1
  124. package/build/client/_app/immutable/chunks/BTX1tKMW.js.br +0 -0
  125. package/build/client/_app/immutable/chunks/BTX1tKMW.js.gz +0 -0
  126. package/build/client/_app/immutable/chunks/BVxLHCs8.js +0 -1
  127. package/build/client/_app/immutable/chunks/BVxLHCs8.js.br +0 -0
  128. package/build/client/_app/immutable/chunks/BVxLHCs8.js.gz +0 -0
  129. package/build/client/_app/immutable/chunks/ChVBNPUb.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/ChVBNPUb.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/E9rdsnHb.js.br +0 -0
  132. package/build/client/_app/immutable/chunks/E9rdsnHb.js.gz +0 -0
  133. package/build/client/_app/immutable/chunks/Z1_c4us_.js +0 -3
  134. package/build/client/_app/immutable/chunks/Z1_c4us_.js.br +0 -0
  135. package/build/client/_app/immutable/chunks/Z1_c4us_.js.gz +0 -0
  136. package/build/client/_app/immutable/chunks/lmnC3bvv.js.br +0 -0
  137. package/build/client/_app/immutable/chunks/lmnC3bvv.js.gz +0 -0
  138. package/build/client/_app/immutable/entry/app.BY5DP2K_.js.br +0 -0
  139. package/build/client/_app/immutable/entry/app.BY5DP2K_.js.gz +0 -0
  140. package/build/client/_app/immutable/entry/start.BeIIwvvV.js +0 -1
  141. package/build/client/_app/immutable/entry/start.BeIIwvvV.js.br +0 -0
  142. package/build/client/_app/immutable/entry/start.BeIIwvvV.js.gz +0 -0
  143. package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.br +0 -0
  144. package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.gz +0 -0
  145. package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.br +0 -0
  146. package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.gz +0 -0
  147. package/build/client/_app/immutable/nodes/10.yW2mHq85.js +0 -12
  148. package/build/client/_app/immutable/nodes/10.yW2mHq85.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/10.yW2mHq85.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/17.COHkD98p.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/17.COHkD98p.js.gz +0 -0
  152. package/build/client/_app/immutable/nodes/18.DQVit7Md.js.br +0 -0
  153. package/build/client/_app/immutable/nodes/18.DQVit7Md.js.gz +0 -0
  154. package/build/client/_app/immutable/nodes/2.DVwYttKv.js.br +0 -0
  155. package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.br +0 -0
  156. package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/4.CxATX0ci.js.br +0 -0
  158. package/build/client/_app/immutable/nodes/4.CxATX0ci.js.gz +0 -0
  159. package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.br +0 -0
  160. package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.gz +0 -0
  161. package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.br +0 -0
  162. package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.gz +0 -0
  163. package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.br +0 -0
  164. package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.gz +0 -0
  165. package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.br +0 -0
  166. package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.gz +0 -0
  167. package/build/server/chunks/10-CjLtjXyZ.js.map +0 -1
  168. package/build/server/chunks/17-DklEmmOx.js.map +0 -1
  169. package/build/server/chunks/18-BJS-eRcz.js +0 -9
  170. package/build/server/chunks/18-BJS-eRcz.js.map +0 -1
  171. package/build/server/chunks/2-DCrAVYXs.js.map +0 -1
  172. package/build/server/chunks/9-NEe24O4u.js +0 -9
  173. package/build/server/chunks/9-NEe24O4u.js.map +0 -1
  174. package/build/server/chunks/ContextEditor-ngRBOJVc.js +0 -89
  175. package/build/server/chunks/ContextEditor-ngRBOJVc.js.map +0 -1
  176. package/build/server/chunks/_layout.svelte-PtdpfgwJ.js.map +0 -1
  177. package/build/server/chunks/_page.svelte-BTKQMuSz.js.map +0 -1
  178. package/build/server/chunks/_page.svelte-C3gizoSD.js.map +0 -1
  179. package/build/server/chunks/_page.svelte-Cu8a_kUO.js.map +0 -1
  180. package/build/server/chunks/edit-workspace-context-BEioXKWB.js +0 -12
  181. package/build/server/chunks/edit-workspace-context-BEioXKWB.js.map +0 -1
  182. package/src/backend/tests/config.ts +0 -30
  183. /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css → Popper.CLTyHc88.css} +0 -0
  184. /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css.br → Popper.CLTyHc88.css.br} +0 -0
  185. /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css.gz → Popper.CLTyHc88.css.gz} +0 -0
@@ -1,39 +1,27 @@
1
- import { tryReadConfig, writeConfig } from './config'
2
- import { access, readdir, readFile, stat, writeFile } from 'node:fs/promises'
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import {
5
- getSuiteFilePath,
6
5
  readSuite,
7
6
  type TestSuite,
8
- upsertSuiteMeta,
9
7
  withSuiteMutationLock,
10
8
  writeSuite,
11
9
  type TestRow as SuiteRow
12
10
  } from './suite'
13
11
  import { getRootPath } from '../lib/get-root-path'
14
- import z from 'zod'
15
12
  import type {
16
13
  ResultEntry,
17
14
  WrittenRunReport,
18
15
  WrittenRunReportConfig,
19
16
  WrittenRunResult
20
17
  } from './academy'
21
- import { makeContext, makeContextDiff } from './context-utils'
18
+ import { makeContext } from './context-utils'
22
19
  import { listAgentReportFiles, readReportFile, reportTimestamp } from './report-files'
23
20
 
24
21
  export type CsvRow = Record<string, string>
25
22
 
26
23
  export type TestRow = SuiteRow
27
24
 
28
- export const RunConfigSchema = z.object({
29
- repeats: z.number().optional(),
30
- concurrency: z.number().optional(),
31
- defaultContext: z.record(z.unknown()).optional(),
32
- expandedTestIds: z.array(z.string()).optional()
33
- })
34
-
35
- export type RunConfig = z.infer<typeof RunConfigSchema>
36
-
37
25
  export interface RunResult {
38
26
  success: boolean
39
27
  result: string
@@ -67,6 +55,7 @@ export type LatestRunResultSummary = Pick<RunResult, 'success' | 'result' | 'eva
67
55
  export interface TestTable {
68
56
  rows: TestRow[]
69
57
  headers: string[]
58
+ settingsKey: string
70
59
  }
71
60
 
72
61
  export interface TestRunHistorySummary {
@@ -104,6 +93,7 @@ export interface TestRunHistoryDetail {
104
93
  }
105
94
 
106
95
  const defaultTestRow: Omit<TestRow, 'id'> = {
96
+ order: 0,
107
97
  pinned: 'FALSE',
108
98
  scenario: 'New scenario',
109
99
  author: '',
@@ -130,7 +120,7 @@ export async function loadTests(agent: string): Promise<TestTable> {
130
120
  let id = String(r.id || '').trim()
131
121
  if (!id || seenIds.has(id)) {
132
122
  changed = true
133
- id = globalThis.crypto?.randomUUID?.() || randomUUID()
123
+ id = randomUUID()
134
124
  }
135
125
  seenIds.add(id)
136
126
  // previously tests were storing context as a string...
@@ -159,7 +149,9 @@ export async function loadTests(agent: string): Promise<TestTable> {
159
149
  'context',
160
150
  'mocks'
161
151
  ]
162
- return { rows, headers }
152
+ const projectPath = path.dirname(await getRootPath())
153
+ const workspaceId = createHash('sha256').update(projectPath).digest('hex').slice(0, 16)
154
+ return { rows, headers, settingsKey: `bosun:test-settings:${workspaceId}:${agent}` }
163
155
  })
164
156
  }
165
157
 
@@ -218,71 +210,32 @@ export async function patchTestsBatch(
218
210
  }
219
211
 
220
212
  // Add a new row at the top
221
- export async function addTest(
222
- agent: string,
223
- mergeContexts: boolean,
224
- seed?: Partial<TestRow>
225
- ): Promise<TestRow> {
213
+ export async function addTest(agent: string, seed?: Partial<TestRow>): Promise<TestRow> {
226
214
  return withSuiteMutationLock(agent, async () => {
227
- await ensureConfigExists(agent)
228
215
  const suite = await loadSuiteOrEmpty(agent)
229
- const id = globalThis.crypto?.randomUUID?.() || randomUUID()
216
+ const id = randomUUID()
217
+ const firstOrder = suite.tests.reduce(
218
+ (minimum, row) => Math.min(minimum, row.order),
219
+ Number.POSITIVE_INFINITY
220
+ )
230
221
 
231
222
  const newRow: TestRow = {
232
223
  ...defaultTestRow,
233
224
  id,
225
+ order: Number.isFinite(firstOrder) ? firstOrder - 1024 : 0,
234
226
  ...seed,
235
- context: mergeContexts
236
- ? makeContextDiff(seed?.context, suite.meta?.defaultContext)
237
- : makeContext(seed?.context)
227
+ context: makeContext(seed?.context)
238
228
  }
239
229
  if (typeof seed?.mocks !== 'undefined') {
240
230
  newRow.mocks = makeContext(seed.mocks)
241
231
  }
242
- await writeSuite(agent, { ...(suite as any), tests: [newRow, ...suite.tests] })
243
- await ensureTestExpandedByDefault(agent, newRow.id)
232
+ await writeSuite(agent, { tests: [newRow, ...suite.tests] })
244
233
  return newRow
245
234
  })
246
235
  }
247
236
 
248
- async function ensureTestExpandedByDefault(agent: string, id: string): Promise<void> {
249
- const normalizedId = String(id ?? '').trim()
250
- if (!normalizedId) return
251
-
252
- const cfg = (await tryReadConfig(agent)) || {}
253
- const existingIds = Array.isArray((cfg as any).expandedTestIds)
254
- ? (cfg as any).expandedTestIds
255
- .map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
256
- .filter((v: string) => v.length > 0)
257
- : []
258
-
259
- if (existingIds.includes(normalizedId)) return
260
- await writeConfig(agent, { ...(cfg as any), expandedTestIds: [...existingIds, normalizedId] })
261
- }
262
-
263
- async function ensureConfigExists(agent: string): Promise<void> {
264
- const existing = await tryReadConfig(agent)
265
- if (existing) return
266
- await writeConfig(agent, { repeats: 1, concurrency: 75, expandedTestIds: [] })
267
- }
268
-
269
237
  async function loadSuiteOrEmpty(agent: string): Promise<TestSuite> {
270
- const jsonlPath = await getSuiteFilePath(agent)
271
- const legacyPath = jsonlPath.replace(/\.jsonl$/, '.json')
272
-
273
- const exists = async (p: string) => {
274
- try {
275
- await access(p)
276
- return true
277
- } catch {
278
- return false
279
- }
280
- }
281
-
282
- if (await exists(jsonlPath)) return readSuite(agent)
283
- if (await exists(legacyPath)) return readSuite(agent)
284
-
285
- return { version: 2, meta: {}, tests: [] }
238
+ return readSuite(agent)
286
239
  }
287
240
 
288
241
  export async function deleteTest(agent: string, id: string): Promise<boolean> {
@@ -303,63 +256,16 @@ export async function reorderTests(agent: string, order: string[]): Promise<void
303
256
  for (const id of order) {
304
257
  const r = map.get(id)
305
258
  if (r) {
306
- next.push(r)
259
+ next.push({ ...r, order: next.length * 1024 })
307
260
  map.delete(id)
308
261
  }
309
262
  }
310
263
  // Append any rows that were not included in order (safety)
311
- for (const r of map.values()) next.push(r)
264
+ for (const r of map.values()) next.push({ ...r, order: next.length * 1024 })
312
265
  await writeSuite(agent, { ...suite, tests: next })
313
266
  })
314
267
  }
315
268
 
316
- export async function loadConfig(agent: string): Promise<RunConfig> {
317
- const cfg = (await tryReadConfig(agent)) || {}
318
- // Always source defaultContext from the suite meta, not config
319
- let defaultContext: Record<string, unknown> = {}
320
- try {
321
- const suite = await readSuite(agent)
322
- defaultContext = (suite.meta?.defaultContext as any) || {}
323
- } catch {
324
- // ignore
325
- }
326
-
327
- const expandedTestIds = Array.isArray((cfg as any).expandedTestIds)
328
- ? (cfg as any).expandedTestIds.filter((id: unknown) => typeof id === 'string')
329
- : []
330
- return {
331
- repeats: typeof (cfg as any).repeats === 'number' ? (cfg as any).repeats : 1,
332
- concurrency: typeof (cfg as any).concurrency === 'number' ? (cfg as any).concurrency : 75,
333
- defaultContext,
334
- expandedTestIds
335
- }
336
- }
337
-
338
- export async function saveConfig(agent: string, cfg: RunConfig): Promise<void> {
339
- // Merge patch into existing config
340
- const current = (await tryReadConfig(agent)) || {}
341
- const expandedTestIds = Array.isArray(cfg.expandedTestIds)
342
- ? Array.from(
343
- new Set(
344
- cfg.expandedTestIds.map((id) => String(id || '').trim()).filter((id) => id.length > 0)
345
- )
346
- )
347
- : Array.isArray((current as any).expandedTestIds)
348
- ? (current as any).expandedTestIds
349
- : undefined
350
- const next = {
351
- repeats: typeof cfg.repeats === 'number' ? cfg.repeats : (current as any).repeats,
352
- concurrency:
353
- typeof cfg.concurrency === 'number' ? cfg.concurrency : (current as any).concurrency,
354
- expandedTestIds
355
- }
356
- await writeConfig(agent, next)
357
- // Persist defaultContext exclusively in the suite meta (do not write to config)
358
- if (cfg.defaultContext) {
359
- await upsertSuiteMeta(agent, { defaultContext: cfg.defaultContext })
360
- }
361
- }
362
-
363
269
  // We no longer migrate legacy reports automatically. Please update old reports manually.
364
270
 
365
271
  type ReportFileState = {
@@ -750,15 +656,6 @@ export async function getRunHistory(
750
656
  }
751
657
  }
752
658
 
753
- // Fallback UUID for Node environments without global crypto
754
- function randomUUID() {
755
- const rnd = () =>
756
- Math.floor(Math.random() * 0xffffffff)
757
- .toString(16)
758
- .padStart(8, '0')
759
- return `${rnd()}-${rnd().slice(0, 4)}-${rnd().slice(0, 4)}-${rnd().slice(0, 4)}-${rnd()}${rnd()}`
760
- }
761
-
762
659
  async function getRun(agent: string, ts?: string) {
763
660
  const projectRoot = path.dirname(await getRootPath())
764
661
  const outDir = path.join(projectRoot, 'data', 'test_runs')
@@ -41,6 +41,7 @@ function sanitized(input: TraceTestSeed, replacement = 'Maya Turner'): PIISaniti
41
41
  function testRow(input: TraceTestSeed): TestRow {
42
42
  return {
43
43
  id: 'created-test',
44
+ order: 0,
44
45
  scenario: '',
45
46
  author: '',
46
47
  category: '',
@@ -51,7 +52,7 @@ function testRow(input: TraceTestSeed): TestRow {
51
52
  describe('PII-sanitised test drafts', () => {
52
53
  it('persists an unchanged server-created draft exactly once', async () => {
53
54
  const sanitize = vi.fn(async (input: TraceTestSeed) => sanitized(input))
54
- const persist = vi.fn(async (_suiteKey, _mergeContexts, input: TraceTestSeed) => testRow(input))
55
+ const persist = vi.fn(async (_suiteKey, input: TraceTestSeed) => testRow(input))
55
56
  const draft = await createPIISanitizedTestDraft(
56
57
  { suiteKey: 'agent.en-gb', seed: seed() },
57
58
  { sanitize, now: () => 1_000 }
@@ -73,12 +74,12 @@ describe('PII-sanitised test drafts', () => {
73
74
  expect(result.status).toBe('created')
74
75
  expect(sanitize).toHaveBeenCalledTimes(1)
75
76
  expect(persist).toHaveBeenCalledOnce()
76
- expect(JSON.stringify(persist.mock.calls[0][2])).not.toContain('Sarah')
77
+ expect(JSON.stringify(persist.mock.calls[0][1])).not.toContain('Sarah')
77
78
  })
78
79
 
79
80
  it('persists reviewed edits immediately without sanitizing them again', async () => {
80
81
  const sanitize = vi.fn(async (input: TraceTestSeed) => sanitized(input))
81
- const persist = vi.fn(async (_suiteKey, _mergeContexts, input: TraceTestSeed) => testRow(input))
82
+ const persist = vi.fn(async (_suiteKey, input: TraceTestSeed) => testRow(input))
82
83
  const draft = await createPIISanitizedTestDraft(
83
84
  { suiteKey: 'agent', seed: seed() },
84
85
  { sanitize, now: () => 1_000 }
@@ -100,7 +101,7 @@ describe('PII-sanitised test drafts', () => {
100
101
  expect(result.status).toBe('created')
101
102
  expect(sanitize).toHaveBeenCalledOnce()
102
103
  expect(persist).toHaveBeenCalledOnce()
103
- expect(persist.mock.calls[0][2].conversation).toBe('User: I am Sarah Khan.')
104
+ expect(persist.mock.calls[0][1].conversation).toBe('User: I am Sarah Khan.')
104
105
  })
105
106
 
106
107
  it('rejects a draft used for a different suite', async () => {
@@ -128,7 +129,7 @@ describe('PII-sanitised test drafts', () => {
128
129
  it('allows only one create operation to consume a draft', async () => {
129
130
  let finishPersist!: (row: TestRow) => void
130
131
  const persist = vi.fn(
131
- async (_suiteKey, _mergeContexts, _input: TraceTestSeed) =>
132
+ async (_suiteKey, _input: TraceTestSeed) =>
132
133
  new Promise<TestRow>((resolve) => {
133
134
  finishPersist = resolve
134
135
  })
@@ -26,7 +26,7 @@ type StoredDraft = {
26
26
  const drafts = new Map<string, StoredDraft>()
27
27
 
28
28
  type Sanitize = (seed: TraceTestSeed) => Promise<PIISanitizationResult>
29
- type Persist = (suiteKey: string, mergeContexts: boolean, seed: TraceTestSeed) => Promise<TestRow>
29
+ type Persist = (suiteKey: string, seed: TraceTestSeed) => Promise<TestRow>
30
30
 
31
31
  export async function createPIISanitizedTestDraft(
32
32
  input: { suiteKey: string; seed: TraceTestSeed },
@@ -76,7 +76,7 @@ export async function createTestFromPIISanitizedTestDraft(
76
76
  })
77
77
 
78
78
  const persist = dependencies.persist ?? addTest
79
- const test = await persist(stored.suiteKey, false, reviewedSeed)
79
+ const test = await persist(stored.suiteKey, reviewedSeed)
80
80
  drafts.delete(input.draftId)
81
81
  return { status: 'created', test }
82
82
  } catch (error) {
@@ -1,7 +1,5 @@
1
1
  import type { TRPCRootObject } from '@trpc/server/unstable-core-do-not-import'
2
2
  import { z } from 'zod'
3
- import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
4
- import path from 'node:path'
5
3
 
6
4
  import {
7
5
  addTest,
@@ -9,22 +7,17 @@ import {
9
7
  getResultEntry,
10
8
  getRunHistory,
11
9
  listRunHistory,
12
- loadConfig,
13
10
  loadLatestResultsByTest,
14
11
  loadTests,
15
12
  patchTest,
16
13
  patchTestsBatch,
17
- reorderTests,
18
- RunConfigSchema,
19
- saveConfig
14
+ reorderTests
20
15
  } from './data'
21
16
  import { readSuite, TestRowSchema } from './suite'
22
17
  import { cancelPromptTests, getPromptTestsProgress, runPromptTestsManaged } from './academy'
23
18
  import { analyzeRunQuotes } from './analyze'
24
19
  import { findPromptIssues } from './prompt-issues'
25
20
  import type { TrpcContext } from '../lib/trpc-context'
26
- import { getRootPath } from '../lib/get-root-path'
27
- import { type BosunAgentMeta } from '../lib/get-agent-factories'
28
21
  import { getProjectBosunApi } from '../lib/project-api'
29
22
  import {
30
23
  TraceTestReviewContentSchema,
@@ -35,68 +28,6 @@ import {
35
28
  createTestFromPIISanitizedTestDraft
36
29
  } from './pii-sanitized-test-drafts'
37
30
 
38
- async function fileExists(p: string): Promise<boolean> {
39
- try {
40
- await access(p)
41
- return true
42
- } catch {
43
- return false
44
- }
45
- }
46
-
47
- async function suiteOrConfigExists(baseDir: string, suiteKey: string): Promise<boolean> {
48
- const jsonl = path.resolve(baseDir, `${suiteKey}.tests.jsonl`)
49
- const legacyJson = path.resolve(baseDir, `${suiteKey}.tests.json`)
50
- const cfg = path.resolve(baseDir, `${suiteKey}.config.json`)
51
- return (await fileExists(jsonl)) || (await fileExists(legacyJson)) || (await fileExists(cfg))
52
- }
53
-
54
- async function migrateLegacySuiteToDefaultRegion(
55
- baseDir: string,
56
- agent: string,
57
- meta: BosunAgentMeta | null
58
- ): Promise<void> {
59
- if (!meta || meta.availableRegions.length === 0) return
60
-
61
- const defaultRegion =
62
- meta.defaultRegion && meta.availableRegions.includes(meta.defaultRegion)
63
- ? meta.defaultRegion
64
- : null
65
- if (!defaultRegion) return
66
-
67
- const defaultSuiteKey = `${agent}.${defaultRegion}`
68
- if (await suiteOrConfigExists(baseDir, defaultSuiteKey)) return
69
- if (!(await suiteOrConfigExists(baseDir, agent))) return
70
-
71
- // Non-trivial: one-time migration so the first region-aware run does NOT silently fall back to the
72
- // legacy suite/config (which would incorrectly show the same tests for every region).
73
- const legacyJsonl = path.resolve(baseDir, `${agent}.tests.jsonl`)
74
- const legacyJson = path.resolve(baseDir, `${agent}.tests.json`)
75
- const legacyCfg = path.resolve(baseDir, `${agent}.config.json`)
76
-
77
- const targetJsonl = path.resolve(baseDir, `${defaultSuiteKey}.tests.jsonl`)
78
- const targetCfg = path.resolve(baseDir, `${defaultSuiteKey}.config.json`)
79
-
80
- await mkdir(baseDir, { recursive: true })
81
-
82
- if (!(await fileExists(targetJsonl)) && (await fileExists(legacyJsonl))) {
83
- await copyFile(legacyJsonl, targetJsonl)
84
- } else if (!(await fileExists(targetJsonl)) && (await fileExists(legacyJson))) {
85
- // Convert legacy JSON suite to JSONL so the new file matches the expected layout.
86
- const raw = await readFile(legacyJson, 'utf8')
87
- const parsed = JSON.parse(raw)
88
- const metaObj = parsed?.meta && typeof parsed.meta === 'object' ? parsed.meta : {}
89
- const tests = Array.isArray(parsed?.tests) ? parsed.tests : []
90
- const header = { version: 1, meta: metaObj }
91
- const lines = [JSON.stringify(header), ...tests.map((t: any) => JSON.stringify(t))]
92
- await writeFile(targetJsonl, lines.join('\n') + '\n', 'utf8')
93
- }
94
-
95
- if (!(await fileExists(targetCfg)) && (await fileExists(legacyCfg))) {
96
- await copyFile(legacyCfg, targetCfg)
97
- }
98
- }
99
-
100
31
  type SuiteSelection = {
101
32
  suiteKey: string
102
33
  agentId: string
@@ -112,16 +43,11 @@ async function resolveSuiteSelection(
112
43
  // - `suiteKey` is the filesystem/report key:
113
44
  // - non-regional projects: `${agentId}`
114
45
  // - regional projects: `${agentId}.${region}`
115
- // We never substitute legacy tests for the wrong region.
116
- const baseDir = path.dirname(await getRootPath())
117
-
118
46
  const bosunApi = await getProjectBosunApi()
119
47
  const meta = bosunApi ? await bosunApi.getAgentMeta(agentId) : null
120
48
  const isRegionScoped = meta?.testScope === 'region'
121
49
  if (!isRegionScoped) return { suiteKey: agentId, agentId }
122
50
 
123
- await migrateLegacySuiteToDefaultRegion(baseDir, agentId, meta)
124
-
125
51
  if (!bosunApi) {
126
52
  throw new Error(`Project does not export bosunApi, cannot resolve suite for '${agentId}'.`)
127
53
  }
@@ -142,18 +68,6 @@ export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
142
68
  const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
143
69
  return loadTests(suiteKey)
144
70
  }),
145
- config: t.procedure
146
- .input(z.object({ agent: z.string(), region: z.string().nullish() }))
147
- .query(async ({ input }) => {
148
- const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
149
- return loadConfig(suiteKey)
150
- }),
151
- updateConfig: t.procedure
152
- .input(z.object({ agent: z.string(), region: z.string().nullish(), cfg: RunConfigSchema }))
153
- .mutation(async ({ input }) => {
154
- const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
155
- return saveConfig(suiteKey, input.cfg)
156
- }),
157
71
  latestResult: t.procedure
158
72
  .input(z.object({ agent: z.string(), region: z.string().nullish() }))
159
73
  .query(async ({ input }) => {
@@ -247,13 +161,12 @@ export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
247
161
  z.object({
248
162
  agent: z.string(),
249
163
  region: z.string().nullish(),
250
- mergeContexts: z.boolean(),
251
- seed: TestRowSchema.partial()
164
+ seed: TestRowSchema.omit({ id: true, order: true }).partial()
252
165
  })
253
166
  )
254
167
  .mutation(async ({ input }) => {
255
168
  const { suiteKey } = await resolveSuiteSelection(input.agent, input.region)
256
- return addTest(suiteKey, input.mergeContexts, input.seed)
169
+ return addTest(suiteKey, input.seed)
257
170
  }),
258
171
  createPIISanitizedTestDraft: t.procedure
259
172
  .input(
@@ -290,7 +203,7 @@ export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
290
203
  agent: z.string(),
291
204
  region: z.string().nullish(),
292
205
  id: z.string(),
293
- patch: TestRowSchema.partial()
206
+ patch: TestRowSchema.omit({ id: true, order: true }).partial()
294
207
  })
295
208
  )
296
209
  .mutation(async ({ input }) => {
@@ -302,7 +215,12 @@ export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
302
215
  z.object({
303
216
  agent: z.string(),
304
217
  region: z.string().nullish(),
305
- updates: z.array(z.object({ id: z.string(), patch: TestRowSchema.partial() }))
218
+ updates: z.array(
219
+ z.object({
220
+ id: z.string(),
221
+ patch: TestRowSchema.omit({ id: true, order: true }).partial()
222
+ })
223
+ )
306
224
  })
307
225
  )
308
226
  .mutation(async ({ input }) => {
@@ -331,27 +249,36 @@ export function createTestsRouter(t: TRPCRootObject<TrpcContext, any, any>) {
331
249
  agent: z.string(),
332
250
  region: z.string().nullish(),
333
251
  step: z.string(),
334
- scope: z.enum(['pinned', 'all']).optional()
252
+ scope: z.enum(['pinned', 'all']).optional(),
253
+ repeats: z.number().int().min(1).max(500),
254
+ concurrency: z.number().int().min(1).max(75)
335
255
  })
336
256
  .strict(),
337
257
  z
338
258
  .object({
339
259
  agent: z.string(),
340
260
  region: z.string().nullish(),
341
- testId: z.string()
261
+ testId: z.string(),
262
+ repeats: z.number().int().min(1).max(500),
263
+ concurrency: z.number().int().min(1).max(75)
342
264
  })
343
265
  .strict()
344
266
  ])
345
267
  )
346
268
  .mutation(async ({ input }) => {
347
269
  const selection = await resolveSuiteSelection(input.agent, input.region)
270
+ const runConfig = { repeats: input.repeats, concurrency: input.concurrency }
348
271
  if ('testId' in input) {
349
- return runPromptTestsManaged(selection, { testId: input.testId })
272
+ return runPromptTestsManaged(selection, { testId: input.testId }, runConfig)
350
273
  }
351
- return runPromptTestsManaged(selection, {
352
- scope: input.scope ?? 'pinned',
353
- step: input.step
354
- })
274
+ return runPromptTestsManaged(
275
+ selection,
276
+ {
277
+ scope: input.scope ?? 'pinned',
278
+ step: input.step
279
+ },
280
+ runConfig
281
+ )
355
282
  }),
356
283
  cancelRun: t.procedure
357
284
  .input(z.object({ agent: z.string(), region: z.string().nullish() }))
@@ -1,13 +1,14 @@
1
- import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
1
+ import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { afterEach, describe, expect, it, vi } from 'vitest'
5
5
 
6
- const root = vi.hoisted(() => ({ path: '' }))
6
+ const project = vi.hoisted(() => ({ basePath: '', rootPath: '' }))
7
7
 
8
- vi.mock('../lib/get-root-path', () => ({ getRootPath: async () => root.path }))
8
+ vi.mock('../lib/project-api', () => ({ getProjectBasePath: async () => project.basePath }))
9
+ vi.mock('../lib/get-root-path', () => ({ getRootPath: async () => project.rootPath }))
9
10
 
10
- import { readSuite } from './suite'
11
+ import { LEGACY_TEST_SUITE_ERROR_PREFIX, readSuite, writeSuite, type TestRow } from './suite'
11
12
 
12
13
  const temporaryDirectories: string[] = []
13
14
 
@@ -15,40 +16,87 @@ afterEach(async () => {
15
16
  await Promise.all(temporaryDirectories.splice(0).map((dir) => rm(dir, { recursive: true })))
16
17
  })
17
18
 
18
- describe('test suite migration', () => {
19
- it('moves every version-one test to main and removes final capture mode', async () => {
20
- const dir = await mkdtemp(path.join(os.tmpdir(), 'bosun-suite-migration-'))
21
- temporaryDirectories.push(dir)
22
- root.path = path.join(dir, 'index.ts')
23
- const suitePath = path.join(dir, 'agent.tests.jsonl')
19
+ function row(id: string, order: number): TestRow {
20
+ return {
21
+ id,
22
+ order,
23
+ pinned: 'TRUE',
24
+ scenario: id,
25
+ step: 'main',
26
+ author: '',
27
+ category: '',
28
+ assertion_type: 'llm-rubric',
29
+ assertion_value: '',
30
+ conversation: 'User: hello',
31
+ expected_output: 'Hello',
32
+ evaluation_mode: 'assertion',
33
+ context_mode: 'merge',
34
+ timeout: '0',
35
+ similarity_threshold: '50',
36
+ context: {}
37
+ }
38
+ }
39
+
40
+ async function makeSuite(suiteKey = 'agent') {
41
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bosun-yaml-suite-'))
42
+ temporaryDirectories.push(dir)
43
+ project.basePath = path.join(dir, 'agents')
44
+ project.rootPath = path.join(dir, 'bosun', 'workflows.ts')
45
+ const suiteDirectory = path.join(project.basePath, 'support', 'bosun-tests', suiteKey)
46
+ await mkdir(suiteDirectory, { recursive: true })
47
+ return suiteDirectory
48
+ }
49
+
50
+ describe('YAML test suites', () => {
51
+ it('uses file names as ids and sorts tests by their stored order', async () => {
52
+ const suiteDirectory = await makeSuite()
53
+ await writeFile(
54
+ path.join(suiteDirectory, 'second.yaml'),
55
+ 'order: 1024\npinned: "TRUE"\nscenario: second\nassertion_type: llm-rubric\nassertion_value: ""\nconversation: ""\nexpected_output: ""\ntimeout: "0"\nsimilarity_threshold: "50"\ncontext: {}\n'
56
+ )
24
57
  await writeFile(
25
- suitePath,
26
- [
27
- JSON.stringify({ version: 1, meta: {} }),
28
- JSON.stringify({
29
- id: 'old',
30
- pinned: 'TRUE',
31
- scenario: 'Old test',
32
- step: 'experimental-step',
33
- assertion_type: 'llm-rubric',
34
- assertion_value: '',
35
- conversation: 'User: hello',
36
- expected_output: 'Hello',
37
- capture_mode: 'final',
38
- timeout: '0',
39
- similarity_threshold: '50',
40
- context: {}
41
- })
42
- ].join('\n') + '\n'
58
+ path.join(suiteDirectory, 'first.yaml'),
59
+ 'order: 0\npinned: "TRUE"\nscenario: first\nassertion_type: llm-rubric\nassertion_value: ""\nconversation: ""\nexpected_output: ""\ntimeout: "0"\nsimilarity_threshold: "50"\ncontext: {}\n'
43
60
  )
44
61
 
45
62
  const suite = await readSuite('agent')
46
63
 
47
- expect(suite.version).toBe(2)
48
- expect(suite.tests[0]).toMatchObject({ step: 'main', evaluation_mode: 'assertion' })
49
- expect(suite.tests[0]).not.toHaveProperty('capture_mode')
50
- const persisted = await readFile(suitePath, 'utf8')
51
- expect(JSON.parse(persisted.split('\n')[0])).toMatchObject({ version: 2 })
52
- expect(persisted).not.toContain('capture_mode')
64
+ expect(suite.tests.map((test) => test.id)).toEqual(['first', 'second'])
53
65
  })
66
+
67
+ it('adds and edits independent files and deletes removed tests', async () => {
68
+ const suiteDirectory = await makeSuite()
69
+ await writeSuite('agent', { tests: [row('one', 0), row('two', 1024)] })
70
+ const twoBefore = await readFile(path.join(suiteDirectory, 'two.yaml'), 'utf8')
71
+
72
+ await writeSuite('agent', {
73
+ tests: [{ ...row('one', 0), scenario: 'edited' }, row('two', 1024), row('three', 2048)]
74
+ })
75
+
76
+ expect(await readFile(path.join(suiteDirectory, 'two.yaml'), 'utf8')).toBe(twoBefore)
77
+ expect((await readdir(suiteDirectory)).sort()).toEqual(['one.yaml', 'three.yaml', 'two.yaml'])
78
+
79
+ await writeSuite('agent', { tests: [row('two', 1024), row('three', 2048)] })
80
+ expect((await readdir(suiteDirectory)).sort()).toEqual(['three.yaml', 'two.yaml'])
81
+ })
82
+
83
+ it.each(['tests.jsonl', 'tests.json', 'config.json'])(
84
+ 'reports that a legacy %s suite requires migration',
85
+ async (suffix) => {
86
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bosun-legacy-suite-'))
87
+ temporaryDirectories.push(dir)
88
+ project.basePath = path.join(dir, 'agents')
89
+ project.rootPath = path.join(dir, 'bosun', 'workflows.ts')
90
+ await mkdir(project.basePath, { recursive: true })
91
+ await mkdir(path.dirname(project.rootPath), { recursive: true })
92
+ await mkdir(path.join(project.basePath, 'support', 'bosun-tests', 'agent'), {
93
+ recursive: true
94
+ })
95
+ const legacyPath = path.join(path.dirname(project.rootPath), `agent.${suffix}`)
96
+ await writeFile(legacyPath, '{}\n')
97
+
98
+ await expect(readSuite('agent')).rejects.toThrow(LEGACY_TEST_SUITE_ERROR_PREFIX)
99
+ expect(await readFile(legacyPath, 'utf8')).toBe('{}\n')
100
+ }
101
+ )
54
102
  })