@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.
- package/build/client/_app/immutable/assets/{0.b0jhAoSC.css → 0.FwR3B_mV.css} +1 -1
- package/build/client/_app/immutable/assets/0.FwR3B_mV.css.br +0 -0
- package/build/client/_app/immutable/assets/{0.b0jhAoSC.css.gz → 0.FwR3B_mV.css.gz} +0 -0
- package/build/client/_app/immutable/chunks/BD9x_mnu.js +1 -0
- package/build/client/_app/immutable/chunks/BD9x_mnu.js.br +0 -0
- package/build/client/_app/immutable/chunks/BD9x_mnu.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{lmnC3bvv.js → Bj8jfBTi.js} +1 -1
- package/build/client/_app/immutable/chunks/Bj8jfBTi.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bj8jfBTi.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CX0Ekq8t.js +1 -0
- package/build/client/_app/immutable/chunks/CX0Ekq8t.js.br +0 -0
- package/build/client/_app/immutable/chunks/CX0Ekq8t.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{ChVBNPUb.js → CXESoUel.js} +1 -1
- package/build/client/_app/immutable/chunks/CXESoUel.js.br +0 -0
- package/build/client/_app/immutable/chunks/CXESoUel.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{E9rdsnHb.js → DCuEIs0S.js} +1 -1
- package/build/client/_app/immutable/chunks/DCuEIs0S.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCuEIs0S.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DgGeUDDh.js +1 -0
- package/build/client/_app/immutable/chunks/DgGeUDDh.js.br +0 -0
- package/build/client/_app/immutable/chunks/DgGeUDDh.js.gz +0 -0
- package/build/client/_app/immutable/chunks/_U9sB4BM.js +3 -0
- package/build/client/_app/immutable/chunks/_U9sB4BM.js.br +0 -0
- package/build/client/_app/immutable/chunks/_U9sB4BM.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.BY5DP2K_.js → app.Du7JgOF0.js} +2 -2
- package/build/client/_app/immutable/entry/app.Du7JgOF0.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Du7JgOF0.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BssSvo4L.js +1 -0
- package/build/client/_app/immutable/entry/start.BssSvo4L.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BssSvo4L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.Cao0LJwW.js → 0.EJrjlOP2.js} +1 -1
- package/build/client/_app/immutable/nodes/0.EJrjlOP2.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.EJrjlOP2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.Kjgu6yxL.js → 1.BX3yW9Ki.js} +1 -1
- package/build/client/_app/immutable/nodes/1.BX3yW9Ki.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BX3yW9Ki.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js +13 -0
- package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.HnX8Jhtn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{17.COHkD98p.js → 17.DQQN2LTu.js} +3 -3
- package/build/client/_app/immutable/nodes/17.DQQN2LTu.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.DQQN2LTu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{18.DQVit7Md.js → 18.Dx1VeGYs.js} +1 -1
- package/build/client/_app/immutable/nodes/18.Dx1VeGYs.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.Dx1VeGYs.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.DVwYttKv.js → 2.BSZUitGm.js} +1 -1
- package/build/client/_app/immutable/nodes/2.BSZUitGm.js.br +0 -0
- package/build/client/_app/immutable/nodes/{2.DVwYttKv.js.gz → 2.BSZUitGm.js.gz} +0 -0
- package/build/client/_app/immutable/nodes/{3.BhmyTb3R.js → 3.D2P9M-PP.js} +1 -1
- package/build/client/_app/immutable/nodes/3.D2P9M-PP.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.D2P9M-PP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.CxATX0ci.js → 4.Cx97sxk5.js} +1 -1
- package/build/client/_app/immutable/nodes/4.Cx97sxk5.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.Cx97sxk5.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.Dal1qxB1.js → 5.BAliV6Wc.js} +1 -1
- package/build/client/_app/immutable/nodes/5.BAliV6Wc.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.BAliV6Wc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.iAOxBhJw.js → 6.CmUnus-n.js} +1 -1
- package/build/client/_app/immutable/nodes/6.CmUnus-n.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CmUnus-n.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.DFhKu3dp.js → 8.DLYtZXzs.js} +1 -1
- package/build/client/_app/immutable/nodes/8.DLYtZXzs.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DLYtZXzs.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.DRIEv5m9.js → 9.DQ0jyQBU.js} +1 -1
- package/build/client/_app/immutable/nodes/9.DQ0jyQBU.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DQ0jyQBU.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-CIzAMtA3.js → 0-DB4bFp_f.js} +3 -3
- package/build/server/chunks/{0-CIzAMtA3.js.map → 0-DB4bFp_f.js.map} +1 -1
- package/build/server/chunks/{1-D1heuNbf.js → 1-kigiZghR.js} +2 -2
- package/build/server/chunks/{1-D1heuNbf.js.map → 1-kigiZghR.js.map} +1 -1
- package/build/server/chunks/{10-CjLtjXyZ.js → 10-C49_L5kn.js} +3 -3
- package/build/server/chunks/10-C49_L5kn.js.map +1 -0
- package/build/server/chunks/{17-DklEmmOx.js → 17-Dbb76ckK.js} +3 -3
- package/build/server/chunks/17-Dbb76ckK.js.map +1 -0
- package/build/server/chunks/18-CvqluQBN.js +9 -0
- package/build/server/chunks/18-CvqluQBN.js.map +1 -0
- package/build/server/chunks/{2-DCrAVYXs.js → 2-JGarRt-z.js} +4 -4
- package/build/server/chunks/2-JGarRt-z.js.map +1 -0
- package/build/server/chunks/{3-BpTqa4DU.js → 3-CnXV_OO3.js} +2 -2
- package/build/server/chunks/{3-BpTqa4DU.js.map → 3-CnXV_OO3.js.map} +1 -1
- package/build/server/chunks/{4-B7GjwQzI.js → 4-B-XLuotH.js} +2 -2
- package/build/server/chunks/{4-B7GjwQzI.js.map → 4-B-XLuotH.js.map} +1 -1
- package/build/server/chunks/{5-XjS__bcp.js → 5-Cumig7Am.js} +2 -2
- package/build/server/chunks/{5-XjS__bcp.js.map → 5-Cumig7Am.js.map} +1 -1
- package/build/server/chunks/{6-DWkWptmB.js → 6-Ez7v6PpX.js} +2 -2
- package/build/server/chunks/{6-DWkWptmB.js.map → 6-Ez7v6PpX.js.map} +1 -1
- package/build/server/chunks/{8-_swAx6gL.js → 8-COGeKy3N.js} +3 -3
- package/build/server/chunks/{8-_swAx6gL.js.map → 8-COGeKy3N.js.map} +1 -1
- package/build/server/chunks/9-DlMHnRdZ.js +9 -0
- package/build/server/chunks/9-DlMHnRdZ.js.map +1 -0
- package/build/server/chunks/Panel-D0IY1AzO.js +11 -0
- package/build/server/chunks/Panel-D0IY1AzO.js.map +1 -0
- package/build/server/chunks/{_layout.svelte-PtdpfgwJ.js → _layout.svelte-Blfto3jh.js} +82 -12
- package/build/server/chunks/_layout.svelte-Blfto3jh.js.map +1 -0
- package/build/server/chunks/{_page.svelte-Cu8a_kUO.js → _page.svelte-B1erTzoN.js} +3 -2
- package/build/server/chunks/_page.svelte-B1erTzoN.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BTKQMuSz.js → _page.svelte-DhhqK6z3.js} +12 -85
- package/build/server/chunks/_page.svelte-DhhqK6z3.js.map +1 -0
- package/build/server/chunks/{_page.svelte-C3gizoSD.js → _page.svelte-xf-0yXQM.js} +3 -2
- package/build/server/chunks/_page.svelte-xf-0yXQM.js.map +1 -0
- package/build/server/chunks/edit-workspace-context-biaZqjOf.js +6 -0
- package/build/server/chunks/edit-workspace-context-biaZqjOf.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +13 -13
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/src/backend/lib/project-api.ts +6 -0
- package/src/backend/roleplay/agents.ts +1 -1
- package/src/backend/tests/academy.ts +35 -39
- package/src/backend/tests/context-utils.ts +0 -12
- package/src/backend/tests/data.spec.ts +21 -14
- package/src/backend/tests/data.ts +21 -124
- package/src/backend/tests/pii-sanitized-test-drafts.spec.ts +6 -5
- package/src/backend/tests/pii-sanitized-test-drafts.ts +2 -2
- package/src/backend/tests/router.ts +26 -99
- package/src/backend/tests/suite.spec.ts +82 -34
- package/src/backend/tests/suite.ts +145 -84
- package/build/client/_app/immutable/assets/0.b0jhAoSC.css.br +0 -0
- package/build/client/_app/immutable/chunks/BTX1tKMW.js +0 -1
- package/build/client/_app/immutable/chunks/BTX1tKMW.js.br +0 -0
- package/build/client/_app/immutable/chunks/BTX1tKMW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BVxLHCs8.js +0 -1
- package/build/client/_app/immutable/chunks/BVxLHCs8.js.br +0 -0
- package/build/client/_app/immutable/chunks/BVxLHCs8.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ChVBNPUb.js.br +0 -0
- package/build/client/_app/immutable/chunks/ChVBNPUb.js.gz +0 -0
- package/build/client/_app/immutable/chunks/E9rdsnHb.js.br +0 -0
- package/build/client/_app/immutable/chunks/E9rdsnHb.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Z1_c4us_.js +0 -3
- package/build/client/_app/immutable/chunks/Z1_c4us_.js.br +0 -0
- package/build/client/_app/immutable/chunks/Z1_c4us_.js.gz +0 -0
- package/build/client/_app/immutable/chunks/lmnC3bvv.js.br +0 -0
- package/build/client/_app/immutable/chunks/lmnC3bvv.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.BY5DP2K_.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BY5DP2K_.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BeIIwvvV.js +0 -1
- package/build/client/_app/immutable/entry/start.BeIIwvvV.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BeIIwvvV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.yW2mHq85.js +0 -12
- package/build/client/_app/immutable/nodes/10.yW2mHq85.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.yW2mHq85.js.gz +0 -0
- package/build/client/_app/immutable/nodes/17.COHkD98p.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.COHkD98p.js.gz +0 -0
- package/build/client/_app/immutable/nodes/18.DQVit7Md.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.DQVit7Md.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DVwYttKv.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.CxATX0ci.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.CxATX0ci.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.gz +0 -0
- package/build/server/chunks/10-CjLtjXyZ.js.map +0 -1
- package/build/server/chunks/17-DklEmmOx.js.map +0 -1
- package/build/server/chunks/18-BJS-eRcz.js +0 -9
- package/build/server/chunks/18-BJS-eRcz.js.map +0 -1
- package/build/server/chunks/2-DCrAVYXs.js.map +0 -1
- package/build/server/chunks/9-NEe24O4u.js +0 -9
- package/build/server/chunks/9-NEe24O4u.js.map +0 -1
- package/build/server/chunks/ContextEditor-ngRBOJVc.js +0 -89
- package/build/server/chunks/ContextEditor-ngRBOJVc.js.map +0 -1
- package/build/server/chunks/_layout.svelte-PtdpfgwJ.js.map +0 -1
- package/build/server/chunks/_page.svelte-BTKQMuSz.js.map +0 -1
- package/build/server/chunks/_page.svelte-C3gizoSD.js.map +0 -1
- package/build/server/chunks/_page.svelte-Cu8a_kUO.js.map +0 -1
- package/build/server/chunks/edit-workspace-context-BEioXKWB.js +0 -12
- package/build/server/chunks/edit-workspace-context-BEioXKWB.js.map +0 -1
- package/src/backend/tests/config.ts +0 -30
- /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css → Popper.CLTyHc88.css} +0 -0
- /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css.br → Popper.CLTyHc88.css.br} +0 -0
- /package/build/client/_app/immutable/assets/{steps.CLTyHc88.css.gz → Popper.CLTyHc88.css.gz} +0 -0
|
@@ -1,39 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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, {
|
|
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
|
-
|
|
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,
|
|
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][
|
|
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,
|
|
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][
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
6
|
+
const project = vi.hoisted(() => ({ basePath: '', rootPath: '' }))
|
|
7
7
|
|
|
8
|
-
vi.mock('../lib/
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
})
|