@langwatch/mcp-server 0.8.0 → 0.9.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 (115) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +7 -0
  3. package/dist/archive-project-HZBRPR2S.js +19 -0
  4. package/dist/{archive-scenario-YKGLJ4MZ.js → archive-scenario-P5RRITRM.js} +3 -3
  5. package/dist/{archive-suite-QFNDO3Y3.js → archive-suite-NUUFD2EM.js} +3 -3
  6. package/dist/{assign-prompt-tag-Y2B4CBBT.js → assign-prompt-tag-FKCCZPML.js} +2 -2
  7. package/dist/{chunk-YH2IBTEL.js → chunk-42ZHHHIJ.js} +14 -3
  8. package/dist/chunk-ADSJ2FDA.js +44 -0
  9. package/dist/{chunk-L6QENXYA.js → chunk-BB7EOKHR.js} +2 -2
  10. package/dist/chunk-BBJ5WAJB.js +24 -0
  11. package/dist/{chunk-PZUWCWOS.js → chunk-BYWYM2RP.js} +2 -2
  12. package/dist/{chunk-NACSR6B7.js → chunk-CCM4ZFGF.js} +2 -2
  13. package/dist/{chunk-FBRKHHJV.js → chunk-CHKCEZBP.js} +2 -2
  14. package/dist/{chunk-PO7FM7IX.js → chunk-HKY3GW45.js} +2 -2
  15. package/dist/{chunk-KZNBCKWG.js → chunk-JHOSVOHT.js} +2 -2
  16. package/dist/{chunk-B57MZ4WI.js → chunk-JVLJKQJG.js} +657 -88
  17. package/dist/{chunk-HDJDFKZR.js → chunk-R7C7OUVG.js} +2 -2
  18. package/dist/{chunk-OVQF3D5R.js → chunk-TROIWSEQ.js} +2 -2
  19. package/dist/{chunk-CPUPGDNA.js → chunk-X2SEURT2.js} +2 -2
  20. package/dist/{chunk-YI3OG4PI.js → chunk-XC62KJIT.js} +3 -3
  21. package/dist/{create-agent-YRSDH7P7.js → create-agent-K5Q7BFGZ.js} +3 -3
  22. package/dist/{create-annotation-FYQNKMVJ.js → create-annotation-KN3GE3DK.js} +3 -3
  23. package/dist/create-api-key-OQI5PUNQ.js +26 -0
  24. package/dist/{create-dashboard-CORBR4YQ.js → create-dashboard-G4QA5AXP.js} +3 -3
  25. package/dist/{create-dataset-ZC63T6BB.js → create-dataset-WQSC37N7.js} +3 -3
  26. package/dist/{create-dataset-records-ILVNVKVR.js → create-dataset-records-B4RL3FLC.js} +3 -3
  27. package/dist/{create-evaluator-2AU3PXFB.js → create-evaluator-YFFQIR7E.js} +3 -3
  28. package/dist/create-mcp-server.js +2 -1
  29. package/dist/create-project-CUVQ67VN.js +44 -0
  30. package/dist/{create-prompt-NKRJ47YY.js → create-prompt-NGXKSP4X.js} +2 -2
  31. package/dist/{create-prompt-tag-S4JX3ULL.js → create-prompt-tag-IUKYPVHH.js} +2 -2
  32. package/dist/{create-scenario-BPRF75KF.js → create-scenario-FNBXALH3.js} +3 -3
  33. package/dist/{create-suite-4YN7D6K2.js → create-suite-DJEPD4ZB.js} +3 -3
  34. package/dist/{delete-agent-Y72R2YQA.js → delete-agent-TJI5XBBM.js} +3 -3
  35. package/dist/{delete-annotation-UHWDMHSH.js → delete-annotation-KRP53TUU.js} +3 -3
  36. package/dist/{delete-dashboard-LPWT36KN.js → delete-dashboard-N5SZJIR3.js} +3 -3
  37. package/dist/{delete-dataset-GGPQRHKX.js → delete-dataset-KDUOHL2P.js} +3 -3
  38. package/dist/{delete-dataset-records-LTG5LT26.js → delete-dataset-records-DM4W3PAW.js} +3 -3
  39. package/dist/{delete-evaluator-F7QLXDHU.js → delete-evaluator-7ZATLJOA.js} +3 -3
  40. package/dist/{delete-prompt-tag-B5UKZL45.js → delete-prompt-tag-I6HNMSVJ.js} +2 -2
  41. package/dist/{delete-workflow-4DGUDIDE.js → delete-workflow-QEO74JQR.js} +3 -3
  42. package/dist/{get-agent-YEVICUX4.js → get-agent-3KDWCP5N.js} +3 -3
  43. package/dist/{get-analytics-3W5CMU3F.js → get-analytics-GPVJX7SF.js} +2 -2
  44. package/dist/{get-dashboard-NCQHN6MW.js → get-dashboard-YPTIE5YY.js} +3 -3
  45. package/dist/{get-dataset-KBK6PH36.js → get-dataset-6DQFYITM.js} +3 -3
  46. package/dist/{get-evaluator-QDUL5LXR.js → get-evaluator-PCM6XQAU.js} +3 -3
  47. package/dist/get-project-JCMWXVLU.js +26 -0
  48. package/dist/{get-prompt-XCSSNRKR.js → get-prompt-5XAGWWBG.js} +2 -2
  49. package/dist/{get-scenario-FNRKOY7E.js → get-scenario-7XBCDZWX.js} +3 -3
  50. package/dist/{get-simulation-run-WOEKUF7K.js → get-simulation-run-AV24FJWI.js} +3 -3
  51. package/dist/{get-suite-U5AOD5TC.js → get-suite-4DBVHFEM.js} +3 -3
  52. package/dist/{get-trace-MUWEJM2Z.js → get-trace-MRBDS3HJ.js} +2 -2
  53. package/dist/{get-workflow-SOA7MT7L.js → get-workflow-UR4Y6UFX.js} +3 -3
  54. package/dist/{http-server-7DMOCB75.js → http-server-DEGNV5EP.js} +3 -2
  55. package/dist/index.js +1060 -220
  56. package/dist/{langwatch-api-annotations-Q57NDUVR.js → langwatch-api-annotations-ZJDRB4JH.js} +3 -3
  57. package/dist/{langwatch-api-dashboards-VBWWXV3Y.js → langwatch-api-dashboards-WARLI6NF.js} +3 -3
  58. package/dist/{langwatch-api-monitors-L3IY7FDT.js → langwatch-api-monitors-4XKXHYCG.js} +2 -2
  59. package/dist/{langwatch-api-secrets-S5HHF3XR.js → langwatch-api-secrets-6SHK5G7Q.js} +2 -2
  60. package/dist/{langwatch-api-triggers-ME33QE26.js → langwatch-api-triggers-5SX6R2WU.js} +2 -2
  61. package/dist/{langwatch-api-workflows-4CJQAYP2.js → langwatch-api-workflows-NLJENNXE.js} +3 -3
  62. package/dist/{list-agents-3HGIBJFM.js → list-agents-NECO7MPI.js} +3 -3
  63. package/dist/{list-annotations-KWXYVEPI.js → list-annotations-3OJEO774.js} +3 -3
  64. package/dist/list-api-keys-AWW7ROLB.js +37 -0
  65. package/dist/{list-dashboards-VQ2LH666.js → list-dashboards-DVPVVVKW.js} +3 -3
  66. package/dist/{list-dataset-records-RNRGROJP.js → list-dataset-records-I4BYORF5.js} +3 -3
  67. package/dist/{list-datasets-I4A2LT2S.js → list-datasets-WUAE5HGX.js} +3 -3
  68. package/dist/{list-evaluators-AKKRJ5MX.js → list-evaluators-B6OSVAM2.js} +3 -3
  69. package/dist/{list-model-providers-DVFEHFHZ.js → list-model-providers-NL6IP4UZ.js} +3 -3
  70. package/dist/list-projects-5FHMJ4PQ.js +42 -0
  71. package/dist/{list-prompt-tags-PL5OISUA.js → list-prompt-tags-Z2DD6FLQ.js} +2 -2
  72. package/dist/{list-prompts-7VTFWE2Z.js → list-prompts-VNXKHK7T.js} +2 -2
  73. package/dist/{list-scenarios-5BZWZ6ZN.js → list-scenarios-CFZHXL76.js} +3 -3
  74. package/dist/{list-simulation-runs-WCZ6RLPE.js → list-simulation-runs-AM2BXLBW.js} +3 -3
  75. package/dist/{list-suites-RRQ2JX5Y.js → list-suites-ZDOHAAXF.js} +3 -3
  76. package/dist/{list-workflows-UQB5FCX5.js → list-workflows-WBRBKLIT.js} +3 -3
  77. package/dist/{rename-prompt-tag-TURD6KYU.js → rename-prompt-tag-A4RJUK7Z.js} +2 -2
  78. package/dist/revoke-api-key-ZSPSPF5G.js +15 -0
  79. package/dist/{run-agent-BEJ4K4CM.js → run-agent-JFNDY7MR.js} +3 -3
  80. package/dist/{run-suite-U7V432G2.js → run-suite-TH7LO5TJ.js} +3 -3
  81. package/dist/{run-workflow-5WX7DK5T.js → run-workflow-CK3JP5U2.js} +3 -3
  82. package/dist/{search-traces-ZCHNOMTX.js → search-traces-3BBVYPC3.js} +2 -2
  83. package/dist/{set-model-provider-6CFHMCNE.js → set-model-provider-WOC5FNMV.js} +3 -3
  84. package/dist/{update-agent-UE6UWLZP.js → update-agent-YMEV5MTL.js} +3 -3
  85. package/dist/{update-dataset-4XQZZE4S.js → update-dataset-EYT5POFM.js} +3 -3
  86. package/dist/{update-dataset-record-GNNM5TKM.js → update-dataset-record-VFF5JTX4.js} +3 -3
  87. package/dist/{update-evaluator-LHVDXIZD.js → update-evaluator-6IJHFG2E.js} +3 -3
  88. package/dist/update-project-OIT4WPFI.js +24 -0
  89. package/dist/{update-prompt-3XEHECJ7.js → update-prompt-ZU2DD6KM.js} +2 -2
  90. package/dist/{update-scenario-DFPREYCD.js → update-scenario-3VUTPDRU.js} +3 -3
  91. package/dist/{update-suite-RATU5THO.js → update-suite-C6JFDQHL.js} +3 -3
  92. package/package.json +2 -2
  93. package/pnpm-workspace.yaml +2 -0
  94. package/pyproject.toml +3 -0
  95. package/src/__tests__/experiment-list-tools.unit.test.ts +157 -0
  96. package/src/__tests__/get-experiment-results.unit.test.ts +222 -0
  97. package/src/__tests__/project-api-key-tools.unit.test.ts +451 -0
  98. package/src/create-mcp-server.ts +257 -13
  99. package/src/langwatch-api-api-keys.ts +54 -0
  100. package/src/langwatch-api-projects.ts +82 -0
  101. package/src/langwatch-api.ts +17 -3
  102. package/src/tools/archive-project.ts +14 -0
  103. package/src/tools/create-api-key.ts +29 -0
  104. package/src/tools/create-project.ts +45 -0
  105. package/src/tools/get-experiment-results.ts +273 -0
  106. package/src/tools/get-project.ts +20 -0
  107. package/src/tools/list-api-keys.ts +33 -0
  108. package/src/tools/list-experiment-runs.ts +142 -0
  109. package/src/tools/list-experiments.ts +95 -0
  110. package/src/tools/list-projects.ts +41 -0
  111. package/src/tools/revoke-api-key.ts +9 -0
  112. package/src/tools/{run-evaluation.ts → run-experiment.ts} +5 -5
  113. package/src/tools/update-project.ts +22 -0
  114. package/uv.lock +7 -3
  115. package/dist/run-evaluation-AJ7WZNQD.js +0 -64
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langwatch/mcp-server",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "An MCP server for Langwatch.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "@types/node": "^25.6.0",
46
46
  "@types/yargs": "^17.0.33",
47
47
  "@typescript/native-preview": "7.0.0-dev.20250911.1",
48
- "ai": "^6.0.0",
48
+ "ai": "^6.0.161",
49
49
  "dotenv": "^17.2.2",
50
50
  "esbuild": "^0.25.0",
51
51
  "eslint": "^10.2.1",
@@ -1,2 +1,4 @@
1
+ minimumReleaseAge: 10080
2
+
1
3
  onlyBuiltDependencies:
2
4
  - node-pty
package/pyproject.toml CHANGED
@@ -13,5 +13,8 @@ dependencies = [
13
13
  "tenacity>=9.1.2",
14
14
  ]
15
15
 
16
+ [tool.uv]
17
+ exclude-newer = "7 days"
18
+
16
19
  [tool.uv.sources]
17
20
  langwatch = { path = "../python-sdk", editable = true }
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("../langwatch-api.js", async (importOriginal) => {
4
+ const actual = (await importOriginal()) as Record<string, unknown>;
5
+ return {
6
+ ...actual,
7
+ makeRequest: vi.fn(),
8
+ };
9
+ });
10
+
11
+ import { makeRequest } from "../langwatch-api.js";
12
+ import { handleExperimentList } from "../tools/list-experiments.js";
13
+ import { handleExperimentListRuns } from "../tools/list-experiment-runs.js";
14
+
15
+ const mockMakeRequest = vi.mocked(makeRequest);
16
+
17
+ describe("handleExperimentList()", () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ describe("given experiments exist", () => {
23
+ describe("when invoked with default limit", () => {
24
+ /** @scenario "Lists experiments as markdown" */
25
+ it("renders a markdown table with each slug", async () => {
26
+ mockMakeRequest.mockResolvedValueOnce({
27
+ experiments: [
28
+ {
29
+ id: "exp_1",
30
+ slug: "checkout-flow",
31
+ name: "Checkout Flow",
32
+ type: "EVALUATIONS_V3",
33
+ workflowId: null,
34
+ createdAt: "2026-01-01T00:00:00Z",
35
+ updatedAt: "2026-01-02T00:00:00Z",
36
+ runsCount: 3,
37
+ lastRunAt: "2026-01-02T00:00:00Z",
38
+ },
39
+ ],
40
+ pagination: {
41
+ page: 1,
42
+ pageSize: 25,
43
+ totalHits: 1,
44
+ hasMore: false,
45
+ },
46
+ });
47
+
48
+ const out = await handleExperimentList({});
49
+ expect(out).toContain("# Experiments");
50
+ expect(out).toContain("`checkout-flow`");
51
+ expect(out).toContain("Checkout Flow");
52
+ });
53
+ });
54
+
55
+ describe("when invoked with limit above the cap", () => {
56
+ /** @scenario "Limit is bounded to protect agent context" */
57
+ it("clamps the effective pageSize to 100", async () => {
58
+ mockMakeRequest.mockResolvedValueOnce({
59
+ experiments: [],
60
+ pagination: { page: 1, pageSize: 100, totalHits: 0, hasMore: false },
61
+ });
62
+
63
+ await handleExperimentList({ limit: 5000 });
64
+ const path = mockMakeRequest.mock.calls[0]![1] as string;
65
+ expect(path).toContain("pageSize=100");
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("given no experiments exist", () => {
71
+ describe("when invoked", () => {
72
+ it("returns an empty-state message", async () => {
73
+ mockMakeRequest.mockResolvedValueOnce({
74
+ experiments: [],
75
+ pagination: { page: 1, pageSize: 25, totalHits: 0, hasMore: false },
76
+ });
77
+ const out = await handleExperimentList({});
78
+ expect(out).toContain("No experiments");
79
+ });
80
+ });
81
+ });
82
+ });
83
+
84
+ describe("handleExperimentListRuns()", () => {
85
+ beforeEach(() => {
86
+ vi.clearAllMocks();
87
+ });
88
+
89
+ describe("given a known experiment with runs", () => {
90
+ describe("when invoked", () => {
91
+ /** @scenario "Lists runs for a known experiment" */
92
+ it("renders a markdown table with each runId", async () => {
93
+ mockMakeRequest.mockResolvedValueOnce({
94
+ experimentId: "exp_1",
95
+ experimentSlug: "checkout-flow",
96
+ runs: [
97
+ {
98
+ experimentId: "exp_1",
99
+ runId: "run_visible",
100
+ workflowVersion: null,
101
+ timestamps: { createdAt: 1000, updatedAt: 2000, finishedAt: 2000 },
102
+ summary: { evaluations: {} },
103
+ },
104
+ ],
105
+ pagination: {
106
+ page: 1,
107
+ pageSize: 25,
108
+ totalHits: 1,
109
+ hasMore: false,
110
+ },
111
+ });
112
+
113
+ const out = await handleExperimentListRuns({
114
+ experimentSlug: "checkout-flow",
115
+ });
116
+ expect(out).toContain("# Evaluation Runs: checkout-flow");
117
+ expect(out).toContain("`run_visible`");
118
+ });
119
+ });
120
+ });
121
+
122
+ describe("given the experiment slug is unknown", () => {
123
+ describe("when the API throws 404", () => {
124
+ /** @scenario "Unknown experiment slug returns a graceful not-found message" */
125
+ it("returns a graceful not-found message suggesting platform_experiment_list", async () => {
126
+ mockMakeRequest.mockRejectedValueOnce(
127
+ new Error("LangWatch API error 404: experiment not found"),
128
+ );
129
+
130
+ const out = await handleExperimentListRuns({
131
+ experimentSlug: "does-not-exist",
132
+ });
133
+ expect(out).toContain("not found");
134
+ expect(out).toContain("platform_experiment_list");
135
+ });
136
+ });
137
+ });
138
+
139
+ describe("given the experiment exists but has no runs", () => {
140
+ describe("when invoked", () => {
141
+ it("returns an empty-state message that points at platform_run_experiment", async () => {
142
+ mockMakeRequest.mockResolvedValueOnce({
143
+ experimentId: "exp_1",
144
+ experimentSlug: "support-bot",
145
+ runs: [],
146
+ pagination: { page: 1, pageSize: 25, totalHits: 0, hasMore: false },
147
+ });
148
+
149
+ const out = await handleExperimentListRuns({
150
+ experimentSlug: "support-bot",
151
+ });
152
+ expect(out).toContain("no runs yet");
153
+ expect(out).toContain("platform_run_experiment");
154
+ });
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("../langwatch-api.js", async (importOriginal) => {
4
+ const actual = (await importOriginal()) as Record<string, unknown>;
5
+ return {
6
+ ...actual,
7
+ makeRequest: vi.fn(),
8
+ };
9
+ });
10
+
11
+ import { LangWatchApiError, makeRequest } from "../langwatch-api.js";
12
+ import { handleExperimentResults } from "../tools/get-experiment-results.js";
13
+
14
+ const mockMakeRequest = vi.mocked(makeRequest);
15
+
16
+ const sample = {
17
+ experimentId: "exp_1",
18
+ runId: "run_1",
19
+ projectId: "proj_1",
20
+ progress: 3,
21
+ total: 3,
22
+ dataset: [
23
+ { index: 0, entry: { input: "hello world" } },
24
+ { index: 1, entry: { input: "broken" }, error: "boom" },
25
+ { index: 2, entry: { input: "low" } },
26
+ ],
27
+ evaluations: [
28
+ { evaluator: "quality", index: 0, status: "processed", score: 0.9, passed: true },
29
+ { evaluator: "quality", index: 2, status: "processed", score: 0.2, passed: false, details: "off-topic" },
30
+ { evaluator: "safety", index: 0, status: "processed", score: 1.0, passed: true },
31
+ ],
32
+ timestamps: { createdAt: 0, updatedAt: 0, finishedAt: 1 },
33
+ };
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ describe("handleExperimentResults()", () => {
40
+ describe("given a completed run", () => {
41
+ describe("when called without filters", () => {
42
+ it("hits the v3 results endpoint with the run id", async () => {
43
+ mockMakeRequest.mockResolvedValueOnce(sample);
44
+ await handleExperimentResults({ runId: "run_1" });
45
+ expect(mockMakeRequest).toHaveBeenCalledWith(
46
+ "GET",
47
+ "/api/experiments/runs/run_1/results",
48
+ );
49
+ });
50
+
51
+ it("renders evaluator averages and per-row sections in markdown", async () => {
52
+ mockMakeRequest.mockResolvedValueOnce(sample);
53
+ const out = await handleExperimentResults({ runId: "run_1" });
54
+ expect(out).toContain("# Evaluation Results: run_1");
55
+ expect(out).toContain("## Evaluator Summary");
56
+ expect(out).toContain("quality");
57
+ expect(out).toContain("safety");
58
+ expect(out).toContain("Row #0");
59
+ expect(out).toContain("Row #1");
60
+ expect(out).toContain("Row #2");
61
+ });
62
+ });
63
+
64
+ describe("when filter is 'failed'", () => {
65
+ it("includes only failed/errored rows", async () => {
66
+ mockMakeRequest.mockResolvedValueOnce(sample);
67
+ const out = await handleExperimentResults({
68
+ runId: "run_1",
69
+ filter: "failed",
70
+ });
71
+ expect(out).toContain("Row #1");
72
+ expect(out).toContain("Row #2");
73
+ expect(out).not.toContain("Row #0");
74
+ });
75
+ });
76
+
77
+ describe("when an evaluator name is supplied", () => {
78
+ it("limits the report to that evaluator", async () => {
79
+ mockMakeRequest.mockResolvedValueOnce(sample);
80
+ const out = await handleExperimentResults({
81
+ runId: "run_1",
82
+ evaluator: "quality",
83
+ });
84
+ expect(out).toContain("quality");
85
+ // The per-row sections should not list the safety evaluator bullet
86
+ expect(out).not.toMatch(/\*\*safety\*\*/);
87
+ });
88
+ });
89
+
90
+ describe("when there are more rows than the cap", () => {
91
+ it("truncates and notes the cap", async () => {
92
+ const big = {
93
+ ...sample,
94
+ dataset: Array.from({ length: 80 }, (_, i) => ({
95
+ index: i,
96
+ entry: { input: `row ${i}` },
97
+ })),
98
+ evaluations: [],
99
+ };
100
+ mockMakeRequest.mockResolvedValueOnce(big);
101
+ const out = await handleExperimentResults({ runId: "run_1" });
102
+ expect(out).toContain("Output truncated to 50 rows of 80");
103
+ });
104
+ });
105
+
106
+ describe("when a caller asks for more than the row cap", () => {
107
+ it("still caps output at 50 rows", async () => {
108
+ const big = {
109
+ ...sample,
110
+ dataset: Array.from({ length: 80 }, (_, i) => ({
111
+ index: i,
112
+ entry: { input: `row ${i}` },
113
+ })),
114
+ evaluations: [],
115
+ };
116
+ mockMakeRequest.mockResolvedValueOnce(big);
117
+ const out = await handleExperimentResults({
118
+ runId: "run_1",
119
+ limit: 5000,
120
+ });
121
+ expect(out).toContain("Output truncated to 50 rows of 80");
122
+ });
123
+ });
124
+
125
+ describe("when only some rows have evaluations", () => {
126
+ it("explains rows with no evaluation coverage", async () => {
127
+ mockMakeRequest.mockResolvedValueOnce({
128
+ ...sample,
129
+ dataset: [
130
+ { index: 0, entry: { input: "covered" } },
131
+ { index: 1, entry: { input: "uncovered" } },
132
+ ],
133
+ evaluations: [
134
+ {
135
+ evaluator: "quality",
136
+ index: 0,
137
+ status: "processed",
138
+ score: 0.9,
139
+ passed: true,
140
+ },
141
+ ],
142
+ });
143
+
144
+ const out = await handleExperimentResults({ runId: "run_1" });
145
+
146
+ expect(out).toContain("uncovered");
147
+ expect(out).toContain("No evaluations recorded for this row");
148
+ });
149
+ });
150
+
151
+ describe("when multiple targets share the same dataset index", () => {
152
+ it("keeps evaluator rows target-scoped", async () => {
153
+ mockMakeRequest.mockResolvedValueOnce({
154
+ ...sample,
155
+ dataset: [
156
+ { index: 0, targetId: "a", entry: { input: "target a" } },
157
+ { index: 0, targetId: "b", entry: { input: "target b" } },
158
+ ],
159
+ evaluations: [
160
+ {
161
+ evaluator: "quality",
162
+ index: 0,
163
+ targetId: "a",
164
+ status: "processed",
165
+ score: 0.9,
166
+ passed: true,
167
+ },
168
+ {
169
+ evaluator: "quality",
170
+ index: 0,
171
+ targetId: "b",
172
+ status: "processed",
173
+ score: 0.1,
174
+ passed: false,
175
+ details: "target-b-only",
176
+ },
177
+ ],
178
+ });
179
+
180
+ const out = await handleExperimentResults({
181
+ runId: "run_1",
182
+ filter: "failed",
183
+ });
184
+
185
+ expect(out).toContain("target b");
186
+ expect(out).toContain("target-b-only");
187
+ expect(out).not.toContain("target a");
188
+ });
189
+ });
190
+ });
191
+
192
+ describe("given the run is missing", () => {
193
+ describe("when the API throws a 404-shaped error", () => {
194
+ it("returns a graceful 'not found' markdown", async () => {
195
+ mockMakeRequest.mockRejectedValueOnce(
196
+ new LangWatchApiError("missing", 404, "Run not found"),
197
+ );
198
+ const out = await handleExperimentResults({ runId: "missing" });
199
+ expect(out).toContain("not found");
200
+ expect(out).toContain("platform_experiment_status");
201
+ });
202
+ });
203
+ });
204
+
205
+ describe("given the run is still in progress", () => {
206
+ describe("when results are requested", () => {
207
+ it("returns guidance to check status instead of partial results", async () => {
208
+ mockMakeRequest.mockResolvedValueOnce({
209
+ ...sample,
210
+ progress: 2,
211
+ total: 3,
212
+ timestamps: { createdAt: 0, updatedAt: 0, finishedAt: null },
213
+ });
214
+
215
+ const out = await handleExperimentResults({ runId: "unfinished" });
216
+
217
+ expect(out).toContain("results are not yet available");
218
+ expect(out).toContain("platform_experiment_status");
219
+ });
220
+ });
221
+ });
222
+ });