@langwatch/mcp-server 0.7.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.
- package/.release-please-shim +1 -0
- package/CHANGELOG.md +87 -0
- package/LICENSE +7 -0
- package/dist/archive-project-HZBRPR2S.js +19 -0
- package/dist/{archive-scenario-WJ2A7ETN.js → archive-scenario-P5RRITRM.js} +4 -4
- package/dist/{archive-suite-4JOM4SSL.js → archive-suite-NUUFD2EM.js} +4 -4
- package/dist/{assign-prompt-tag-GL7EAAF3.js → assign-prompt-tag-FKCCZPML.js} +3 -3
- package/dist/{chunk-2WPLXEKF.js → chunk-42ZHHHIJ.js} +20 -5
- package/dist/chunk-ADSJ2FDA.js +44 -0
- package/dist/{chunk-IE4M5OGE.js → chunk-BB7EOKHR.js} +2 -2
- package/dist/chunk-BBJ5WAJB.js +24 -0
- package/dist/{chunk-EGH5RPXM.js → chunk-BYWYM2RP.js} +2 -2
- package/dist/{chunk-2GT7IFWK.js → chunk-CCM4ZFGF.js} +2 -2
- package/dist/{chunk-LUMEZEHL.js → chunk-CHKCEZBP.js} +2 -2
- package/dist/{chunk-ZVS4UKN2.js → chunk-HKY3GW45.js} +2 -2
- package/dist/{chunk-EUQDVOSU.js → chunk-JHOSVOHT.js} +2 -2
- package/dist/{chunk-E5JYYU3J.js → chunk-JVLJKQJG.js} +669 -94
- package/dist/{chunk-BWWY5KEF.js → chunk-KOO5733S.js} +3 -2
- package/dist/{chunk-HYMIWO3U.js → chunk-R7C7OUVG.js} +2 -2
- package/dist/{chunk-WQG2DFHX.js → chunk-TROIWSEQ.js} +2 -2
- package/dist/{chunk-CCWCIJJ3.js → chunk-X2SEURT2.js} +2 -2
- package/dist/{chunk-4AUB2BIA.js → chunk-XC62KJIT.js} +3 -3
- package/dist/config.js +1 -1
- package/dist/{create-agent-MBOOT3XK.js → create-agent-K5Q7BFGZ.js} +4 -4
- package/dist/{create-annotation-XMV6A7ED.js → create-annotation-KN3GE3DK.js} +4 -4
- package/dist/create-api-key-OQI5PUNQ.js +26 -0
- package/dist/{create-dashboard-VRKLFVZJ.js → create-dashboard-G4QA5AXP.js} +4 -4
- package/dist/{create-dataset-7HQPZCRE.js → create-dataset-WQSC37N7.js} +4 -4
- package/dist/{create-dataset-records-2NIVFYTV.js → create-dataset-records-B4RL3FLC.js} +4 -4
- package/dist/{create-evaluator-WYFRFLRC.js → create-evaluator-YFFQIR7E.js} +4 -4
- package/dist/create-mcp-server.js +3 -2
- package/dist/create-project-CUVQ67VN.js +44 -0
- package/dist/{create-prompt-IEN7O6JT.js → create-prompt-NGXKSP4X.js} +3 -3
- package/dist/{create-prompt-tag-CAEZ45VO.js → create-prompt-tag-IUKYPVHH.js} +3 -3
- package/dist/{create-scenario-6YHSMGLP.js → create-scenario-FNBXALH3.js} +4 -4
- package/dist/{create-suite-H5XOFDE5.js → create-suite-DJEPD4ZB.js} +4 -4
- package/dist/{delete-agent-JKTNBWAA.js → delete-agent-TJI5XBBM.js} +4 -4
- package/dist/{delete-annotation-ACMDV6QO.js → delete-annotation-KRP53TUU.js} +4 -4
- package/dist/{delete-dashboard-VJSR7CRU.js → delete-dashboard-N5SZJIR3.js} +4 -4
- package/dist/{delete-dataset-RTTTWP4D.js → delete-dataset-KDUOHL2P.js} +4 -4
- package/dist/{delete-dataset-records-KNQTIPQK.js → delete-dataset-records-DM4W3PAW.js} +4 -4
- package/dist/{delete-evaluator-7FVC5WM4.js → delete-evaluator-7ZATLJOA.js} +4 -4
- package/dist/{delete-prompt-tag-ZMZUAGFW.js → delete-prompt-tag-I6HNMSVJ.js} +3 -3
- package/dist/{delete-workflow-EYQ3T4RQ.js → delete-workflow-QEO74JQR.js} +4 -4
- package/dist/{get-agent-E3QSTVKG.js → get-agent-3KDWCP5N.js} +4 -4
- package/dist/{get-analytics-2OHV763I.js → get-analytics-GPVJX7SF.js} +3 -3
- package/dist/{get-dashboard-PC7JDHH6.js → get-dashboard-YPTIE5YY.js} +4 -4
- package/dist/{get-dataset-H5IK5FVU.js → get-dataset-6DQFYITM.js} +4 -4
- package/dist/{get-evaluator-UWVUPYTM.js → get-evaluator-PCM6XQAU.js} +4 -4
- package/dist/get-project-JCMWXVLU.js +26 -0
- package/dist/{get-prompt-WZUOLLQC.js → get-prompt-5XAGWWBG.js} +3 -3
- package/dist/{get-scenario-Q6LCNUYN.js → get-scenario-7XBCDZWX.js} +4 -4
- package/dist/{get-simulation-run-Z2XKO256.js → get-simulation-run-AV24FJWI.js} +4 -4
- package/dist/{get-suite-BXK5TN7P.js → get-suite-4DBVHFEM.js} +4 -4
- package/dist/{get-trace-4EJAYVBG.js → get-trace-MRBDS3HJ.js} +3 -3
- package/dist/{get-workflow-O34QJLHD.js → get-workflow-UR4Y6UFX.js} +4 -4
- package/dist/{http-server-ISHF7NJX.js → http-server-DEGNV5EP.js} +4 -3
- package/dist/index.js +1453 -431
- package/dist/{langwatch-api-annotations-ERBG27RF.js → langwatch-api-annotations-ZJDRB4JH.js} +4 -4
- package/dist/{langwatch-api-dashboards-G6TR7VVO.js → langwatch-api-dashboards-WARLI6NF.js} +4 -4
- package/dist/{langwatch-api-monitors-WSXZLS2B.js → langwatch-api-monitors-4XKXHYCG.js} +3 -3
- package/dist/{langwatch-api-secrets-RR5DKG2X.js → langwatch-api-secrets-6SHK5G7Q.js} +3 -3
- package/dist/{langwatch-api-triggers-2RTNSWQY.js → langwatch-api-triggers-5SX6R2WU.js} +3 -3
- package/dist/langwatch-api-workflows-NLJENNXE.js +15 -0
- package/dist/{list-agents-AW3P6FJV.js → list-agents-NECO7MPI.js} +4 -4
- package/dist/{list-annotations-QNCGWPQ3.js → list-annotations-3OJEO774.js} +4 -4
- package/dist/list-api-keys-AWW7ROLB.js +37 -0
- package/dist/{list-dashboards-R7RK2IXL.js → list-dashboards-DVPVVVKW.js} +4 -4
- package/dist/{list-dataset-records-WVCSQP7P.js → list-dataset-records-I4BYORF5.js} +4 -4
- package/dist/{list-datasets-KEXRLDEU.js → list-datasets-WUAE5HGX.js} +4 -4
- package/dist/{list-evaluators-7R2LB4JH.js → list-evaluators-B6OSVAM2.js} +4 -4
- package/dist/{list-model-providers-OBYJCKBX.js → list-model-providers-NL6IP4UZ.js} +4 -4
- package/dist/list-projects-5FHMJ4PQ.js +42 -0
- package/dist/{list-prompt-tags-6LY3ZPYI.js → list-prompt-tags-Z2DD6FLQ.js} +3 -3
- package/dist/{list-prompts-VWEZ4CDE.js → list-prompts-VNXKHK7T.js} +3 -3
- package/dist/{list-scenarios-TXD4W2ZJ.js → list-scenarios-CFZHXL76.js} +4 -4
- package/dist/{list-simulation-runs-RHPOLMFI.js → list-simulation-runs-AM2BXLBW.js} +4 -4
- package/dist/{list-suites-FSGBX27Z.js → list-suites-ZDOHAAXF.js} +4 -4
- package/dist/{list-workflows-VRGEZ3EN.js → list-workflows-WBRBKLIT.js} +4 -4
- package/dist/{rename-prompt-tag-VOJJLX4M.js → rename-prompt-tag-A4RJUK7Z.js} +3 -3
- package/dist/revoke-api-key-ZSPSPF5G.js +15 -0
- package/dist/{run-agent-KTCC7TB7.js → run-agent-JFNDY7MR.js} +4 -4
- package/dist/{run-suite-BTMHGI2D.js → run-suite-TH7LO5TJ.js} +4 -4
- package/dist/{run-workflow-IGAYGKJL.js → run-workflow-CK3JP5U2.js} +4 -4
- package/dist/{search-traces-VYFUIMXH.js → search-traces-3BBVYPC3.js} +3 -3
- package/dist/{set-model-provider-W5IUI326.js → set-model-provider-WOC5FNMV.js} +4 -4
- package/dist/{update-agent-4BB7EYLR.js → update-agent-YMEV5MTL.js} +4 -4
- package/dist/{update-dataset-CXBKZ4W3.js → update-dataset-EYT5POFM.js} +4 -4
- package/dist/{update-dataset-record-HOHAMHXG.js → update-dataset-record-VFF5JTX4.js} +4 -4
- package/dist/{update-evaluator-IVOR6FTR.js → update-evaluator-6IJHFG2E.js} +4 -4
- package/dist/update-project-OIT4WPFI.js +24 -0
- package/dist/{update-prompt-OWQAKE7K.js → update-prompt-ZU2DD6KM.js} +3 -3
- package/dist/{update-scenario-GDFVILPN.js → update-scenario-3VUTPDRU.js} +4 -4
- package/dist/{update-suite-BXYUYSPT.js → update-suite-C6JFDQHL.js} +4 -4
- package/package.json +7 -7
- package/pnpm-workspace.yaml +2 -0
- package/pyproject.toml +3 -0
- package/src/__tests__/all-tools.integration.test.ts +9 -0
- package/src/__tests__/datasets.integration.test.ts +17 -0
- package/src/__tests__/datasets.unit.test.ts +4 -0
- package/src/__tests__/experiment-list-tools.unit.test.ts +157 -0
- package/src/__tests__/get-experiment-results.unit.test.ts +222 -0
- package/src/__tests__/project-api-key-tools.unit.test.ts +451 -0
- package/src/__tests__/scenario-tools.unit.test.ts +3 -0
- package/src/config.ts +2 -0
- package/src/create-mcp-server.ts +257 -13
- package/src/langwatch-api-api-keys.ts +54 -0
- package/src/langwatch-api-projects.ts +82 -0
- package/src/langwatch-api.ts +22 -4
- package/src/tools/archive-project.ts +14 -0
- package/src/tools/create-api-key.ts +29 -0
- package/src/tools/create-project.ts +45 -0
- package/src/tools/get-experiment-results.ts +273 -0
- package/src/tools/get-project.ts +20 -0
- package/src/tools/list-api-keys.ts +33 -0
- package/src/tools/list-experiment-runs.ts +142 -0
- package/src/tools/list-experiments.ts +95 -0
- package/src/tools/list-projects.ts +41 -0
- package/src/tools/revoke-api-key.ts +9 -0
- package/src/tools/{run-evaluation.ts → run-experiment.ts} +5 -5
- package/src/tools/update-project.ts +22 -0
- package/uv.lock +192 -530
- package/Dockerfile +0 -22
- package/build.js +0 -12
- package/dist/archive-scenario-WJ2A7ETN.js.map +0 -1
- package/dist/archive-suite-4JOM4SSL.js.map +0 -1
- package/dist/assign-prompt-tag-GL7EAAF3.js.map +0 -1
- package/dist/chunk-2GT7IFWK.js.map +0 -1
- package/dist/chunk-2WPLXEKF.js.map +0 -1
- package/dist/chunk-4AUB2BIA.js.map +0 -1
- package/dist/chunk-BWWY5KEF.js.map +0 -1
- package/dist/chunk-CCWCIJJ3.js.map +0 -1
- package/dist/chunk-E5JYYU3J.js.map +0 -1
- package/dist/chunk-EGH5RPXM.js.map +0 -1
- package/dist/chunk-EUQDVOSU.js.map +0 -1
- package/dist/chunk-HYMIWO3U.js.map +0 -1
- package/dist/chunk-IE4M5OGE.js.map +0 -1
- package/dist/chunk-LUMEZEHL.js.map +0 -1
- package/dist/chunk-NUFPTHKU.js.map +0 -1
- package/dist/chunk-WQG2DFHX.js.map +0 -1
- package/dist/chunk-WVI5CJ6S.js.map +0 -1
- package/dist/chunk-ZVS4UKN2.js.map +0 -1
- package/dist/chunk-ZXKLPC2E.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/create-agent-MBOOT3XK.js.map +0 -1
- package/dist/create-annotation-XMV6A7ED.js.map +0 -1
- package/dist/create-dashboard-VRKLFVZJ.js.map +0 -1
- package/dist/create-dataset-7HQPZCRE.js.map +0 -1
- package/dist/create-dataset-records-2NIVFYTV.js.map +0 -1
- package/dist/create-evaluator-WYFRFLRC.js.map +0 -1
- package/dist/create-mcp-server.js.map +0 -1
- package/dist/create-prompt-IEN7O6JT.js.map +0 -1
- package/dist/create-prompt-tag-CAEZ45VO.js.map +0 -1
- package/dist/create-scenario-6YHSMGLP.js.map +0 -1
- package/dist/create-suite-H5XOFDE5.js.map +0 -1
- package/dist/delete-agent-JKTNBWAA.js.map +0 -1
- package/dist/delete-annotation-ACMDV6QO.js.map +0 -1
- package/dist/delete-dashboard-VJSR7CRU.js.map +0 -1
- package/dist/delete-dataset-RTTTWP4D.js.map +0 -1
- package/dist/delete-dataset-records-KNQTIPQK.js.map +0 -1
- package/dist/delete-evaluator-7FVC5WM4.js.map +0 -1
- package/dist/delete-prompt-tag-ZMZUAGFW.js.map +0 -1
- package/dist/delete-workflow-EYQ3T4RQ.js.map +0 -1
- package/dist/discover-evaluator-schema-H23XCLNE.js.map +0 -1
- package/dist/discover-scenario-schema-MEEEVND7.js.map +0 -1
- package/dist/discover-schema-3T52ORPB.js.map +0 -1
- package/dist/get-agent-E3QSTVKG.js.map +0 -1
- package/dist/get-analytics-2OHV763I.js.map +0 -1
- package/dist/get-dashboard-PC7JDHH6.js.map +0 -1
- package/dist/get-dataset-H5IK5FVU.js.map +0 -1
- package/dist/get-evaluator-UWVUPYTM.js.map +0 -1
- package/dist/get-prompt-WZUOLLQC.js.map +0 -1
- package/dist/get-scenario-Q6LCNUYN.js.map +0 -1
- package/dist/get-simulation-run-Z2XKO256.js.map +0 -1
- package/dist/get-suite-BXK5TN7P.js.map +0 -1
- package/dist/get-trace-4EJAYVBG.js.map +0 -1
- package/dist/get-workflow-O34QJLHD.js.map +0 -1
- package/dist/http-server-ISHF7NJX.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/langwatch-api-annotations-ERBG27RF.js.map +0 -1
- package/dist/langwatch-api-dashboards-G6TR7VVO.js.map +0 -1
- package/dist/langwatch-api-monitors-WSXZLS2B.js.map +0 -1
- package/dist/langwatch-api-secrets-RR5DKG2X.js.map +0 -1
- package/dist/langwatch-api-triggers-2RTNSWQY.js.map +0 -1
- package/dist/langwatch-api-workflows-VJRIIDZ7.js +0 -15
- package/dist/langwatch-api-workflows-VJRIIDZ7.js.map +0 -1
- package/dist/list-agents-AW3P6FJV.js.map +0 -1
- package/dist/list-annotations-QNCGWPQ3.js.map +0 -1
- package/dist/list-dashboards-R7RK2IXL.js.map +0 -1
- package/dist/list-dataset-records-WVCSQP7P.js.map +0 -1
- package/dist/list-datasets-KEXRLDEU.js.map +0 -1
- package/dist/list-evaluators-7R2LB4JH.js.map +0 -1
- package/dist/list-model-providers-OBYJCKBX.js.map +0 -1
- package/dist/list-prompt-tags-6LY3ZPYI.js.map +0 -1
- package/dist/list-prompts-VWEZ4CDE.js.map +0 -1
- package/dist/list-scenarios-TXD4W2ZJ.js.map +0 -1
- package/dist/list-simulation-runs-RHPOLMFI.js.map +0 -1
- package/dist/list-suites-FSGBX27Z.js.map +0 -1
- package/dist/list-workflows-VRGEZ3EN.js.map +0 -1
- package/dist/rename-prompt-tag-VOJJLX4M.js.map +0 -1
- package/dist/run-agent-KTCC7TB7.js.map +0 -1
- package/dist/run-evaluation-2X3V3Q6L.js +0 -64
- package/dist/run-evaluation-2X3V3Q6L.js.map +0 -1
- package/dist/run-suite-BTMHGI2D.js.map +0 -1
- package/dist/run-workflow-IGAYGKJL.js.map +0 -1
- package/dist/search-traces-VYFUIMXH.js.map +0 -1
- package/dist/set-model-provider-W5IUI326.js.map +0 -1
- package/dist/update-agent-4BB7EYLR.js.map +0 -1
- package/dist/update-dataset-CXBKZ4W3.js.map +0 -1
- package/dist/update-dataset-record-HOHAMHXG.js.map +0 -1
- package/dist/update-evaluator-IVOR6FTR.js.map +0 -1
- package/dist/update-prompt-OWQAKE7K.js.map +0 -1
- package/dist/update-scenario-GDFVILPN.js.map +0 -1
- package/dist/update-suite-BXYUYSPT.js.map +0 -1
- package/tests/evaluations.ipynb +0 -649
- package/tests/fixtures/azure/azure_openai_stream_bot_expected.py +0 -102
- package/tests/fixtures/azure/azure_openai_stream_bot_input.py +0 -78
- package/tests/fixtures/dspy/dspy_bot_expected.py +0 -61
- package/tests/fixtures/dspy/dspy_bot_input.py +0 -53
- package/tests/fixtures/fastapi/fastapi_app_expected.py +0 -68
- package/tests/fixtures/fastapi/fastapi_app_input.py +0 -60
- package/tests/fixtures/fastapi/prompt_management_fastapi_expected.py +0 -114
- package/tests/fixtures/fastapi/prompt_management_fastapi_input.py +0 -88
- package/tests/fixtures/haystack/haystack_bot_expected.py +0 -141
- package/tests/fixtures/haystack/haystack_bot_input.py +0 -69
- package/tests/fixtures/langchain/langchain_bot_expected.py +0 -53
- package/tests/fixtures/langchain/langchain_bot_input.py +0 -45
- package/tests/fixtures/langchain/langchain_bot_with_memory_expected.py +0 -69
- package/tests/fixtures/langchain/langchain_bot_with_memory_input.py +0 -61
- package/tests/fixtures/langchain/langchain_rag_bot_expected.py +0 -97
- package/tests/fixtures/langchain/langchain_rag_bot_input.py +0 -77
- package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_expected.py +0 -116
- package/tests/fixtures/langchain/langchain_rag_bot_vertex_ai_input.py +0 -81
- package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_expected.py +0 -331
- package/tests/fixtures/langchain/langgraph_rag_bot_with_threads_input.py +0 -106
- package/tests/fixtures/litellm/litellm_bot_expected.py +0 -40
- package/tests/fixtures/litellm/litellm_bot_input.py +0 -35
- package/tests/fixtures/openai/openai_bot_expected.py +0 -43
- package/tests/fixtures/openai/openai_bot_function_call_expected.py +0 -91
- package/tests/fixtures/openai/openai_bot_function_call_input.py +0 -82
- package/tests/fixtures/openai/openai_bot_input.py +0 -36
- package/tests/fixtures/openai/openai_bot_rag_expected.py +0 -73
- package/tests/fixtures/openai/openai_bot_rag_input.py +0 -51
- package/tests/fixtures/opentelemetry/openinference_dspy_bot_expected.py +0 -63
- package/tests/fixtures/opentelemetry/openinference_dspy_bot_input.py +0 -58
- package/tests/fixtures/opentelemetry/openinference_langchain_bot_expected.py +0 -53
- package/tests/fixtures/opentelemetry/openinference_langchain_bot_input.py +0 -52
- package/tests/fixtures/opentelemetry/openinference_openai_bot_expected.py +0 -49
- package/tests/fixtures/opentelemetry/openinference_openai_bot_input.py +0 -41
- package/tests/fixtures/opentelemetry/openllmetry_openai_bot_expected.py +0 -44
- package/tests/fixtures/opentelemetry/openllmetry_openai_bot_input.py +0 -40
- package/tests/fixtures/strands/strands_bot_expected.py +0 -84
- package/tests/fixtures/strands/strands_bot_input.py +0 -52
- package/tests/scenario-openai.test.ts +0 -160
- package/tsup.config.ts +0 -14
- package/vitest.config.js +0 -7
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../langwatch-api-projects.js", () => ({
|
|
4
|
+
listProjects: vi.fn(),
|
|
5
|
+
getProject: vi.fn(),
|
|
6
|
+
createProject: vi.fn(),
|
|
7
|
+
updateProject: vi.fn(),
|
|
8
|
+
archiveProject: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("../langwatch-api-api-keys.js", () => ({
|
|
12
|
+
listApiKeys: vi.fn(),
|
|
13
|
+
createApiKey: vi.fn(),
|
|
14
|
+
revokeApiKey: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
listProjects,
|
|
19
|
+
getProject,
|
|
20
|
+
createProject,
|
|
21
|
+
updateProject,
|
|
22
|
+
archiveProject,
|
|
23
|
+
} from "../langwatch-api-projects.js";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
listApiKeys,
|
|
27
|
+
createApiKey,
|
|
28
|
+
revokeApiKey,
|
|
29
|
+
} from "../langwatch-api-api-keys.js";
|
|
30
|
+
|
|
31
|
+
import { handleListProjects } from "../tools/list-projects.js";
|
|
32
|
+
import { handleGetProject } from "../tools/get-project.js";
|
|
33
|
+
import { handleCreateProject } from "../tools/create-project.js";
|
|
34
|
+
import { handleUpdateProject } from "../tools/update-project.js";
|
|
35
|
+
import { handleArchiveProject } from "../tools/archive-project.js";
|
|
36
|
+
import { handleListApiKeys } from "../tools/list-api-keys.js";
|
|
37
|
+
import { handleCreateApiKey } from "../tools/create-api-key.js";
|
|
38
|
+
import { handleRevokeApiKey } from "../tools/revoke-api-key.js";
|
|
39
|
+
|
|
40
|
+
const mockListProjects = vi.mocked(listProjects);
|
|
41
|
+
const mockGetProject = vi.mocked(getProject);
|
|
42
|
+
const mockCreateProject = vi.mocked(createProject);
|
|
43
|
+
const mockUpdateProject = vi.mocked(updateProject);
|
|
44
|
+
const mockArchiveProject = vi.mocked(archiveProject);
|
|
45
|
+
const mockListApiKeys = vi.mocked(listApiKeys);
|
|
46
|
+
const mockCreateApiKey = vi.mocked(createApiKey);
|
|
47
|
+
const mockRevokeApiKey = vi.mocked(revokeApiKey);
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- Project Tools ---
|
|
54
|
+
|
|
55
|
+
describe("handleListProjects()", () => {
|
|
56
|
+
const sampleProjects = [
|
|
57
|
+
{
|
|
58
|
+
id: "proj_abc123",
|
|
59
|
+
name: "My Project",
|
|
60
|
+
slug: "my-project",
|
|
61
|
+
language: "python",
|
|
62
|
+
framework: "openai",
|
|
63
|
+
teamId: "team_1",
|
|
64
|
+
piiRedactionLevel: "ESSENTIAL",
|
|
65
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
66
|
+
updatedAt: "2024-06-01T00:00:00Z",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "proj_def456",
|
|
70
|
+
name: "Another Project",
|
|
71
|
+
slug: "another-project",
|
|
72
|
+
language: "typescript",
|
|
73
|
+
framework: "langchain",
|
|
74
|
+
teamId: "team_1",
|
|
75
|
+
piiRedactionLevel: "DISABLED",
|
|
76
|
+
createdAt: "2024-02-01T00:00:00Z",
|
|
77
|
+
updatedAt: "2024-07-01T00:00:00Z",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
describe("when projects exist", () => {
|
|
82
|
+
let result: string;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
mockListProjects.mockResolvedValue({
|
|
86
|
+
data: sampleProjects,
|
|
87
|
+
pagination: { page: 1, limit: 100, total: 2, totalPages: 1 },
|
|
88
|
+
});
|
|
89
|
+
result = await handleListProjects();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("includes project count", () => {
|
|
93
|
+
expect(result).toContain("2 total");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("includes project names", () => {
|
|
97
|
+
expect(result).toContain("My Project");
|
|
98
|
+
expect(result).toContain("Another Project");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("includes project IDs", () => {
|
|
102
|
+
expect(result).toContain("proj_abc123");
|
|
103
|
+
expect(result).toContain("proj_def456");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("includes language and framework", () => {
|
|
107
|
+
expect(result).toContain("python");
|
|
108
|
+
expect(result).toContain("openai");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("when no projects exist", () => {
|
|
113
|
+
it("returns empty state with tip", async () => {
|
|
114
|
+
mockListProjects.mockResolvedValue({
|
|
115
|
+
data: [],
|
|
116
|
+
pagination: { page: 1, limit: 100, total: 0, totalPages: 0 },
|
|
117
|
+
});
|
|
118
|
+
const result = await handleListProjects();
|
|
119
|
+
expect(result).toContain("No projects found");
|
|
120
|
+
expect(result).toContain("platform_create_project");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("when paginated", () => {
|
|
125
|
+
it("shows pagination hint", async () => {
|
|
126
|
+
mockListProjects.mockResolvedValue({
|
|
127
|
+
data: [sampleProjects[0]!],
|
|
128
|
+
pagination: { page: 1, limit: 1, total: 5, totalPages: 5 },
|
|
129
|
+
});
|
|
130
|
+
const result = await handleListProjects({ limit: 1 });
|
|
131
|
+
expect(result).toContain("page 1 of 5");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("handleGetProject()", () => {
|
|
137
|
+
it("returns formatted project details", async () => {
|
|
138
|
+
mockGetProject.mockResolvedValue({
|
|
139
|
+
id: "proj_abc123",
|
|
140
|
+
name: "My Project",
|
|
141
|
+
slug: "my-project",
|
|
142
|
+
language: "python",
|
|
143
|
+
framework: "openai",
|
|
144
|
+
teamId: "team_1",
|
|
145
|
+
piiRedactionLevel: "ESSENTIAL",
|
|
146
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
147
|
+
updatedAt: "2024-06-01T00:00:00Z",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await handleGetProject({ id: "proj_abc123" });
|
|
151
|
+
|
|
152
|
+
expect(result).toContain("My Project");
|
|
153
|
+
expect(result).toContain("proj_abc123");
|
|
154
|
+
expect(result).toContain("python");
|
|
155
|
+
expect(result).toContain("ESSENTIAL");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("handleCreateProject()", () => {
|
|
160
|
+
it("returns project details with service API key", async () => {
|
|
161
|
+
mockCreateProject.mockResolvedValue({
|
|
162
|
+
id: "proj_new",
|
|
163
|
+
name: "New Project",
|
|
164
|
+
slug: "new-project",
|
|
165
|
+
language: "typescript",
|
|
166
|
+
framework: "custom",
|
|
167
|
+
teamId: "team_1",
|
|
168
|
+
piiRedactionLevel: "DISABLED",
|
|
169
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
170
|
+
updatedAt: "2024-01-01T00:00:00Z",
|
|
171
|
+
serviceApiKey: "lw_sk_abc123secret",
|
|
172
|
+
serviceApiKeyId: "key_svc_1",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await handleCreateProject({
|
|
176
|
+
name: "New Project",
|
|
177
|
+
language: "typescript",
|
|
178
|
+
framework: "custom",
|
|
179
|
+
newTeamName: "My Team",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result).toContain("created successfully");
|
|
183
|
+
expect(result).toContain("New Project");
|
|
184
|
+
expect(result).toContain("proj_new");
|
|
185
|
+
expect(result).toContain("lw_sk_abc123secret");
|
|
186
|
+
expect(result).toContain("Save the service API key now");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("handleUpdateProject()", () => {
|
|
191
|
+
it("returns updated project details", async () => {
|
|
192
|
+
mockUpdateProject.mockResolvedValue({
|
|
193
|
+
id: "proj_abc123",
|
|
194
|
+
name: "Renamed Project",
|
|
195
|
+
slug: "my-project",
|
|
196
|
+
language: "python",
|
|
197
|
+
framework: "openai",
|
|
198
|
+
teamId: "team_1",
|
|
199
|
+
piiRedactionLevel: "STRICT",
|
|
200
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
201
|
+
updatedAt: "2024-08-01T00:00:00Z",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await handleUpdateProject({
|
|
205
|
+
id: "proj_abc123",
|
|
206
|
+
name: "Renamed Project",
|
|
207
|
+
piiRedactionLevel: "STRICT",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result).toContain("updated successfully");
|
|
211
|
+
expect(result).toContain("Renamed Project");
|
|
212
|
+
expect(result).toContain("STRICT");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("handleArchiveProject()", () => {
|
|
217
|
+
it("returns archive confirmation", async () => {
|
|
218
|
+
mockArchiveProject.mockResolvedValue({
|
|
219
|
+
id: "proj_abc123",
|
|
220
|
+
name: "My Project",
|
|
221
|
+
archivedAt: "2024-09-01T00:00:00Z",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await handleArchiveProject({ id: "proj_abc123" });
|
|
225
|
+
|
|
226
|
+
expect(result).toContain("archived successfully");
|
|
227
|
+
expect(result).toContain("proj_abc123");
|
|
228
|
+
expect(result).toContain("My Project");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// --- API Key Tools ---
|
|
233
|
+
|
|
234
|
+
describe("handleListApiKeys()", () => {
|
|
235
|
+
describe("when keys exist", () => {
|
|
236
|
+
it("shows key metadata and status", async () => {
|
|
237
|
+
mockListApiKeys.mockResolvedValue({
|
|
238
|
+
data: [
|
|
239
|
+
{
|
|
240
|
+
id: "key_1",
|
|
241
|
+
name: "Production Key",
|
|
242
|
+
description: "Main production API key",
|
|
243
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
244
|
+
expiresAt: null,
|
|
245
|
+
lastUsedAt: "2024-06-01T00:00:00Z",
|
|
246
|
+
revokedAt: null,
|
|
247
|
+
roleBindings: [
|
|
248
|
+
{ id: "rb_1", role: "ADMIN", scopeType: "PROJECT", scopeId: "proj_abc" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "key_2",
|
|
253
|
+
name: "Revoked Key",
|
|
254
|
+
description: null,
|
|
255
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
256
|
+
expiresAt: null,
|
|
257
|
+
lastUsedAt: null,
|
|
258
|
+
revokedAt: "2024-05-01T00:00:00Z",
|
|
259
|
+
roleBindings: [],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await handleListApiKeys();
|
|
265
|
+
|
|
266
|
+
expect(result).toContain("2 total");
|
|
267
|
+
expect(result).toContain("Production Key");
|
|
268
|
+
expect(result).toContain("ACTIVE");
|
|
269
|
+
expect(result).toContain("Revoked Key");
|
|
270
|
+
expect(result).toContain("REVOKED");
|
|
271
|
+
expect(result).toContain("ADMIN on PROJECT:proj_abc");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("when no keys exist", () => {
|
|
276
|
+
it("returns empty state with tip", async () => {
|
|
277
|
+
mockListApiKeys.mockResolvedValue({ data: [] });
|
|
278
|
+
|
|
279
|
+
const result = await handleListApiKeys();
|
|
280
|
+
|
|
281
|
+
expect(result).toContain("No API keys found");
|
|
282
|
+
expect(result).toContain("platform_create_api_key");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("handleCreateApiKey()", () => {
|
|
288
|
+
it("returns token with save warning", async () => {
|
|
289
|
+
mockCreateApiKey.mockResolvedValue({
|
|
290
|
+
token: "lw_pat_secret_token_123",
|
|
291
|
+
apiKey: {
|
|
292
|
+
id: "key_new",
|
|
293
|
+
name: "My New Key",
|
|
294
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = await handleCreateApiKey({
|
|
299
|
+
keyType: "personal",
|
|
300
|
+
name: "My New Key",
|
|
301
|
+
bindings: [
|
|
302
|
+
{ role: "ADMIN", scopeType: "ORGANIZATION", scopeId: "org_1" },
|
|
303
|
+
],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(result).toContain("created successfully");
|
|
307
|
+
expect(result).toContain("My New Key");
|
|
308
|
+
expect(result).toContain("lw_pat_secret_token_123");
|
|
309
|
+
expect(result).toContain("Save this token now");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("handleRevokeApiKey()", () => {
|
|
314
|
+
it("confirms revocation", async () => {
|
|
315
|
+
mockRevokeApiKey.mockResolvedValue({ success: true });
|
|
316
|
+
|
|
317
|
+
const result = await handleRevokeApiKey({ id: "key_123" });
|
|
318
|
+
|
|
319
|
+
expect(result).toContain("key_123");
|
|
320
|
+
expect(result).toContain("revoked successfully");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// --- Error-path tests ---
|
|
325
|
+
|
|
326
|
+
describe("handleCreateProject() validation", () => {
|
|
327
|
+
describe("when neither teamId nor newTeamName is provided", () => {
|
|
328
|
+
it("returns a validation error without calling the API", async () => {
|
|
329
|
+
const result = await handleCreateProject({
|
|
330
|
+
name: "Test Project",
|
|
331
|
+
language: "python",
|
|
332
|
+
framework: "openai",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(result).toContain("Error");
|
|
336
|
+
expect(result).toContain("teamId");
|
|
337
|
+
expect(result).toContain("newTeamName");
|
|
338
|
+
expect(mockCreateProject).not.toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("when API does not return a serviceApiKey", () => {
|
|
343
|
+
it("throws an error to prevent silent data loss", async () => {
|
|
344
|
+
mockCreateProject.mockResolvedValue({
|
|
345
|
+
id: "proj_new",
|
|
346
|
+
name: "New Project",
|
|
347
|
+
slug: "new-project",
|
|
348
|
+
language: "typescript",
|
|
349
|
+
framework: "custom",
|
|
350
|
+
teamId: "team_1",
|
|
351
|
+
piiRedactionLevel: "DISABLED",
|
|
352
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
353
|
+
updatedAt: "2024-01-01T00:00:00Z",
|
|
354
|
+
serviceApiKey: "",
|
|
355
|
+
serviceApiKeyId: "key_svc_1",
|
|
356
|
+
} as never);
|
|
357
|
+
|
|
358
|
+
await expect(
|
|
359
|
+
handleCreateProject({
|
|
360
|
+
name: "New Project",
|
|
361
|
+
language: "typescript",
|
|
362
|
+
framework: "custom",
|
|
363
|
+
newTeamName: "My Team",
|
|
364
|
+
}),
|
|
365
|
+
).rejects.toThrow("service API key");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("handleListProjects() edge cases", () => {
|
|
371
|
+
describe("when API returns non-array data", () => {
|
|
372
|
+
it("returns empty state message", async () => {
|
|
373
|
+
mockListProjects.mockResolvedValue({
|
|
374
|
+
data: null as never,
|
|
375
|
+
pagination: { page: 1, limit: 100, total: 0, totalPages: 0 },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const result = await handleListProjects();
|
|
379
|
+
|
|
380
|
+
expect(result).toContain("No projects found");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("when API call throws", () => {
|
|
385
|
+
it("propagates the error", async () => {
|
|
386
|
+
mockListProjects.mockRejectedValue(
|
|
387
|
+
new Error('LangWatch API error 403: {"error":"Forbidden","message":"Insufficient permissions"}'),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await expect(handleListProjects()).rejects.toThrow("403");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("handleListApiKeys() edge cases", () => {
|
|
396
|
+
describe("when a key has a past expiration date", () => {
|
|
397
|
+
it("shows EXPIRED status", async () => {
|
|
398
|
+
mockListApiKeys.mockResolvedValue({
|
|
399
|
+
data: [
|
|
400
|
+
{
|
|
401
|
+
id: "key_exp",
|
|
402
|
+
name: "Expired Key",
|
|
403
|
+
description: null,
|
|
404
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
405
|
+
expiresAt: "2024-06-01T00:00:00Z",
|
|
406
|
+
lastUsedAt: null,
|
|
407
|
+
revokedAt: null,
|
|
408
|
+
roleBindings: [],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const result = await handleListApiKeys();
|
|
414
|
+
|
|
415
|
+
expect(result).toContain("EXPIRED");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("when a key has a future expiration date", () => {
|
|
420
|
+
it("shows ACTIVE status", async () => {
|
|
421
|
+
mockListApiKeys.mockResolvedValue({
|
|
422
|
+
data: [
|
|
423
|
+
{
|
|
424
|
+
id: "key_future",
|
|
425
|
+
name: "Future Key",
|
|
426
|
+
description: null,
|
|
427
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
428
|
+
expiresAt: "2099-12-31T23:59:59Z",
|
|
429
|
+
lastUsedAt: null,
|
|
430
|
+
revokedAt: null,
|
|
431
|
+
roleBindings: [],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const result = await handleListApiKeys();
|
|
437
|
+
|
|
438
|
+
expect(result).toContain("ACTIVE");
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe("when API returns non-array data", () => {
|
|
443
|
+
it("returns empty state message", async () => {
|
|
444
|
+
mockListApiKeys.mockResolvedValue({ data: null as never });
|
|
445
|
+
|
|
446
|
+
const result = await handleListApiKeys();
|
|
447
|
+
|
|
448
|
+
expect(result).toContain("No API keys found");
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
});
|
|
@@ -79,6 +79,7 @@ describe("handleListScenarios()", () => {
|
|
|
79
79
|
expect(result).toContain("scen_def456");
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
/** @scenario Agent lists all scenarios in a project */
|
|
82
83
|
it("includes the total count header", () => {
|
|
83
84
|
expect(result).toContain("# Scenarios (2 total)");
|
|
84
85
|
});
|
|
@@ -92,6 +93,7 @@ describe("handleListScenarios()", () => {
|
|
|
92
93
|
result = await handleListScenarios({});
|
|
93
94
|
});
|
|
94
95
|
|
|
96
|
+
/** @scenario Agent lists scenarios when none exist */
|
|
95
97
|
it("returns a no-scenarios message", () => {
|
|
96
98
|
expect(result).toContain("No scenarios found");
|
|
97
99
|
});
|
|
@@ -130,6 +132,7 @@ describe("handleGetScenario()", () => {
|
|
|
130
132
|
result = await handleGetScenario({ scenarioId: "scen_abc123" });
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
/** @scenario Agent gets full details of a scenario */
|
|
133
136
|
it("includes the scenario name in the heading", () => {
|
|
134
137
|
expect(result).toContain("# Scenario: Login Flow Happy Path");
|
|
135
138
|
});
|