@cursor/july 0.1.26 → 0.1.27

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 (277) hide show
  1. package/dist/ab.d.ts +6 -0
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +1 -0
  4. package/dist/artifacts.d.ts +34 -0
  5. package/dist/artifacts.d.ts.map +1 -0
  6. package/dist/artifacts.js +57 -0
  7. package/dist/bin/agent-serve.js +31 -9
  8. package/dist/channels/github/github-channel.d.ts +1 -1
  9. package/dist/channels/github/github-channel.d.ts.map +1 -1
  10. package/dist/channels/github/github-channel.js +14 -1
  11. package/dist/channels/github/types.d.ts +6 -1
  12. package/dist/channels/github/types.d.ts.map +1 -1
  13. package/dist/channels/slack/api.d.ts +2 -1
  14. package/dist/channels/slack/api.d.ts.map +1 -1
  15. package/dist/channels/slack/api.js +1 -0
  16. package/dist/channels/slack/cursor-account.d.ts +2 -1
  17. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  18. package/dist/channels/slack/cursor-account.js +1 -1
  19. package/dist/channels/slack/dispatch.d.ts +4 -1
  20. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  21. package/dist/channels/slack/dispatch.js +3 -3
  22. package/dist/channels/slack/interactive.d.ts +2 -1
  23. package/dist/channels/slack/interactive.d.ts.map +1 -1
  24. package/dist/channels/slack/interactive.js +1 -0
  25. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  26. package/dist/channels/slack/slack-channel.js +4 -3
  27. package/dist/channels/slack/types.d.ts +6 -1
  28. package/dist/channels/slack/types.d.ts.map +1 -1
  29. package/dist/docs/404.html +1 -1
  30. package/dist/docs/ab.html +2 -2
  31. package/dist/docs/assets/{app.DYcC9FY-.js → app.BICLoJYF.js} +1 -1
  32. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +1 -0
  33. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.wN4Ko9dT.js} +1 -1
  34. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.dnnTE-Co.js} +2 -2
  35. package/dist/docs/building-with-agents.html +2 -2
  36. package/dist/docs/concepts.html +2 -2
  37. package/dist/docs/deployment.html +2 -2
  38. package/dist/docs/evals.html +2 -2
  39. package/dist/docs/example-agents/approval-buddy.html +2 -2
  40. package/dist/docs/example-agents/benny.html +2 -2
  41. package/dist/docs/example-agents/bugbot.html +2 -2
  42. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  43. package/dist/docs/example-agents/codeowners-review.html +2 -2
  44. package/dist/docs/example-agents/concierge.html +2 -2
  45. package/dist/docs/example-agents/fsd.html +2 -2
  46. package/dist/docs/example-agents/index.html +2 -2
  47. package/dist/docs/example-agents/knowledge-base.html +2 -2
  48. package/dist/docs/example-agents/oncall.html +2 -2
  49. package/dist/docs/example-agents/security-reviewer.html +2 -2
  50. package/dist/docs/example-agents/slack-agent.html +2 -2
  51. package/dist/docs/example-agents/weather-agent.html +2 -2
  52. package/dist/docs/guides/agent-to-agent.html +2 -2
  53. package/dist/docs/guides/cloud-runtime.html +2 -2
  54. package/dist/docs/guides/github.html +2 -2
  55. package/dist/docs/guides/human-in-the-loop.html +2 -2
  56. package/dist/docs/guides/mcp-oauth.html +2 -2
  57. package/dist/docs/guides/slack.html +2 -2
  58. package/dist/docs/guides/webhooks.html +2 -2
  59. package/dist/docs/hillclimbing.html +2 -2
  60. package/dist/docs/index.html +2 -2
  61. package/dist/docs/quickstart.html +2 -2
  62. package/dist/docs/reference/agent-config.html +2 -2
  63. package/dist/docs/reference/channels.html +2 -2
  64. package/dist/docs/reference/cli.html +2 -2
  65. package/dist/docs/reference/connections.html +2 -2
  66. package/dist/docs/reference/hooks.html +2 -2
  67. package/dist/docs/reference/http-api.html +2 -2
  68. package/dist/docs/reference/instructions.html +2 -2
  69. package/dist/docs/reference/playground.html +2 -2
  70. package/dist/docs/reference/project-layout.html +2 -2
  71. package/dist/docs/reference/prompt.html +2 -2
  72. package/dist/docs/reference/schedules.html +2 -2
  73. package/dist/docs/reference/sessions.html +2 -2
  74. package/dist/docs/reference/skills.html +2 -2
  75. package/dist/docs/reference/subagents.html +2 -2
  76. package/dist/docs/reference/tools.html +2 -2
  77. package/dist/docs/scaffolding-agents.html +2 -2
  78. package/dist/docs/storage.html +2 -2
  79. package/dist/docs/troubleshooting.html +2 -2
  80. package/dist/evals/assertions.d.ts +10 -1
  81. package/dist/evals/assertions.d.ts.map +1 -1
  82. package/dist/evals/assertions.js +12 -0
  83. package/dist/evals.d.ts +1 -1
  84. package/dist/evals.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -0
  88. package/dist/internal/ab-fold.d.ts.map +1 -1
  89. package/dist/internal/ab-fold.js +4 -0
  90. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  91. package/dist/internal/ab-snapshot.js +1 -0
  92. package/dist/internal/artifacts-store.d.ts +45 -0
  93. package/dist/internal/artifacts-store.d.ts.map +1 -0
  94. package/dist/internal/artifacts-store.js +250 -0
  95. package/dist/internal/builtin-tools/artifacts.d.ts +10 -0
  96. package/dist/internal/builtin-tools/artifacts.d.ts.map +1 -0
  97. package/dist/internal/builtin-tools/artifacts.js +73 -0
  98. package/dist/internal/builtin-tools/index.d.ts +8 -1
  99. package/dist/internal/builtin-tools/index.d.ts.map +1 -1
  100. package/dist/internal/builtin-tools/index.js +15 -0
  101. package/dist/internal/cli-ax.d.ts +11 -0
  102. package/dist/internal/cli-ax.d.ts.map +1 -1
  103. package/dist/internal/cli-ax.js +80 -0
  104. package/dist/internal/cli-slack.d.ts.map +1 -1
  105. package/dist/internal/cli-slack.js +14 -6
  106. package/dist/internal/discovery.d.ts.map +1 -1
  107. package/dist/internal/discovery.js +83 -2
  108. package/dist/internal/distribution.d.ts.map +1 -1
  109. package/dist/internal/distribution.js +1 -0
  110. package/dist/internal/model-pricing.d.ts +49 -0
  111. package/dist/internal/model-pricing.d.ts.map +1 -0
  112. package/dist/internal/model-pricing.js +377 -0
  113. package/dist/internal/reminder-runner.d.ts.map +1 -1
  114. package/dist/internal/reminder-runner.js +1 -0
  115. package/dist/internal/schedule-runner.d.ts.map +1 -1
  116. package/dist/internal/schedule-runner.js +1 -0
  117. package/dist/internal/sdk-runner.d.ts.map +1 -1
  118. package/dist/internal/sdk-runner.js +9 -1
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +39 -0
  121. package/dist/internal/session-cost.d.ts +49 -0
  122. package/dist/internal/session-cost.d.ts.map +1 -0
  123. package/dist/internal/session-cost.js +175 -0
  124. package/dist/internal/session-engine.d.ts +35 -1
  125. package/dist/internal/session-engine.d.ts.map +1 -1
  126. package/dist/internal/session-engine.js +128 -7
  127. package/dist/internal/storage-coordinator.d.ts +15 -0
  128. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  129. package/dist/internal/storage-coordinator.js +52 -0
  130. package/dist/internal/trajectory.d.ts +9 -1
  131. package/dist/internal/trajectory.d.ts.map +1 -1
  132. package/dist/internal/trajectory.js +32 -5
  133. package/dist/playground/assets/index-BDCeW4uc.js +86 -0
  134. package/dist/playground/assets/index-Bul1WfrB.css +1 -0
  135. package/dist/playground/index.html +2 -2
  136. package/dist/storage.d.ts +4 -0
  137. package/dist/storage.d.ts.map +1 -1
  138. package/dist/storage.js +4 -0
  139. package/dist/types.d.ts +164 -1
  140. package/dist/types.d.ts.map +1 -1
  141. package/package.json +26 -26
  142. package/src/ab.ts +7 -0
  143. package/src/bin/agent-serve.ts +30 -2
  144. package/src/internal/ab-fold.ts +4 -0
  145. package/src/internal/ab-snapshot.ts +1 -0
  146. package/src/internal/cli-ax.ts +95 -0
  147. package/src/internal/model-pricing.ts +426 -0
  148. package/src/internal/sdk-runner.ts +10 -2
  149. package/src/internal/session-cost.ts +231 -0
  150. package/src/internal/session-engine.ts +28 -0
  151. package/src/internal/trajectory.ts +45 -3
  152. package/src/types.ts +24 -1
  153. package/dist/channels/github/instrument.d.ts +0 -20
  154. package/dist/channels/github/instrument.d.ts.map +0 -1
  155. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  156. package/dist/internal/json-dir-store.d.ts +0 -32
  157. package/dist/internal/json-dir-store.d.ts.map +0 -1
  158. package/dist/internal/json-dir-store.js +0 -100
  159. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  160. package/dist/playground/assets/index-DqXdAFGa.js +0 -85
  161. package/src/bin/agent-serve.version.test.ts +0 -64
  162. package/src/channels/github/api.test.ts +0 -64
  163. package/src/channels/github/auth.test.ts +0 -105
  164. package/src/channels/github/cursor-account.test.ts +0 -204
  165. package/src/channels/github/forward.test.ts +0 -457
  166. package/src/channels/github/github.test.ts +0 -985
  167. package/src/channels/github/replay.test.ts +0 -179
  168. package/src/channels/slack/api.post-message.test.ts +0 -148
  169. package/src/channels/slack/approvals.test.ts +0 -328
  170. package/src/channels/slack/block-actions.test.ts +0 -469
  171. package/src/channels/slack/bot-mentions.test.ts +0 -267
  172. package/src/channels/slack/channel-watch.test.ts +0 -363
  173. package/src/channels/slack/cursor-account.test.ts +0 -301
  174. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  175. package/src/channels/slack/dispatch.test.ts +0 -875
  176. package/src/channels/slack/eval-directive.test.ts +0 -273
  177. package/src/channels/slack/message-body.test.ts +0 -54
  178. package/src/channels/slack/nudge-store.test.ts +0 -143
  179. package/src/channels/slack/slack.test.ts +0 -391
  180. package/src/channels/slack/stop.test.ts +0 -23
  181. package/src/channels/slack/thread-context.test.ts +0 -202
  182. package/src/evals/assertions.test.ts +0 -617
  183. package/src/evals/expect.test.ts +0 -144
  184. package/src/evals/judge.test.ts +0 -181
  185. package/src/evals/loaders.test.ts +0 -132
  186. package/src/evals/matchers.test.ts +0 -95
  187. package/src/evals/reporters.test.ts +0 -303
  188. package/src/evals/run-facts.test.ts +0 -259
  189. package/src/internal/ab-snapshot.test.ts +0 -325
  190. package/src/internal/approval-gate.test.ts +0 -49
  191. package/src/internal/approvals.integration.test.ts +0 -383
  192. package/src/internal/artifacts-store.test.ts +0 -283
  193. package/src/internal/authored-loaders.test.ts +0 -31
  194. package/src/internal/builtin-tools/artifacts.test.ts +0 -247
  195. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  196. package/src/internal/channel-route-schema.test.ts +0 -294
  197. package/src/internal/chat-attach.test.ts +0 -262
  198. package/src/internal/cli-deploy.test.ts +0 -1991
  199. package/src/internal/cli-docs.test.ts +0 -161
  200. package/src/internal/cli-mcp.test.ts +0 -789
  201. package/src/internal/cli-skills.test.ts +0 -133
  202. package/src/internal/cli-slack.test.ts +0 -1654
  203. package/src/internal/cloud-merge.test.ts +0 -74
  204. package/src/internal/cron.test.ts +0 -22
  205. package/src/internal/cursor/account-mcp.test.ts +0 -807
  206. package/src/internal/cursor/backend-client.test.ts +0 -591
  207. package/src/internal/cursor/credentials.test.ts +0 -351
  208. package/src/internal/cursor/github-credentials.test.ts +0 -136
  209. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  210. package/src/internal/cursor-account.integration.test.ts +0 -441
  211. package/src/internal/cursor-event-relay.test.ts +0 -746
  212. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  213. package/src/internal/cursor-slack-relay.test.ts +0 -525
  214. package/src/internal/deploy-source.test.ts +0 -111
  215. package/src/internal/discovery.artifact-tool.test.ts +0 -136
  216. package/src/internal/discovery.artifacts.test.ts +0 -119
  217. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  218. package/src/internal/discovery.concurrency.test.ts +0 -60
  219. package/src/internal/discovery.cursor-account.test.ts +0 -133
  220. package/src/internal/discovery.cwd.test.ts +0 -83
  221. package/src/internal/discovery.hosting.test.ts +0 -80
  222. package/src/internal/discovery.identity.test.ts +0 -44
  223. package/src/internal/docs-site.test.ts +0 -66
  224. package/src/internal/duration.test.ts +0 -29
  225. package/src/internal/eval-judge-model.test.ts +0 -187
  226. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  227. package/src/internal/eval-run-store.storage.test.ts +0 -211
  228. package/src/internal/eval-runner.http.test.ts +0 -403
  229. package/src/internal/eval-runner.run.test.ts +0 -928
  230. package/src/internal/evals-client.test.ts +0 -307
  231. package/src/internal/event-mapper.test.ts +0 -243
  232. package/src/internal/github-fanout.test.ts +0 -213
  233. package/src/internal/handleAgentServeTrigger.test.ts +0 -185
  234. package/src/internal/host-kv.test.ts +0 -82
  235. package/src/internal/host-platforms.test.ts +0 -126
  236. package/src/internal/http-channel.test.ts +0 -402
  237. package/src/internal/init-project.test.ts +0 -270
  238. package/src/internal/install-cursor-skills.test.ts +0 -262
  239. package/src/internal/local-env.test.ts +0 -120
  240. package/src/internal/log-ring.test.ts +0 -31
  241. package/src/internal/logs-client.test.ts +0 -350
  242. package/src/internal/mcp-endpoint.test.ts +0 -436
  243. package/src/internal/mcp-host.test.ts +0 -298
  244. package/src/internal/mcp-oauth.test.ts +0 -148
  245. package/src/internal/net.test.ts +0 -17
  246. package/src/internal/peer-connections.test.ts +0 -128
  247. package/src/internal/peer-mcp.integration.test.ts +0 -289
  248. package/src/internal/playground/toolchain.test.ts +0 -53
  249. package/src/internal/playground-cli.test.ts +0 -187
  250. package/src/internal/playground-proxy.test.ts +0 -376
  251. package/src/internal/prompt-context.integration.test.ts +0 -232
  252. package/src/internal/prompt-context.test.ts +0 -127
  253. package/src/internal/reminder-runner.test.ts +0 -432
  254. package/src/internal/reminder-store.test.ts +0 -53
  255. package/src/internal/request-headers.test.ts +0 -27
  256. package/src/internal/resolve-prod-target.test.ts +0 -787
  257. package/src/internal/resolved-connections.test.ts +0 -295
  258. package/src/internal/router.test.ts +0 -57
  259. package/src/internal/sdk-runner.test.ts +0 -290
  260. package/src/internal/server.artifacts.test.ts +0 -334
  261. package/src/internal/session-engine.artifacts.test.ts +0 -293
  262. package/src/internal/session-engine.coalesce.test.ts +0 -169
  263. package/src/internal/session-engine.concurrency.test.ts +0 -250
  264. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  265. package/src/internal/session-engine.interrupt.test.ts +0 -577
  266. package/src/internal/session-engine.storage.test.ts +0 -547
  267. package/src/internal/session-urls.test.ts +0 -28
  268. package/src/internal/sessions-client.test.ts +0 -518
  269. package/src/internal/storage-coordinator.test.ts +0 -517
  270. package/src/internal/tool-call.test.ts +0 -458
  271. package/src/internal/tool-result.test.ts +0 -52
  272. package/src/internal/trajectory.approvals.test.ts +0 -83
  273. package/src/internal/trajectory.subagents.test.ts +0 -198
  274. package/src/internal/turn-governor.test.ts +0 -137
  275. package/src/internal/update-check.test.ts +0 -485
  276. package/src/internal/workspace.test.ts +0 -207
  277. package/src/storage-backends/cursor-hosted.test.ts +0 -121
@@ -1,928 +0,0 @@
1
- /**
2
- * End-to-end coverage for the eval runner: discovery, the severity model,
3
- * judges, verdicts, and reporters. `runSession` is mocked so a case can drive
4
- * a scripted agent without a live model or an HTTP server.
5
- */
6
-
7
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
- import { EvalJudgeUnavailableError } from "../evals/judge.js";
12
- import { EVAL_ROOT_ENV } from "../evals/loaders.js";
13
- import type {
14
- EvalDefinition,
15
- EvalReporter,
16
- EvalRunResult,
17
- EvalTestContext,
18
- } from "../evals.js";
19
- import { defineEval, defineEvalConfig, equals, similarity } from "../evals.js";
20
- import type { SessionEvent } from "../types.js";
21
- import * as evalRunner from "./eval-runner.js";
22
- import * as trajectory from "./trajectory.js";
23
-
24
- const runSession = vi.hoisted(() => vi.fn());
25
-
26
- vi.mock("./run-client.js", async (importOriginal) => {
27
- const actual = await importOriginal<typeof import("./run-client.js")>();
28
- return { ...actual, runSession };
29
- });
30
-
31
- // `vi.mock` is hoisted above these imports, so the runner sees the scripted
32
- // `runSession` even though the imports are static.
33
- const { discoverEvals, expandEvalModule, runDiscoveredEvals } = evalRunner;
34
- const { buildTrajectory } = trajectory;
35
-
36
- let sequence = 0;
37
-
38
- function event(type: string, data: unknown): SessionEvent {
39
- sequence++;
40
- return {
41
- sessionId: "ses_1",
42
- seq: sequence,
43
- at: new Date(sequence * 1000).toISOString(),
44
- turnId: "turn_1",
45
- type,
46
- data,
47
- } as unknown as SessionEvent;
48
- }
49
-
50
- /** A settled turn that called `get_weather` and replied "Sunny, 72F". */
51
- function weatherEvents(reply = "Sunny, 72F"): SessionEvent[] {
52
- return [
53
- event("message.received", { text: "weather?" }),
54
- event("actions.requested", {
55
- calls: [{ callId: "c1", toolName: "get_weather", args: { city: "NYC" } }],
56
- }),
57
- event("action.result", {
58
- callId: "c1",
59
- toolName: "get_weather",
60
- output: { tempF: 72 },
61
- isError: false,
62
- }),
63
- event("message.completed", { text: reply, finishReason: "stop" }),
64
- event("turn.completed", {}),
65
- event("session.waiting", {}),
66
- ];
67
- }
68
-
69
- function scriptTurn(events: SessionEvent[]): void {
70
- runSession.mockImplementation(async () => {
71
- const trajectory = buildTrajectory(events);
72
- return {
73
- sessionId: "ses_1",
74
- continuationToken: "tok_1",
75
- baseUrl: "http://127.0.0.1:3000",
76
- events,
77
- trajectory,
78
- ok: trajectory.ok,
79
- };
80
- });
81
- }
82
-
83
- async function runCase(
84
- definition: EvalDefinition,
85
- options: {
86
- reporters?: EvalReporter[];
87
- strict?: boolean;
88
- judgeCaller?: (args: { prompt: string }) => Promise<string>;
89
- judgeModel?: string;
90
- skipReport?: boolean;
91
- configReporters?: EvalReporter[];
92
- } = {}
93
- ): Promise<EvalRunResult[]> {
94
- const expanded = expandEvalModule("smoke", definition);
95
- return runDiscoveredEvals({
96
- projectRoot: "/unused",
97
- baseUrl: "http://127.0.0.1:3000",
98
- reporters: options.reporters,
99
- skipReport: options.skipReport,
100
- strict: options.strict,
101
- judgeCaller: options.judgeCaller as never,
102
- discovered: {
103
- evals: expanded.map((e) => ({
104
- id: e.id,
105
- fileId: "smoke",
106
- path: "/repo/evals/smoke.eval.ts",
107
- definition: e.definition,
108
- })),
109
- config: {
110
- maxConcurrency: 4,
111
- judge:
112
- options.judgeModel === undefined
113
- ? undefined
114
- : { model: options.judgeModel },
115
- reporters: options.configReporters,
116
- },
117
- },
118
- });
119
- }
120
-
121
- beforeEach(() => {
122
- sequence = 0;
123
- runSession.mockReset();
124
- scriptTurn(weatherEvents());
125
- });
126
-
127
- describe("verdicts", () => {
128
- it("passes a case whose gates all pass", async () => {
129
- const [result] = await runCase(
130
- defineEval({
131
- async test(t) {
132
- await t.send("weather?");
133
- t.succeeded();
134
- t.calledTool("get_weather", { input: { city: "NYC" }, count: 1 });
135
- t.messageIncludes("Sunny");
136
- },
137
- })
138
- );
139
- expect(result).toMatchObject({ verdict: "passed", ok: true });
140
- expect(result?.assertions.every((a) => a.passed)).toBe(true);
141
- });
142
-
143
- it("fails a case with a missed gate and still records every assertion", async () => {
144
- const [result] = await runCase(
145
- defineEval({
146
- async test(t) {
147
- await t.send("weather?");
148
- t.calledTool("send_email");
149
- t.messageIncludes("Rainy");
150
- t.succeeded();
151
- },
152
- })
153
- );
154
- expect(result).toMatchObject({ verdict: "failed", ok: false });
155
- // Recorded assertions never throw, so one run reports all three outcomes.
156
- expect(result?.assertions.map((a) => a.passed)).toEqual([
157
- false,
158
- false,
159
- true,
160
- ]);
161
- });
162
-
163
- it("scores a case that only misses a soft threshold, leaving ok true", async () => {
164
- const [result] = await runCase(
165
- defineEval({
166
- async test(t) {
167
- await t.send("weather?");
168
- t.succeeded();
169
- t.check(t.reply, similarity("Rainy and cold")).atLeast(0.9);
170
- },
171
- })
172
- );
173
- expect(result).toMatchObject({ verdict: "scored", ok: true });
174
- expect(result?.assertions[1]).toMatchObject({
175
- severity: "soft",
176
- threshold: 0.9,
177
- passed: false,
178
- });
179
- });
180
-
181
- it("keeps the historical fallback when a case records no assertions", async () => {
182
- const [passing] = await runCase(
183
- defineEval({
184
- async test(t) {
185
- await t.send("weather?");
186
- },
187
- })
188
- );
189
- expect(passing?.verdict).toBe("passed");
190
-
191
- scriptTurn([
192
- event("message.received", { text: "hi" }),
193
- event("turn.failed", { message: "model error" }),
194
- ]);
195
- const [failing] = await runCase(
196
- defineEval({
197
- async test(t) {
198
- await t.send("hi");
199
- },
200
- })
201
- );
202
- expect(failing?.verdict).toBe("failed");
203
- });
204
-
205
- it("reports a thrown test body as a failed execution error", async () => {
206
- const [result] = await runCase(
207
- defineEval({
208
- async test(t) {
209
- await t.send("weather?");
210
- t.succeeded();
211
- throw new Error("helper blew up");
212
- },
213
- })
214
- );
215
- expect(result).toMatchObject({ verdict: "failed", ok: false });
216
- expect(result?.error).toBe("helper blew up");
217
- // Assertions recorded before the throw are still reported.
218
- expect(result?.assertions).toHaveLength(1);
219
- });
220
- });
221
-
222
- describe("t.skip and t.require", () => {
223
- it("reports t.skip separately and keeps ok true", async () => {
224
- const [result] = await runCase(
225
- defineEval({
226
- async test(t) {
227
- t.skip("cloud runtime not available here");
228
- await t.send("weather?");
229
- },
230
- })
231
- );
232
- expect(result).toMatchObject({
233
- verdict: "skipped",
234
- ok: true,
235
- skipReason: "cloud runtime not available here",
236
- });
237
- expect(runSession).not.toHaveBeenCalled();
238
- });
239
-
240
- it("stops the body on a failed require without a duplicate execution error", async () => {
241
- const seen: string[] = [];
242
- const [result] = await runCase(
243
- defineEval({
244
- async test(t) {
245
- await t.send("weather?");
246
- await t.require(t.reply, equals("Rainy"));
247
- seen.push("unreachable");
248
- t.succeeded();
249
- },
250
- })
251
- );
252
- expect(seen).toEqual([]);
253
- expect(result).toMatchObject({ verdict: "failed", ok: false });
254
- // The recorded gate is the whole story; no duplicate execution error.
255
- expect(result?.error).toBeUndefined();
256
- expect(result?.assertions).toHaveLength(1);
257
- expect(result?.assertions[0]).toMatchObject({
258
- name: "require(equals)",
259
- passed: false,
260
- detail: "got Sunny, 72F",
261
- });
262
- });
263
-
264
- it("returns the value and continues when require passes", async () => {
265
- const [result] = await runCase(
266
- defineEval({
267
- async test(t) {
268
- await t.send("weather?");
269
- const reply = await t.require(t.reply, equals("Sunny, 72F"));
270
- t.check(reply, equals("Sunny, 72F"));
271
- },
272
- })
273
- );
274
- expect(result?.verdict).toBe("passed");
275
- expect(result?.assertions).toHaveLength(2);
276
- });
277
- });
278
-
279
- describe("turn scoping", () => {
280
- it("grades an intermediate turn before the next send overwrites the reply", async () => {
281
- const first = weatherEvents("Draft: Best regards");
282
- const second = [
283
- event("message.received", { text: "send it" }),
284
- event("actions.requested", {
285
- calls: [{ callId: "c2", toolName: "send_email", args: {} }],
286
- }),
287
- event("action.result", {
288
- callId: "c2",
289
- toolName: "send_email",
290
- output: "sent",
291
- isError: false,
292
- }),
293
- event("message.completed", { text: "Sent.", finishReason: "stop" }),
294
- event("turn.completed", {}),
295
- ];
296
- const scripted = [first, second];
297
- runSession.mockImplementation(async () => {
298
- const events = scripted.shift() ?? [];
299
- const trajectory = buildTrajectory(events);
300
- return {
301
- sessionId: "ses_1",
302
- continuationToken: "tok",
303
- baseUrl: "http://127.0.0.1:3000",
304
- events,
305
- trajectory,
306
- ok: trajectory.ok,
307
- };
308
- });
309
-
310
- const [result] = await runCase(
311
- defineEval({
312
- async test(t) {
313
- const draft = await t.send("draft it");
314
- draft.messageIncludes("Best regards");
315
- draft.notCalledTool("send_email");
316
-
317
- await t.send("send it");
318
- t.calledTool("send_email");
319
- expect(t.turns).toHaveLength(2);
320
- expect(t.reply).toBe("Sent.");
321
- },
322
- })
323
- );
324
- expect(result?.verdict).toBe("passed");
325
- expect(result?.assertions.map((a) => a.name)).toEqual([
326
- "turn[1].messageIncludes(Best regards)",
327
- "turn[1].notCalledTool(send_email)",
328
- "calledTool(send_email)",
329
- ]);
330
- expect(result?.inputs).toEqual(["draft it", "send it"]);
331
- });
332
- });
333
-
334
- describe("judges", () => {
335
- const judgeReply = "CHOICE: Y\nREASON: it names a source";
336
-
337
- it("records a judge score as a soft assertion", async () => {
338
- const judgeCaller = vi.fn(async () => judgeReply);
339
- const [result] = await runCase(
340
- defineEval({
341
- async test(t) {
342
- await t.send("weather?");
343
- t.judge.closedQA("mentions the temperature");
344
- },
345
- }),
346
- { judgeCaller, judgeModel: "gpt-5.4-mini" }
347
- );
348
- expect(result?.verdict).toBe("passed");
349
- expect(result?.assertions[0]).toMatchObject({
350
- name: "judge.closedQA(mentions the temperature)",
351
- severity: "soft",
352
- score: 1,
353
- passed: true,
354
- detail: "Y (1.00) — it names a source",
355
- });
356
- expect(judgeCaller).toHaveBeenCalledTimes(1);
357
- });
358
-
359
- it("grades t.reply by default and an explicit value with `on`", async () => {
360
- const prompts: string[] = [];
361
- await runCase(
362
- defineEval({
363
- async test(t) {
364
- await t.send("weather?");
365
- t.judge.closedQA("criterion");
366
- t.judge.closedQA("criterion", { on: "a different value" });
367
- },
368
- }),
369
- {
370
- judgeModel: "gpt-5.4-mini",
371
- judgeCaller: async ({ prompt }) => {
372
- prompts.push(prompt);
373
- return judgeReply;
374
- },
375
- }
376
- );
377
- expect(prompts[0]).toContain("Sunny, 72F");
378
- expect(prompts[1]).toContain("a different value");
379
- });
380
-
381
- it("marks the eval scored when a judge misses its bar", async () => {
382
- const [result] = await runCase(
383
- defineEval({
384
- async test(t) {
385
- await t.send("weather?");
386
- t.succeeded();
387
- t.judge.factuality("Rainy and 40F").atLeast(0.8);
388
- },
389
- }),
390
- {
391
- judgeModel: "gpt-5.4-mini",
392
- judgeCaller: async () => "CHOICE: D\nREASON: the values disagree",
393
- }
394
- );
395
- expect(result?.verdict).toBe("scored");
396
- expect(result?.assertions[1]).toMatchObject({ score: 0, threshold: 0.8 });
397
- });
398
-
399
- it("fails the eval when a judge is promoted to a gate and misses", async () => {
400
- const [result] = await runCase(
401
- defineEval({
402
- async test(t) {
403
- await t.send("weather?");
404
- t.judge.factuality("Rainy and 40F").gate(0.8);
405
- },
406
- }),
407
- { judgeModel: "gpt-5.4-mini", judgeCaller: async () => "CHOICE: D" }
408
- );
409
- expect(result?.verdict).toBe("failed");
410
- });
411
-
412
- it("records a failed gate when no judge model is configured anywhere", async () => {
413
- const judgeCaller = vi.fn(async () => judgeReply);
414
- const [result] = await runCase(
415
- defineEval({
416
- async test(t) {
417
- await t.send("weather?");
418
- t.judge.closedQA("criterion");
419
- },
420
- }),
421
- { judgeCaller }
422
- );
423
- expect(result?.verdict).toBe("failed");
424
- expect(result?.assertions[0]).toMatchObject({
425
- passed: false,
426
- detail: expect.stringContaining("no judge model configured"),
427
- });
428
- expect(judgeCaller).not.toHaveBeenCalled();
429
- });
430
-
431
- it("prefers the per-eval judge model over the config default", async () => {
432
- const models: unknown[] = [];
433
- await runCase(
434
- defineEval({
435
- judge: { model: "claude-opus-4.8" },
436
- async test(t) {
437
- await t.send("weather?");
438
- t.judge.closedQA("criterion");
439
- t.judge.closedQA("criterion", { model: "gpt-5.4-mini" });
440
- },
441
- }),
442
- {
443
- judgeModel: "config-default",
444
- judgeCaller: async (args: { prompt: string; model?: unknown }) => {
445
- models.push(args.model);
446
- return judgeReply;
447
- },
448
- }
449
- );
450
- expect(models).toEqual(["claude-opus-4.8", "gpt-5.4-mini"]);
451
- });
452
-
453
- it("skips the eval visibly when the judge has no credentials", async () => {
454
- const [result] = await runCase(
455
- defineEval({
456
- async test(t) {
457
- await t.send("weather?");
458
- t.succeeded();
459
- t.judge.closedQA("criterion");
460
- },
461
- }),
462
- {
463
- judgeModel: "gpt-5.4-mini",
464
- judgeCaller: async () => {
465
- throw new EvalJudgeUnavailableError("no Cursor credentials");
466
- },
467
- }
468
- );
469
- // A missing credential is an environment gap, not a quality regression.
470
- expect(result).toMatchObject({ verdict: "skipped", ok: true });
471
- expect(result?.skipReason).toContain("no Cursor credentials");
472
- expect(result?.logs).toContain("judge skipped: no Cursor credentials");
473
- });
474
-
475
- it("still reports a failed gate when the judge is unavailable", async () => {
476
- // Otherwise a machine without judge credentials would turn a genuinely red
477
- // eval green: the skip would bury the deterministic failure.
478
- const [result] = await runCase(
479
- defineEval({
480
- async test(t) {
481
- await t.send("weather?");
482
- t.calledTool("send_email");
483
- t.judge.closedQA("criterion");
484
- },
485
- }),
486
- {
487
- judgeModel: "gpt-5.4-mini",
488
- judgeCaller: async () => {
489
- throw new EvalJudgeUnavailableError("no Cursor credentials");
490
- },
491
- }
492
- );
493
- expect(result).toMatchObject({ verdict: "failed", ok: false });
494
- expect(result?.skipReason).toBeUndefined();
495
- expect(result?.logs.join("\n")).toContain("a gate already failed");
496
- });
497
-
498
- it("skips rather than scoring when only soft judge signal is missing", async () => {
499
- const [result] = await runCase(
500
- defineEval({
501
- async test(t) {
502
- await t.send("weather?");
503
- t.succeeded();
504
- t.judge.closedQA("criterion").atLeast(0.6);
505
- },
506
- }),
507
- {
508
- judgeModel: "gpt-5.4-mini",
509
- judgeCaller: async () => {
510
- throw new EvalJudgeUnavailableError("no Cursor credentials");
511
- },
512
- }
513
- );
514
- expect(result).toMatchObject({ verdict: "skipped", ok: true });
515
- });
516
-
517
- it("fails the assertion when the judge reply is unparseable", async () => {
518
- const [result] = await runCase(
519
- defineEval({
520
- async test(t) {
521
- await t.send("weather?");
522
- t.judge.closedQA("criterion");
523
- },
524
- }),
525
- { judgeModel: "gpt-5.4-mini", judgeCaller: async () => "I refuse" }
526
- );
527
- expect(result?.verdict).toBe("failed");
528
- expect(result?.assertions[0]?.detail).toMatch(/had no choice/);
529
- });
530
-
531
- it("runs several judges concurrently and waits for all of them", async () => {
532
- let inFlight = 0;
533
- let maxInFlight = 0;
534
- const [result] = await runCase(
535
- defineEval({
536
- async test(t) {
537
- await t.send("weather?");
538
- t.judge.closedQA("first");
539
- t.judge.closedQA("second");
540
- t.judge.closedQA("third");
541
- },
542
- }),
543
- {
544
- judgeModel: "gpt-5.4-mini",
545
- judgeCaller: async () => {
546
- inFlight++;
547
- maxInFlight = Math.max(maxInFlight, inFlight);
548
- await new Promise((resolve) => setTimeout(resolve, 5));
549
- inFlight--;
550
- return judgeReply;
551
- },
552
- }
553
- );
554
- expect(maxInFlight).toBeGreaterThan(1);
555
- expect(result?.assertions.map((a) => a.score)).toEqual([1, 1, 1]);
556
- });
557
- });
558
-
559
- describe("t.judge.model", () => {
560
- it("gives a custom judge the configured model and credentials", async () => {
561
- const seen: Array<{ prompt: string; model?: unknown }> = [];
562
- const [result] = await runCase(
563
- defineEval({
564
- async test(t) {
565
- await t.send("weather?");
566
- // The shape a domain judge uses: one raw call, then score the parse.
567
- const reply = await t.judge.model("Grade these findings: [...]");
568
- t.score("recall", JSON.parse(reply).recall as number).atLeast(0.6);
569
- },
570
- }),
571
- {
572
- judgeModel: "gpt-5.4-mini",
573
- judgeCaller: async (args: { prompt: string; model?: unknown }) => {
574
- seen.push(args);
575
- return '{"recall":0.8}';
576
- },
577
- }
578
- );
579
- expect(seen).toEqual([
580
- { prompt: "Grade these findings: [...]", model: "gpt-5.4-mini" },
581
- ]);
582
- expect(result?.verdict).toBe("passed");
583
- expect(result?.assertions[0]).toMatchObject({
584
- name: "score(recall)",
585
- score: 0.8,
586
- threshold: 0.6,
587
- passed: true,
588
- });
589
- });
590
-
591
- it("throws into the test body when no judge model is configured", async () => {
592
- const [result] = await runCase(
593
- defineEval({
594
- async test(t) {
595
- await t.send("weather?");
596
- await t.judge.model("grade this");
597
- },
598
- })
599
- );
600
- expect(result?.verdict).toBe("failed");
601
- expect(result?.error).toMatch(/no judge model configured/);
602
- });
603
- });
604
-
605
- describe("reporters", () => {
606
- function recordingReporter(): {
607
- reporter: EvalReporter;
608
- calls: string[];
609
- } {
610
- const calls: string[] = [];
611
- return {
612
- calls,
613
- reporter: {
614
- onRunStart: (evals, target) => {
615
- calls.push(
616
- `start:${evals.map((e) => e.id).join(",")}:${target.mode}`
617
- );
618
- },
619
- onEvalComplete: (result) => {
620
- calls.push(`case:${result.id}:${result.verdict}`);
621
- },
622
- onRunComplete: (summary) => {
623
- calls.push(
624
- `complete:${summary.passed}p/${summary.failed}f/${summary.scored}s`
625
- );
626
- },
627
- },
628
- };
629
- }
630
-
631
- const twoCases = defineEval({
632
- cases: [
633
- {
634
- id: "good",
635
- async test(t) {
636
- await t.send("weather?");
637
- t.succeeded();
638
- },
639
- },
640
- {
641
- id: "bad",
642
- async test(t) {
643
- await t.send("weather?");
644
- t.calledTool("missing");
645
- },
646
- },
647
- ],
648
- });
649
-
650
- it("drives the full lifecycle for a CLI reporter", async () => {
651
- const { reporter, calls } = recordingReporter();
652
- await runCase(twoCases, { reporters: [reporter] });
653
- expect(calls).toEqual([
654
- "start:smoke/good,smoke/bad:local",
655
- "case:smoke/good:passed",
656
- "case:smoke/bad:failed",
657
- "complete:1p/1f/0s",
658
- ]);
659
- });
660
-
661
- it("observes every case from a config reporter", async () => {
662
- const { reporter, calls } = recordingReporter();
663
- await runCase(twoCases, { configReporters: [reporter] });
664
- expect(calls.filter((c) => c.startsWith("case:"))).toEqual([
665
- "case:smoke/good:passed",
666
- "case:smoke/bad:failed",
667
- ]);
668
- });
669
-
670
- it("scopes a per-eval reporter to its own case", async () => {
671
- const { reporter, calls } = recordingReporter();
672
- await runCase(
673
- defineEval({
674
- cases: [
675
- {
676
- id: "watched",
677
- reporters: [reporter],
678
- async test(t) {
679
- await t.send("weather?");
680
- t.succeeded();
681
- },
682
- },
683
- {
684
- id: "unwatched",
685
- async test(t) {
686
- await t.send("weather?");
687
- t.succeeded();
688
- },
689
- },
690
- ],
691
- })
692
- );
693
- // Run hooks still fire, but only the referencing case reports a result.
694
- expect(calls).toEqual([
695
- "start:smoke/watched,smoke/unwatched:local",
696
- "case:smoke/watched:passed",
697
- "complete:2p/0f/0s",
698
- ]);
699
- });
700
-
701
- it("suppresses config and per-eval reporters under skipReport", async () => {
702
- const config = recordingReporter();
703
- const perEval = recordingReporter();
704
- const cli = recordingReporter();
705
- await runCase(
706
- defineEval({
707
- reporters: [perEval.reporter],
708
- async test(t) {
709
- await t.send("weather?");
710
- t.succeeded();
711
- },
712
- }),
713
- {
714
- configReporters: [config.reporter],
715
- reporters: [cli.reporter],
716
- skipReport: true,
717
- }
718
- );
719
- expect(config.calls).toEqual([]);
720
- expect(perEval.calls).toEqual([]);
721
- // CLI-attached reporters are explicit, so they still run.
722
- expect(cli.calls).toContain("case:smoke:passed");
723
- });
724
-
725
- it("reports strict mode on the summary so reporters can gate on it", async () => {
726
- let sawStrict: boolean | undefined;
727
- await runCase(
728
- defineEval({
729
- async test(t) {
730
- await t.send("weather?");
731
- t.succeeded();
732
- },
733
- }),
734
- {
735
- strict: true,
736
- reporters: [
737
- {
738
- onRunComplete: (summary) => {
739
- sawStrict = summary.strict;
740
- },
741
- },
742
- ],
743
- }
744
- );
745
- expect(sawStrict).toBe(true);
746
- });
747
-
748
- it("keeps running when a reporter throws", async () => {
749
- const results = await runCase(twoCases, {
750
- reporters: [
751
- {
752
- onEvalComplete: () => {
753
- throw new Error("upload failed");
754
- },
755
- },
756
- ],
757
- });
758
- expect(results.map((r) => r.verdict)).toEqual(["passed", "failed"]);
759
- });
760
- });
761
-
762
- describe("metadata and metrics", () => {
763
- it("carries eval metadata and tags onto the result", async () => {
764
- const [result] = await runCase(
765
- defineEval({
766
- tags: ["smoke"],
767
- metadata: { owner: "agents", suite: "weather" },
768
- async test(t) {
769
- await t.send("weather?");
770
- t.succeeded();
771
- t.metric("recall", "40.0% (4/10)");
772
- },
773
- })
774
- );
775
- expect(result).toMatchObject({
776
- tags: ["smoke"],
777
- metadata: { owner: "agents", suite: "weather" },
778
- metrics: { recall: "40.0% (4/10)" },
779
- });
780
- });
781
-
782
- it("lets a case-level metadata key override the file-level one", async () => {
783
- const [result] = await runCase(
784
- defineEval({
785
- metadata: { owner: "agents", suite: "weather" },
786
- cases: [
787
- {
788
- id: "override",
789
- metadata: { owner: "security" },
790
- async test(t) {
791
- await t.send("weather?");
792
- t.succeeded();
793
- },
794
- },
795
- ],
796
- })
797
- );
798
- expect(result?.metadata).toEqual({ owner: "security", suite: "weather" });
799
- });
800
- });
801
-
802
- describe("expandEvalModule", () => {
803
- it("expands a single defineEval to the file id", () => {
804
- const expanded = expandEvalModule(
805
- "weather",
806
- defineEval({ test: async () => undefined })
807
- );
808
- expect(expanded.map((e) => e.id)).toEqual(["weather"]);
809
- });
810
-
811
- it("fans a default-exported array out with zero-padded ids", () => {
812
- const rows = ["a", "b", "c"];
813
- const expanded = expandEvalModule(
814
- "sql",
815
- rows.map((row) =>
816
- defineEval({ description: row, test: async () => undefined })
817
- ) as never
818
- );
819
- expect(expanded.map((e) => e.id)).toEqual([
820
- "sql/0000",
821
- "sql/0001",
822
- "sql/0002",
823
- ]);
824
- expect(expanded[1]?.definition.description).toBe("b");
825
- });
826
-
827
- it("rejects a non-eval default export", () => {
828
- expect(() => expandEvalModule("x", { nope: true })).toThrow(
829
- /must be defineEval\(\{ \.\.\. \}\) or an array of them/
830
- );
831
- expect(() => expandEvalModule("x", [])).toThrow(/array is empty/);
832
- expect(() => expandEvalModule("x", [{ nope: true }])).toThrow(
833
- /array entry 0 must be defineEval/
834
- );
835
- });
836
- });
837
-
838
- describe("discoverEvals", () => {
839
- let projectRoot: string;
840
-
841
- beforeEach(async () => {
842
- projectRoot = await mkdtemp(join(tmpdir(), "agentkit-eval-discovery-"));
843
- await mkdir(join(projectRoot, "evals", "weather"), { recursive: true });
844
- });
845
-
846
- afterEach(async () => {
847
- await rm(projectRoot, { recursive: true, force: true });
848
- });
849
-
850
- /** Map on-disk eval paths to in-memory definitions. */
851
- function importer(
852
- modules: Record<string, unknown>
853
- ): (path: string) => Promise<Record<string, unknown>> {
854
- return async (absolutePath: string) => {
855
- for (const [suffix, mod] of Object.entries(modules)) {
856
- if (absolutePath.endsWith(suffix)) {
857
- return { default: mod };
858
- }
859
- }
860
- throw new Error(`unexpected import: ${absolutePath}`);
861
- };
862
- }
863
-
864
- it("derives ids from paths and loads the config", async () => {
865
- await writeFile(join(projectRoot, "evals/evals.config.ts"), "", "utf8");
866
- await writeFile(join(projectRoot, "evals/smoke.eval.ts"), "", "utf8");
867
- await writeFile(join(projectRoot, "evals/weather/nyc.eval.ts"), "", "utf8");
868
- // A sibling helper that is not an eval file must be ignored.
869
- await writeFile(join(projectRoot, "evals/weather/shared.ts"), "", "utf8");
870
-
871
- const { evals, config } = await discoverEvals(
872
- projectRoot,
873
- importer({
874
- "evals.config.ts": defineEvalConfig({ maxConcurrency: 7 }),
875
- "smoke.eval.ts": defineEval({ test: async () => undefined }),
876
- "nyc.eval.ts": defineEval({ test: async () => undefined }),
877
- })
878
- );
879
- expect(evals.map((e) => e.id)).toEqual(["smoke", "weather/nyc"]);
880
- expect(config.maxConcurrency).toBe(7);
881
- });
882
-
883
- it("points fixture loaders at the app root", async () => {
884
- await writeFile(join(projectRoot, "evals/evals.config.ts"), "", "utf8");
885
- await discoverEvals(
886
- projectRoot,
887
- importer({ "evals.config.ts": defineEvalConfig({ maxConcurrency: 1 }) })
888
- );
889
- expect(process.env[EVAL_ROOT_ENV]).toBe(projectRoot);
890
- });
891
-
892
- it("prefixes the failing file on a bad default export", async () => {
893
- await writeFile(join(projectRoot, "evals/broken.eval.ts"), "", "utf8");
894
- await expect(
895
- discoverEvals(projectRoot, importer({ "broken.eval.ts": { nope: true } }))
896
- ).rejects.toThrow(/broken\.eval\.ts: default export must be defineEval/);
897
- });
898
- });
899
-
900
- describe("cancellation", () => {
901
- it("returns a cancelled result without driving the agent", async () => {
902
- const controller = new AbortController();
903
- controller.abort();
904
- const results = await runDiscoveredEvals({
905
- projectRoot: "/unused",
906
- baseUrl: "http://127.0.0.1:3000",
907
- signal: controller.signal,
908
- discovered: {
909
- evals: expandEvalModule(
910
- "smoke",
911
- defineEval({
912
- async test(t: EvalTestContext) {
913
- await t.send("weather?");
914
- },
915
- })
916
- ).map((e) => ({
917
- id: e.id,
918
- fileId: "smoke",
919
- path: "/repo/evals/smoke.eval.ts",
920
- definition: e.definition,
921
- })),
922
- config: { maxConcurrency: 1 },
923
- },
924
- });
925
- expect(results).toEqual([]);
926
- expect(runSession).not.toHaveBeenCalled();
927
- });
928
- });