@langwatch/mcp-server 2.0.0 → 2.1.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 (177) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +31 -3
  3. package/dist/{archive-project-4EQGLBJJ.js → archive-project-W3IKI7UL.js} +3 -3
  4. package/dist/archive-run-plan-474OKHHU.js +16 -0
  5. package/dist/{archive-scenario-W6BXYDZ3.js → archive-scenario-V5UNYK26.js} +3 -3
  6. package/dist/archive-test-suite-O7FLDMXK.js +16 -0
  7. package/dist/{assign-prompt-tag-VLXUWEKU.js → assign-prompt-tag-Q46WRB6P.js} +2 -2
  8. package/dist/chunk-56OGTS24.js +44 -0
  9. package/dist/chunk-6DJDEXET.js +63 -0
  10. package/dist/chunk-7E4FZIYJ.js +15 -0
  11. package/dist/{chunk-77BNWQ3U.js → chunk-AN4OUA4D.js} +2 -2
  12. package/dist/chunk-CKNAH5TG.js +74 -0
  13. package/dist/{chunk-TOXJQDIZ.js → chunk-EYXMLE5X.js} +85 -10
  14. package/dist/{chunk-7RHG56OH.js → chunk-G4ZAZTZW.js} +2 -2
  15. package/dist/{chunk-A2LEVYQ5.js → chunk-GELJ2MQR.js} +11 -2
  16. package/dist/chunk-HV4DWKBI.js +46 -0
  17. package/dist/chunk-JR6DJZOU.js +49 -0
  18. package/dist/{chunk-SFB4MU6H.js → chunk-JTQJRNFJ.js} +2 -2
  19. package/dist/{chunk-TJCXFNDT.js → chunk-K37ZNPXK.js} +2 -2
  20. package/dist/{chunk-CDYQOY5U.js → chunk-OJLYYJWI.js} +2 -2
  21. package/dist/{chunk-ZWIL4EFL.js → chunk-QAMP3CGR.js} +2 -2
  22. package/dist/{chunk-4QC7SA2L.js → chunk-QRO35SOM.js} +345 -144
  23. package/dist/{chunk-J42FWRJ2.js → chunk-RVAE2MEA.js} +2 -2
  24. package/dist/{chunk-AQJO2EI5.js → chunk-S4GVX36W.js} +2 -2
  25. package/dist/{chunk-YX3A3CFM.js → chunk-SR6QPRIE.js} +2 -2
  26. package/dist/chunk-UF6XIGYR.js +58 -0
  27. package/dist/{chunk-5CCZFUXS.js → chunk-UFOYFYI7.js} +2 -2
  28. package/dist/{create-agent-7XHO6UIS.js → create-agent-JFJ2K6X3.js} +3 -3
  29. package/dist/{create-annotation-YQXRQC6A.js → create-annotation-EUXXQ6NS.js} +3 -3
  30. package/dist/{create-api-key-YUS36KBU.js → create-api-key-BGBB76JX.js} +3 -3
  31. package/dist/{create-dashboard-4Z5JO5LB.js → create-dashboard-Z76AIPLS.js} +3 -3
  32. package/dist/{create-dataset-E6NMSVX7.js → create-dataset-FD5VAXU4.js} +3 -3
  33. package/dist/{create-dataset-records-SKHPRD2K.js → create-dataset-records-WQUXXOHI.js} +3 -3
  34. package/dist/{create-evaluator-JXF5PF33.js → create-evaluator-6NV33Z52.js} +3 -3
  35. package/dist/create-mcp-server.js +7 -2
  36. package/dist/{create-project-CS2CB57Y.js → create-project-ZQ7TFIO2.js} +3 -3
  37. package/dist/{create-prompt-F5RMX3L3.js → create-prompt-BS6QRCUU.js} +2 -2
  38. package/dist/{create-prompt-tag-GMRWTEFR.js → create-prompt-tag-FCQ5ELSQ.js} +2 -2
  39. package/dist/{create-scenario-JDR2VB2H.js → create-scenario-DRPTPXU3.js} +10 -3
  40. package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
  41. package/dist/{delete-agent-O3EEH4S5.js → delete-agent-SA4JMUZF.js} +3 -3
  42. package/dist/{delete-annotation-XP6FQM7A.js → delete-annotation-X3KKK7QY.js} +3 -3
  43. package/dist/{delete-dashboard-RFUNYCNP.js → delete-dashboard-LYMRPF27.js} +3 -3
  44. package/dist/{delete-dataset-3MJFRDLP.js → delete-dataset-W6EZ44HY.js} +3 -3
  45. package/dist/{delete-dataset-records-WPMU5GL5.js → delete-dataset-records-7CNOTALB.js} +3 -3
  46. package/dist/{delete-evaluator-33SP2JFX.js → delete-evaluator-RKYC4TNB.js} +3 -3
  47. package/dist/{delete-prompt-tag-KNMZVSPB.js → delete-prompt-tag-QB3OCZUV.js} +2 -2
  48. package/dist/{delete-workflow-3NUXO46U.js → delete-workflow-NG2VXYYM.js} +3 -3
  49. package/dist/{discover-evaluator-schema-XTAD4X7D.js → discover-evaluator-schema-ESSHQGPY.js} +7 -286
  50. package/dist/{discover-scenario-schema-FE64QK2D.js → discover-scenario-schema-W7KBXA4J.js} +53 -1
  51. package/dist/get-agent-7THF7HCU.js +66 -0
  52. package/dist/{get-analytics-TETCSOK6.js → get-analytics-R6AZD3AW.js} +2 -2
  53. package/dist/{get-dashboard-KHBEGZGL.js → get-dashboard-P4WWVHFU.js} +3 -3
  54. package/dist/{get-dataset-RCMU5VAH.js → get-dataset-AKTYSQDI.js} +3 -3
  55. package/dist/{get-evaluator-ADMKVWYZ.js → get-evaluator-RZ44UIN6.js} +3 -3
  56. package/dist/{get-project-RAK7URQJ.js → get-project-QSQEAB33.js} +3 -3
  57. package/dist/get-prompt-GVKXF5NL.js +100 -0
  58. package/dist/get-run-plan-NY7J5GRE.js +61 -0
  59. package/dist/{get-scenario-ZTAJJL5T.js → get-scenario-Q3LLQE4T.js} +7 -3
  60. package/dist/{get-simulation-run-T2ZQWYH2.js → get-simulation-run-WINEYDMK.js} +12 -3
  61. package/dist/get-test-suite-4HKA7OKS.js +49 -0
  62. package/dist/{get-trace-K7U234Q4.js → get-trace-RCUV7N4O.js} +2 -2
  63. package/dist/{get-workflow-L5L2PSK3.js → get-workflow-EEOC7Y45.js} +3 -3
  64. package/dist/{http-server-YXHFPPGA.js → http-server-WCDUARTH.js} +8 -3
  65. package/dist/index.js +3366 -1910
  66. package/dist/{langwatch-api-annotations-FDAS6NQ7.js → langwatch-api-annotations-6DHVZWUK.js} +3 -3
  67. package/dist/{langwatch-api-dashboards-JTVCS7ZJ.js → langwatch-api-dashboards-2KJL2M42.js} +3 -3
  68. package/dist/{langwatch-api-monitors-3J7UIQME.js → langwatch-api-monitors-6ISFJ7WX.js} +2 -2
  69. package/dist/{langwatch-api-secrets-BUBSNCNO.js → langwatch-api-secrets-OEBD6GBU.js} +2 -2
  70. package/dist/{langwatch-api-triggers-EDFUCJN6.js → langwatch-api-triggers-5E6DDGX6.js} +2 -2
  71. package/dist/{langwatch-api-workflows-NBJX5KOG.js → langwatch-api-workflows-6UJL5U4Z.js} +3 -3
  72. package/dist/list-agents-AMVDQPQL.js +43 -0
  73. package/dist/{list-annotations-ZTVP4EEL.js → list-annotations-6PR5NLIC.js} +3 -3
  74. package/dist/{list-api-keys-CWD4UQT5.js → list-api-keys-GVPNBIHQ.js} +3 -3
  75. package/dist/{list-dashboards-PWG6P6KR.js → list-dashboards-Z3BZKDGF.js} +3 -3
  76. package/dist/{list-dataset-records-IOE7LQXS.js → list-dataset-records-O67HZ35Y.js} +3 -3
  77. package/dist/{list-datasets-ZT4JYUDT.js → list-datasets-WXZL6UMP.js} +3 -3
  78. package/dist/{list-evaluators-SJHC7VBH.js → list-evaluators-PAX72WCN.js} +3 -3
  79. package/dist/{list-model-providers-PPD2WPD2.js → list-model-providers-VVLZMLCX.js} +3 -3
  80. package/dist/{list-projects-K7QAIX2I.js → list-projects-AQVI6SUK.js} +3 -3
  81. package/dist/{list-prompt-tags-FOO6TNHB.js → list-prompt-tags-WZFBJOHG.js} +2 -2
  82. package/dist/{list-prompts-SC2HAUFG.js → list-prompts-BQOHRHYT.js} +2 -2
  83. package/dist/list-run-plans-UBAU2DGH.js +46 -0
  84. package/dist/{list-scenarios-F2WYNRIE.js → list-scenarios-MS4IUIVM.js} +8 -5
  85. package/dist/{list-simulation-runs-KF5THG5H.js → list-simulation-runs-D6YF4ZOU.js} +11 -5
  86. package/dist/list-test-suites-L7R2O6DR.js +35 -0
  87. package/dist/{list-workflows-QSJWURCK.js → list-workflows-6MV4XJHF.js} +3 -3
  88. package/dist/{rename-prompt-tag-EKWU5QF3.js → rename-prompt-tag-K5LQ4CY6.js} +2 -2
  89. package/dist/rename-test-suite-NTYHAO5X.js +21 -0
  90. package/dist/report-issue-RHZZ33IS.js +885 -0
  91. package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
  92. package/dist/{revoke-api-key-FTRTZMOF.js → revoke-api-key-6LAZ3QZZ.js} +3 -3
  93. package/dist/run-agent-CAGWGTYT.js +72 -0
  94. package/dist/run-plan-T4ZYOZGA.js +39 -0
  95. package/dist/run-test-suite-L4MZ7G4D.js +26 -0
  96. package/dist/{run-workflow-QPXC627O.js → run-workflow-DFIS23AP.js} +3 -3
  97. package/dist/{search-traces-A6HFKPLJ.js → search-traces-4XTJYXIK.js} +2 -2
  98. package/dist/{set-model-provider-3WKT63ZN.js → set-model-provider-XRNMFOVR.js} +3 -3
  99. package/dist/{update-agent-SMAEHLXH.js → update-agent-DUWNYBNA.js} +3 -3
  100. package/dist/{update-dataset-BDUK7TUO.js → update-dataset-YSOMDCOB.js} +3 -3
  101. package/dist/{update-dataset-record-4ZSDRBSW.js → update-dataset-record-DEA2ITYR.js} +3 -3
  102. package/dist/{update-evaluator-LS6TIBY7.js → update-evaluator-NH7Q5GWV.js} +3 -3
  103. package/dist/{update-project-LKVYXENH.js → update-project-Q26FBSYH.js} +3 -3
  104. package/dist/update-prompt-ZQQ2C3KU.js +143 -0
  105. package/dist/{update-scenario-NEXCAQI7.js → update-scenario-WUX34JAR.js} +10 -3
  106. package/package.json +8 -9
  107. package/pyproject.toml +4 -4
  108. package/src/__tests__/agent-tools.unit.test.ts +257 -0
  109. package/src/__tests__/all-tools.integration.test.ts +321 -75
  110. package/src/__tests__/integration.integration.test.ts +7 -11
  111. package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
  112. package/src/__tests__/prompts.integration.test.ts +736 -0
  113. package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
  114. package/src/__tests__/scenario-tools.unit.test.ts +19 -0
  115. package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
  116. package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
  117. package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
  118. package/src/create-mcp-server.ts +334 -55
  119. package/src/langwatch-api-agents.ts +196 -12
  120. package/src/langwatch-api-run-plans.ts +157 -0
  121. package/src/langwatch-api-scenarios.ts +9 -0
  122. package/src/langwatch-api-simulation-runs.ts +5 -0
  123. package/src/langwatch-api-test-suites.ts +135 -0
  124. package/src/langwatch-api.ts +62 -12
  125. package/src/schemas/index.ts +5 -0
  126. package/src/schemas/run-plan.ts +92 -0
  127. package/src/schemas/suite-fields.ts +149 -0
  128. package/src/tools/archive-run-plan.ts +12 -0
  129. package/src/tools/archive-test-suite.ts +12 -0
  130. package/src/tools/create-scenario.ts +8 -0
  131. package/src/tools/create-test-suite.ts +43 -0
  132. package/src/tools/discover-scenario-schema.ts +54 -0
  133. package/src/tools/format-run-plan.ts +68 -0
  134. package/src/tools/format-scenario.ts +14 -0
  135. package/src/tools/format-suite-details.ts +96 -0
  136. package/src/tools/get-agent.ts +45 -3
  137. package/src/tools/get-prompt.ts +108 -24
  138. package/src/tools/get-run-plan.ts +62 -0
  139. package/src/tools/get-scenario.ts +3 -0
  140. package/src/tools/get-simulation-run.ts +9 -0
  141. package/src/tools/get-test-suite.ts +49 -0
  142. package/src/tools/list-agents.ts +20 -3
  143. package/src/tools/list-run-plans.ts +47 -0
  144. package/src/tools/list-scenarios.ts +12 -2
  145. package/src/tools/list-simulation-runs.ts +6 -2
  146. package/src/tools/list-test-suites.ts +35 -0
  147. package/src/tools/rename-test-suite.ts +18 -0
  148. package/src/tools/rerun-run-plan.ts +21 -0
  149. package/src/tools/run-agent.ts +54 -6
  150. package/src/tools/run-plan.ts +49 -0
  151. package/src/tools/run-test-suite.ts +30 -0
  152. package/src/tools/test-agent.ts +18 -0
  153. package/src/tools/update-prompt.ts +195 -10
  154. package/src/tools/update-scenario.ts +8 -0
  155. package/src/tools/update-test-suite.ts +45 -0
  156. package/tsconfig.json +5 -0
  157. package/uv.lock +416 -296
  158. package/dist/archive-suite-4T6BSRS7.js +0 -16
  159. package/dist/chunk-YZLYHLLI.js +0 -48
  160. package/dist/create-suite-LZS6QIAF.js +0 -39
  161. package/dist/get-agent-Y7MWZ65F.js +0 -29
  162. package/dist/get-prompt-OY5B7TRI.js +0 -46
  163. package/dist/get-suite-J7JVWO6O.js +0 -45
  164. package/dist/list-agents-KFHKGPXV.js +0 -33
  165. package/dist/list-suites-CRY4VWW6.js +0 -43
  166. package/dist/report-issue-XYVAPAYJ.js +0 -412
  167. package/dist/run-agent-6SM4XNA7.js +0 -37
  168. package/dist/run-suite-NKZCJ2IN.js +0 -30
  169. package/dist/update-prompt-ODCRQRN6.js +0 -25
  170. package/dist/update-suite-SNO3GS7T.js +0 -40
  171. package/src/langwatch-api-suites.ts +0 -118
  172. package/src/tools/archive-suite.ts +0 -12
  173. package/src/tools/create-suite.ts +0 -42
  174. package/src/tools/get-suite.ts +0 -47
  175. package/src/tools/list-suites.ts +0 -43
  176. package/src/tools/run-suite.ts +0 -28
  177. package/src/tools/update-suite.ts +0 -44
@@ -0,0 +1,565 @@
1
+ /**
2
+ * Digests of the run plan tools.
3
+ *
4
+ * @see specs/mcp-server/run-plan-tools.feature
5
+ */
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+
8
+ vi.mock("../langwatch-api-run-plans.js", () => ({
9
+ listRunPlans: vi.fn(),
10
+ getRunPlan: vi.fn(),
11
+ runRunPlan: vi.fn(),
12
+ rerunRunPlan: vi.fn(),
13
+ archiveRunPlan: vi.fn(),
14
+ }));
15
+
16
+ import {
17
+ archiveRunPlan,
18
+ getRunPlan,
19
+ listRunPlans,
20
+ rerunRunPlan,
21
+ runRunPlan,
22
+ type RunPlan,
23
+ type RunPlanRunResult,
24
+ } from "../langwatch-api-run-plans.js";
25
+
26
+ import { handleArchiveRunPlan } from "../tools/archive-run-plan.js";
27
+ import { handleGetRunPlan } from "../tools/get-run-plan.js";
28
+ import { handleListRunPlans } from "../tools/list-run-plans.js";
29
+ import { handleRerunRunPlan } from "../tools/rerun-run-plan.js";
30
+ import { handleRunPlan } from "../tools/run-plan.js";
31
+
32
+ const mockListRunPlans = vi.mocked(listRunPlans);
33
+ const mockGetRunPlan = vi.mocked(getRunPlan);
34
+ const mockRunRunPlan = vi.mocked(runRunPlan);
35
+ const mockRerunRunPlan = vi.mocked(rerunRunPlan);
36
+ const mockArchiveRunPlan = vi.mocked(archiveRunPlan);
37
+
38
+ const samplePlan: RunPlan = {
39
+ id: "plan_abc123",
40
+ name: "Regression Plan",
41
+ slug: "regression-plan",
42
+ scope: { mode: "labels", labels: ["auth", "checkout"] },
43
+ scenarioIds: ["scen_abc123"],
44
+ targets: [{ type: "http", referenceId: "agent_abc" }],
45
+ repeatCount: 3,
46
+ simulatorModel: "openai/gpt-5-mini",
47
+ judgeModel: "openai/gpt-5-mini",
48
+ labels: ["nightly"],
49
+ archivedAt: null,
50
+ createdAt: "2024-01-01T00:00:00Z",
51
+ updatedAt: "2024-01-02T00:00:00Z",
52
+ platformUrl: "https://app.langwatch.ai/proj/agent-testing/results/regression-plan",
53
+ };
54
+
55
+ const sampleRun: RunPlanRunResult = {
56
+ scheduled: true,
57
+ batchRunId: "batch_123",
58
+ setId: "set_456",
59
+ jobCount: 6,
60
+ skippedArchived: { scenarios: [], targets: [] },
61
+ items: [],
62
+ runPlanId: "plan_abc123",
63
+ planName: "Regression Plan",
64
+ created: true,
65
+ platformUrl: "https://app.langwatch.ai/proj/agent-testing/results/regression-plan",
66
+ };
67
+
68
+ const runInput = {
69
+ scope: { mode: "labels" as const, labels: ["auth"] },
70
+ targets: [{ type: "http" as const, referenceId: "agent_abc" }],
71
+ };
72
+
73
+ beforeEach(() => {
74
+ vi.clearAllMocks();
75
+ });
76
+
77
+ describe("handleRunPlan()", () => {
78
+ describe("when the name no plan carries yet", () => {
79
+ let result: string;
80
+
81
+ beforeEach(async () => {
82
+ mockRunRunPlan.mockResolvedValue({ ...sampleRun, created: true });
83
+ result = await handleRunPlan({ name: "Regression Plan", ...runInput });
84
+ });
85
+
86
+ /** @scenario "Agent runs a name that no plan carries yet" */
87
+ it("says the plan was created and started", () => {
88
+ expect(result).toContain(
89
+ 'Run plan "Regression Plan" created and started.',
90
+ );
91
+ });
92
+
93
+ /** @scenario "Agent reads the batch a run started" */
94
+ it("names the plan, the batch and the job count", () => {
95
+ expect(result).toContain("**Plan**: Regression Plan (plan_abc123)");
96
+ expect(result).toContain("**Batch Run ID**: batch_123");
97
+ expect(result).toContain("**Jobs**: 6");
98
+ });
99
+
100
+ it("includes the platform URL of the batch", () => {
101
+ expect(result).toContain(
102
+ "**View**: https://app.langwatch.ai/proj/agent-testing/results/regression-plan",
103
+ );
104
+ });
105
+
106
+ it("sends the configuration under config and the note beside it", async () => {
107
+ await handleRunPlan({
108
+ name: "Regression Plan",
109
+ ...runInput,
110
+ repeatCount: 2,
111
+ note: "nightly regression",
112
+ });
113
+
114
+ expect(mockRunRunPlan).toHaveBeenLastCalledWith({
115
+ name: "Regression Plan",
116
+ config: {
117
+ scope: { mode: "labels", labels: ["auth"] },
118
+ targets: [{ type: "http", referenceId: "agent_abc" }],
119
+ repeatCount: 2,
120
+ simulatorModel: undefined,
121
+ judgeModel: undefined,
122
+ scenarioIds: undefined,
123
+ },
124
+ idempotencyKey: undefined,
125
+ parameters: undefined,
126
+ note: "nightly regression",
127
+ });
128
+ });
129
+ });
130
+
131
+ describe("when a plan already carries the name", () => {
132
+ /** @scenario "Agent runs a name an existing plan carries" */
133
+ it("says the plan ran with the configuration of this run", async () => {
134
+ mockRunRunPlan.mockResolvedValue({ ...sampleRun, created: false });
135
+
136
+ const result = await handleRunPlan({
137
+ name: "Regression Plan",
138
+ ...runInput,
139
+ });
140
+
141
+ expect(result).toContain(
142
+ 'Run plan "Regression Plan" started with the configuration of this run.',
143
+ );
144
+ });
145
+ });
146
+
147
+ describe("when two targets name the same agent with different parameters", () => {
148
+ /** @scenario "Agent compares one agent on two models in one run" */
149
+ it("sends both targets, each carrying its own runParameters", async () => {
150
+ await handleRunPlan({
151
+ scope: { mode: "labels", labels: ["auth"] },
152
+ targets: [
153
+ {
154
+ type: "http",
155
+ referenceId: "agent_abc",
156
+ parameters: { model: "gpt-5" },
157
+ },
158
+ {
159
+ type: "http",
160
+ referenceId: "agent_abc",
161
+ parameters: { model: "gpt-5-mini" },
162
+ },
163
+ ],
164
+ });
165
+
166
+ expect(mockRunRunPlan).toHaveBeenLastCalledWith(
167
+ expect.objectContaining({
168
+ config: expect.objectContaining({
169
+ targets: [
170
+ {
171
+ type: "http",
172
+ referenceId: "agent_abc",
173
+ runParameters: { model: "gpt-5" },
174
+ },
175
+ {
176
+ type: "http",
177
+ referenceId: "agent_abc",
178
+ runParameters: { model: "gpt-5-mini" },
179
+ },
180
+ ],
181
+ }),
182
+ }),
183
+ );
184
+ });
185
+
186
+ /** @scenario "Agent compares one agent on two models in one run" */
187
+ it("keeps the run-level parameters, which a target overrides for itself", async () => {
188
+ await handleRunPlan({
189
+ ...runInput,
190
+ targets: [
191
+ {
192
+ type: "http",
193
+ referenceId: "agent_abc",
194
+ parameters: { model: "gpt-5" },
195
+ },
196
+ ],
197
+ parameters: { model: "gpt-5-mini", account_tier: "gold" },
198
+ });
199
+
200
+ expect(mockRunRunPlan).toHaveBeenLastCalledWith(
201
+ expect.objectContaining({
202
+ parameters: { model: "gpt-5-mini", account_tier: "gold" },
203
+ config: expect.objectContaining({
204
+ targets: [
205
+ {
206
+ type: "http",
207
+ referenceId: "agent_abc",
208
+ runParameters: { model: "gpt-5" },
209
+ },
210
+ ],
211
+ }),
212
+ }),
213
+ );
214
+ });
215
+
216
+ it("leaves runParameters out of a target that names none", async () => {
217
+ await handleRunPlan(runInput);
218
+
219
+ expect(mockRunRunPlan).toHaveBeenLastCalledWith(
220
+ expect.objectContaining({
221
+ config: expect.objectContaining({
222
+ targets: [{ type: "http", referenceId: "agent_abc" }],
223
+ }),
224
+ }),
225
+ );
226
+ });
227
+ });
228
+
229
+ describe("when the run skipped archived rows", () => {
230
+ /** @scenario "A run reports what it skipped as archived" */
231
+ it("lists the skipped scenarios and the skipped targets", async () => {
232
+ mockRunRunPlan.mockResolvedValue({
233
+ ...sampleRun,
234
+ skippedArchived: {
235
+ scenarios: ["scen_gone"],
236
+ targets: ["agent_gone"],
237
+ },
238
+ });
239
+
240
+ const result = await handleRunPlan(runInput);
241
+
242
+ expect(result).toContain("**Skipped archived scenarios**: scen_gone");
243
+ expect(result).toContain("**Skipped archived targets**: agent_gone");
244
+ });
245
+ });
246
+ });
247
+
248
+ describe("handleListRunPlans()", () => {
249
+ describe("when run plans exist", () => {
250
+ let result: string;
251
+
252
+ beforeEach(async () => {
253
+ mockListRunPlans.mockResolvedValue([samplePlan]);
254
+ result = await handleListRunPlans({});
255
+ });
256
+
257
+ /** @scenario "Agent lists the run plans of a project" */
258
+ it("lists each plan with what it covers", () => {
259
+ expect(result).toContain("# Run Plans (1 total)");
260
+ expect(result).toContain("## Regression Plan");
261
+ expect(result).toContain("**Covers**: labels: auth, checkout");
262
+ });
263
+
264
+ it("includes the targets and the repeat count", () => {
265
+ expect(result).toContain("**Targets**: 1 (http:agent_abc)");
266
+ expect(result).toContain("**Repeat**: 3x");
267
+ });
268
+
269
+ it("asks the API for non-archived plans by default", () => {
270
+ expect(mockListRunPlans).toHaveBeenCalledWith({
271
+ includeArchived: undefined,
272
+ });
273
+ });
274
+ });
275
+
276
+ describe("when includeArchived is set", () => {
277
+ it("marks an archived plan as archived", async () => {
278
+ mockListRunPlans.mockResolvedValue([
279
+ { ...samplePlan, archivedAt: "2024-02-01T00:00:00Z" },
280
+ ]);
281
+
282
+ const result = await handleListRunPlans({ includeArchived: true });
283
+
284
+ expect(result).toContain("## Regression Plan (archived)");
285
+ expect(mockListRunPlans).toHaveBeenCalledWith({ includeArchived: true });
286
+ });
287
+ });
288
+
289
+ describe("when no run plans exist", () => {
290
+ let result: string;
291
+
292
+ beforeEach(async () => {
293
+ mockListRunPlans.mockResolvedValue([]);
294
+ result = await handleListRunPlans({});
295
+ });
296
+
297
+ /** @scenario "Agent lists run plans when none exist" */
298
+ it("returns a no-plans message", () => {
299
+ expect(result).toContain("No run plans found");
300
+ });
301
+
302
+ it("includes a tip to use platform_run_plan", () => {
303
+ expect(result).toContain("platform_run_plan");
304
+ });
305
+ });
306
+
307
+ describe("when format is json", () => {
308
+ it("returns valid parseable JSON matching the plan structure", async () => {
309
+ mockListRunPlans.mockResolvedValue([samplePlan]);
310
+
311
+ const result = await handleListRunPlans({ format: "json" });
312
+
313
+ expect(JSON.parse(result)).toEqual([samplePlan]);
314
+ });
315
+ });
316
+ });
317
+
318
+ describe("handleGetRunPlan()", () => {
319
+ describe("when the plan names labels, targets and models", () => {
320
+ let result: string;
321
+
322
+ beforeEach(async () => {
323
+ mockGetRunPlan.mockResolvedValue(samplePlan);
324
+ result = await handleGetRunPlan({ id: "plan_abc123" });
325
+ });
326
+
327
+ /** @scenario "Agent reads the full configuration of a run plan" */
328
+ it("includes the scope, the targets, the repeat count and the models", () => {
329
+ expect(result).toContain("**Covers**: labels: auth, checkout");
330
+ expect(result).toContain("- http:agent_abc");
331
+ expect(result).toContain("**Repeat**: 3x");
332
+ expect(result).toContain("**Simulator model**: openai/gpt-5-mini");
333
+ expect(result).toContain("**Judge model**: openai/gpt-5-mini");
334
+ });
335
+
336
+ it("includes the plan name in the heading", () => {
337
+ expect(result).toContain("# Run Plan: Regression Plan");
338
+ });
339
+ });
340
+
341
+ describe("when a plan compares one agent on two models", () => {
342
+ /** @scenario "Agent reads the full configuration of a run plan" */
343
+ it("reads the two targets apart by the parameters each one runs with", async () => {
344
+ mockGetRunPlan.mockResolvedValue({
345
+ ...samplePlan,
346
+ targets: [
347
+ {
348
+ type: "http",
349
+ referenceId: "agent_abc",
350
+ runParameters: { model: "gpt-5" },
351
+ },
352
+ {
353
+ type: "http",
354
+ referenceId: "agent_abc",
355
+ runParameters: { model: "gpt-5-mini" },
356
+ },
357
+ ],
358
+ });
359
+
360
+ const result = await handleGetRunPlan({ id: "plan_abc123" });
361
+
362
+ expect(result).toContain("- http:agent_abc (model=gpt-5)");
363
+ expect(result).toContain("- http:agent_abc (model=gpt-5-mini)");
364
+ });
365
+ });
366
+
367
+ describe("when the scope names test suites", () => {
368
+ /** @scenario "Agent reads a plan that runs the scenarios of a test suite" */
369
+ it("says the plan covers the scenarios of those test suites", async () => {
370
+ mockGetRunPlan.mockResolvedValue({
371
+ ...samplePlan,
372
+ scope: { mode: "test_suites", testSuiteIds: ["suite_a", "suite_b"] },
373
+ });
374
+
375
+ const result = await handleGetRunPlan({ id: "plan_abc123" });
376
+
377
+ expect(result).toContain("**Covers**: test suites: suite_a, suite_b");
378
+ });
379
+ });
380
+
381
+ describe("when the plan names no models", () => {
382
+ /** @scenario "Agent reads a plan that names its own models" */
383
+ it("says both models are the project default", async () => {
384
+ mockGetRunPlan.mockResolvedValue({
385
+ ...samplePlan,
386
+ simulatorModel: null,
387
+ judgeModel: null,
388
+ });
389
+
390
+ const result = await handleGetRunPlan({ id: "plan_abc123" });
391
+
392
+ expect(result).toContain("**Simulator model**: project default");
393
+ expect(result).toContain("**Judge model**: project default");
394
+ });
395
+ });
396
+
397
+ describe("when the plan carries no scope", () => {
398
+ it("reads as the hand-picked scenario list it already held", async () => {
399
+ mockGetRunPlan.mockResolvedValue({ ...samplePlan, scope: null });
400
+
401
+ const result = await handleGetRunPlan({ id: "plan_abc123" });
402
+
403
+ expect(result).toContain("**Covers**: hand-picked scenarios (1)");
404
+ });
405
+ });
406
+
407
+ describe("when format is json", () => {
408
+ it("returns valid parseable JSON matching the plan structure", async () => {
409
+ mockGetRunPlan.mockResolvedValue(samplePlan);
410
+
411
+ const result = await handleGetRunPlan({
412
+ id: "plan_abc123",
413
+ format: "json",
414
+ });
415
+
416
+ expect(JSON.parse(result)).toEqual(samplePlan);
417
+ });
418
+ });
419
+ });
420
+
421
+ describe("handleRerunRunPlan()", () => {
422
+ describe("when the plan is run again", () => {
423
+ let result: string;
424
+
425
+ beforeEach(async () => {
426
+ mockRerunRunPlan.mockResolvedValue({ ...sampleRun, created: false });
427
+ result = await handleRerunRunPlan({
428
+ id: "plan_abc123",
429
+ note: "after the retry fix",
430
+ });
431
+ });
432
+
433
+ /** @scenario "Agent runs a plan again with the configuration it holds" */
434
+ it("says the plan ran with the configuration of this run", () => {
435
+ expect(result).toContain(
436
+ 'Run plan "Regression Plan" started with the configuration of this run.',
437
+ );
438
+ });
439
+
440
+ it("includes the batch run id", () => {
441
+ expect(result).toContain("**Batch Run ID**: batch_123");
442
+ });
443
+
444
+ it("sends only the parameters and the note", () => {
445
+ expect(mockRerunRunPlan).toHaveBeenCalledWith("plan_abc123", {
446
+ parameters: undefined,
447
+ note: "after the retry fix",
448
+ });
449
+ });
450
+ });
451
+ });
452
+
453
+ describe("handleArchiveRunPlan()", () => {
454
+ describe("when the plan is archived", () => {
455
+ let result: string;
456
+
457
+ beforeEach(async () => {
458
+ mockArchiveRunPlan.mockResolvedValue({
459
+ id: "plan_abc123",
460
+ archived: true,
461
+ });
462
+ result = await handleArchiveRunPlan({ id: "plan_abc123" });
463
+ });
464
+
465
+ /** @scenario "Agent archives a run plan" */
466
+ it("confirms the plan is archived", () => {
467
+ expect(result).toContain("Run plan plan_abc123 is archived");
468
+ });
469
+
470
+ it("says the past runs stay readable", () => {
471
+ expect(result).toContain("past runs stay readable");
472
+ });
473
+ });
474
+ });
475
+
476
+ describe("handleRunPlan()", () => {
477
+ describe("when the plan carries its own evaluators", () => {
478
+ /** @scenario "Agent runs a plan with its own evaluators" */
479
+ it("sends the attachments inside the configuration, with an id and a gate settled", async () => {
480
+ mockRunRunPlan.mockResolvedValue({
481
+ scheduled: true,
482
+ batchRunId: "batch_1",
483
+ setId: "set_1",
484
+ jobCount: 1,
485
+ skippedArchived: { scenarios: [], targets: [] },
486
+ items: [],
487
+ runPlanId: "plan_1",
488
+ planName: "Nightly",
489
+ created: true,
490
+ platformUrl: "https://app.langwatch.ai/proj/agent-testing/results/nightly",
491
+ });
492
+
493
+ await handleRunPlan({
494
+ scope: { mode: "all" },
495
+ targets: [{ type: "http", referenceId: "agent_abc" }],
496
+ evaluators: [
497
+ {
498
+ evaluatorId: "evaluator_pii",
499
+ required: false,
500
+ mappings: {
501
+ input: { type: "source", sourceId: "conversation", path: ["transcript"] },
502
+ },
503
+ },
504
+ ],
505
+ });
506
+
507
+ expect(mockRunRunPlan).toHaveBeenLastCalledWith(
508
+ expect.objectContaining({
509
+ config: expect.objectContaining({
510
+ evaluators: [
511
+ {
512
+ id: expect.stringMatching(/^att_/),
513
+ evaluatorId: "evaluator_pii",
514
+ required: false,
515
+ mappings: {
516
+ input: { type: "source", sourceId: "conversation", path: ["transcript"] },
517
+ },
518
+ },
519
+ ],
520
+ }),
521
+ }),
522
+ );
523
+ });
524
+ });
525
+ });
526
+
527
+ describe("handleGetRunPlan()", () => {
528
+ describe("when the plan carries evaluators", () => {
529
+ /** @scenario "Agent reads a run plan that carries evaluators" */
530
+ it("lists each evaluator with its gate and its mappings", async () => {
531
+ mockGetRunPlan.mockResolvedValue({
532
+ id: "plan_1",
533
+ name: "Nightly",
534
+ slug: "nightly",
535
+ scope: { mode: "all" },
536
+ scenarioIds: [],
537
+ targets: [{ type: "http", referenceId: "agent_abc" }],
538
+ repeatCount: 1,
539
+ simulatorModel: null,
540
+ judgeModel: null,
541
+ labels: [],
542
+ archivedAt: null,
543
+ createdAt: "2026-01-01T00:00:00Z",
544
+ updatedAt: "2026-01-02T00:00:00Z",
545
+ platformUrl: "https://app.langwatch.ai/proj/agent-testing/results/nightly",
546
+ evaluators: [
547
+ {
548
+ id: "att_pii",
549
+ evaluatorId: "evaluator_pii",
550
+ required: false,
551
+ mappings: {
552
+ input: { type: "source", sourceId: "conversation", path: ["transcript"] },
553
+ },
554
+ },
555
+ ],
556
+ });
557
+
558
+ const result = await handleGetRunPlan({ id: "plan_1" });
559
+
560
+ expect(result).toContain("## Evaluators");
561
+ expect(result).toContain("evaluator_pii (reports only, attachment att_pii)");
562
+ expect(result).toContain("input: conversation.transcript");
563
+ });
564
+ });
565
+ });
@@ -186,3 +186,22 @@ describe("formatScenarioSchema()", () => {
186
186
  expect(result).toContain("Specific and testable");
187
187
  });
188
188
  });
189
+
190
+ describe("formatScenarioSchema()", () => {
191
+ describe("when documenting the fields and evaluators sections", () => {
192
+ /** @scenario "Agent discovers the suite fields and the evaluator attachments" */
193
+ it("documents the field rules and every mapping path", () => {
194
+ const result = formatScenarioSchema();
195
+
196
+ expect(result).toContain("## Test Suite Fields");
197
+ expect(result).toContain("text, number or boolean");
198
+ expect(result).toContain("## Evaluator Attachments");
199
+ expect(result).toContain("first_user_message");
200
+ expect(result).toContain("last_agent_message");
201
+ expect(result).toContain('["fields", "<identifier>"]');
202
+ expect(result).toContain('["tool_calls", "<toolName>", "input" | "output"]');
203
+ expect(result).toContain('["contexts"]');
204
+ expect(result).toContain("platform_get_simulation_run");
205
+ });
206
+ });
207
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The digest of a simulation run carries the result of every evaluator that
3
+ * ran after the conversation, beside the judge's verdict.
4
+ *
5
+ * @see specs/mcp-server/test-suite-tools.feature
6
+ */
7
+ import { beforeEach, describe, expect, it, vi } from "vitest";
8
+
9
+ vi.mock("../langwatch-api-simulation-runs.js", () => ({
10
+ getSimulationRun: vi.fn(),
11
+ listSimulationRuns: vi.fn(),
12
+ }));
13
+
14
+ import {
15
+ getSimulationRun,
16
+ type SimulationRunSummary,
17
+ } from "../langwatch-api-simulation-runs.js";
18
+ import { handleGetSimulationRun } from "../tools/get-simulation-run.js";
19
+
20
+ const mockGetSimulationRun = vi.mocked(getSimulationRun);
21
+
22
+ const run: SimulationRunSummary = {
23
+ scenarioRunId: "run_1",
24
+ scenarioId: "scen_1",
25
+ batchRunId: "batch_1",
26
+ name: "Chargebacks by quarter",
27
+ status: "FAILED",
28
+ durationInMs: 4200,
29
+ results: {
30
+ verdict: "failed",
31
+ reasoning: "SQL Query Equivalence failed",
32
+ metCriteria: ["Answers with one row per quarter"],
33
+ unmetCriteria: [],
34
+ error: null,
35
+ evaluations: [
36
+ {
37
+ evaluatorId: "evaluator_sql",
38
+ name: "SQL Query Equivalence",
39
+ status: "failed",
40
+ required: true,
41
+ passed: false,
42
+ details: "The query groups by month, not by quarter",
43
+ },
44
+ {
45
+ evaluatorId: "evaluator_judge",
46
+ name: "Answer quality",
47
+ status: "scored",
48
+ required: false,
49
+ score: 0.8,
50
+ },
51
+ {
52
+ evaluatorId: "evaluator_ctx",
53
+ name: "Context recall",
54
+ status: "skipped",
55
+ required: false,
56
+ details: "no table_schema on this scenario",
57
+ },
58
+ ],
59
+ },
60
+ messages: [],
61
+ timestamp: 1_700_000_000_000,
62
+ updatedAt: 1_700_000_004_200,
63
+ };
64
+
65
+ describe("handleGetSimulationRun()", () => {
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ mockGetSimulationRun.mockResolvedValue(run);
69
+ });
70
+
71
+ describe("when the run carries evaluator results", () => {
72
+ /** @scenario "Agent reads a simulation run with evaluator results" */
73
+ it("lists each evaluator with its status, score, gate and reason", async () => {
74
+ const result = await handleGetSimulationRun({ scenarioRunId: "run_1" });
75
+
76
+ expect(result).toContain("## Evaluators");
77
+ expect(result).toContain("**SQL Query Equivalence**: failed, required");
78
+ expect(result).toContain("The query groups by month, not by quarter");
79
+ expect(result).toContain("**Answer quality**: scored, score 0.8");
80
+ expect(result).toContain("**Context recall**: skipped");
81
+ expect(result).toContain("no table_schema on this scenario");
82
+ });
83
+
84
+ /** @scenario "Agent reads a simulation run with evaluator results" */
85
+ it("serves them under results.evaluations in the json format", async () => {
86
+ const result = await handleGetSimulationRun({
87
+ scenarioRunId: "run_1",
88
+ format: "json",
89
+ });
90
+
91
+ const parsed = JSON.parse(result) as SimulationRunSummary;
92
+ expect(parsed.results?.evaluations).toHaveLength(3);
93
+ });
94
+ });
95
+
96
+ describe("when the run is still waiting on its evaluators", () => {
97
+ /** @scenario "A pending run reads as PENDING_EVALUATION" */
98
+ it("warns that the verdict is not final yet", async () => {
99
+ mockGetSimulationRun.mockResolvedValue({
100
+ ...run,
101
+ status: "PENDING_EVALUATION",
102
+ });
103
+
104
+ const result = await handleGetSimulationRun({ scenarioRunId: "run_1" });
105
+
106
+ expect(result).toContain("**Status**: PENDING_EVALUATION");
107
+ expect(result).toContain("have not been recorded yet");
108
+ });
109
+
110
+ it("says nothing extra once they are recorded", async () => {
111
+ const result = await handleGetSimulationRun({ scenarioRunId: "run_1" });
112
+
113
+ expect(result).not.toContain("have not been recorded yet");
114
+ });
115
+ });
116
+ });