@langwatch/mcp-server 0.6.1 → 0.8.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 +89 -0
- package/README.md +51 -8
- package/dist/{archive-scenario-YFD5THOR.js → archive-scenario-YKGLJ4MZ.js} +4 -4
- package/dist/archive-suite-QFNDO3Y3.js +15 -0
- package/dist/assign-prompt-tag-Y2B4CBBT.js +19 -0
- package/dist/chunk-B57MZ4WI.js +1573 -0
- package/dist/{chunk-IX6QJKAD.js → chunk-CPUPGDNA.js} +2 -2
- package/dist/chunk-FBRKHHJV.js +48 -0
- package/dist/{chunk-5UOPNRXW.js → chunk-HDJDFKZR.js} +10 -3
- package/dist/chunk-KOO5733S.js +72 -0
- package/dist/chunk-KZNBCKWG.js +39 -0
- package/dist/chunk-L6QENXYA.js +42 -0
- package/dist/{chunk-6U4TCGFC.js → chunk-NACSR6B7.js} +2 -2
- package/dist/chunk-NUFPTHKU.js +9 -0
- package/dist/chunk-OVQF3D5R.js +86 -0
- package/dist/chunk-PO7FM7IX.js +35 -0
- package/dist/chunk-PZUWCWOS.js +25 -0
- package/dist/chunk-WVI5CJ6S.js +21 -0
- package/dist/{chunk-LLRQIF52.js → chunk-YH2IBTEL.js} +54 -6
- package/dist/chunk-YI3OG4PI.js +71 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +13 -0
- package/dist/create-agent-YRSDH7P7.js +23 -0
- package/dist/create-annotation-FYQNKMVJ.js +24 -0
- package/dist/create-dashboard-CORBR4YQ.js +18 -0
- package/dist/create-dataset-ZC63T6BB.js +22 -0
- package/dist/create-dataset-records-ILVNVKVR.js +22 -0
- package/dist/{create-evaluator-E5X5ZP3B.js → create-evaluator-2AU3PXFB.js} +4 -4
- package/dist/create-mcp-server.d.ts +8 -0
- package/dist/create-mcp-server.js +8 -0
- package/dist/{create-prompt-7Z35MIL6.js → create-prompt-NKRJ47YY.js} +7 -4
- package/dist/create-prompt-tag-S4JX3ULL.js +17 -0
- package/dist/{create-scenario-DIMPJRPY.js → create-scenario-BPRF75KF.js} +4 -4
- package/dist/create-suite-4YN7D6K2.js +38 -0
- package/dist/delete-agent-Y72R2YQA.js +18 -0
- package/dist/delete-annotation-UHWDMHSH.js +17 -0
- package/dist/delete-dashboard-LPWT36KN.js +18 -0
- package/dist/delete-dataset-GGPQRHKX.js +20 -0
- package/dist/delete-dataset-records-LTG5LT26.js +21 -0
- package/dist/delete-evaluator-F7QLXDHU.js +15 -0
- package/dist/delete-prompt-tag-B5UKZL45.js +17 -0
- package/dist/delete-workflow-4DGUDIDE.js +17 -0
- package/dist/get-agent-YEVICUX4.js +28 -0
- package/dist/{get-analytics-4YJW4S5L.js → get-analytics-3W5CMU3F.js} +22 -5
- package/dist/get-dashboard-NCQHN6MW.js +29 -0
- package/dist/get-dataset-KBK6PH36.js +48 -0
- package/dist/{get-evaluator-WDEH2F7M.js → get-evaluator-QDUL5LXR.js} +4 -4
- package/dist/{get-prompt-F6PDVC76.js → get-prompt-XCSSNRKR.js} +4 -4
- package/dist/{get-scenario-H24ZYNT5.js → get-scenario-FNRKOY7E.js} +4 -4
- package/dist/get-simulation-run-WOEKUF7K.js +56 -0
- package/dist/get-suite-U5AOD5TC.js +44 -0
- package/dist/{get-trace-27USKGO7.js → get-trace-MUWEJM2Z.js} +3 -3
- package/dist/get-workflow-SOA7MT7L.js +25 -0
- package/dist/http-server-7DMOCB75.js +224 -0
- package/dist/index.js +61640 -31868
- package/dist/langwatch-api-annotations-Q57NDUVR.js +17 -0
- package/dist/langwatch-api-dashboards-VBWWXV3Y.js +17 -0
- package/dist/langwatch-api-monitors-L3IY7FDT.js +52 -0
- package/dist/langwatch-api-secrets-S5HHF3XR.js +40 -0
- package/dist/langwatch-api-triggers-ME33QE26.js +30 -0
- package/dist/langwatch-api-workflows-4CJQAYP2.js +15 -0
- package/dist/list-agents-3HGIBJFM.js +32 -0
- package/dist/list-annotations-KWXYVEPI.js +33 -0
- package/dist/list-dashboards-VQ2LH666.js +32 -0
- package/dist/list-dataset-records-RNRGROJP.js +39 -0
- package/dist/list-datasets-I4A2LT2S.js +60 -0
- package/dist/{list-evaluators-KRGI72EH.js → list-evaluators-AKKRJ5MX.js} +4 -4
- package/dist/{list-model-providers-A5YCFTPI.js → list-model-providers-DVFEHFHZ.js} +4 -4
- package/dist/list-prompt-tags-PL5OISUA.js +23 -0
- package/dist/{list-prompts-LKJSE7XN.js → list-prompts-7VTFWE2Z.js} +3 -3
- package/dist/{list-scenarios-ZK5CMGC4.js → list-scenarios-5BZWZ6ZN.js} +4 -4
- package/dist/list-simulation-runs-WCZ6RLPE.js +43 -0
- package/dist/list-suites-RRQ2JX5Y.js +42 -0
- package/dist/list-workflows-UQB5FCX5.js +35 -0
- package/dist/rename-prompt-tag-TURD6KYU.js +18 -0
- package/dist/run-agent-BEJ4K4CM.js +36 -0
- package/dist/run-evaluation-AJ7WZNQD.js +64 -0
- package/dist/run-suite-U7V432G2.js +29 -0
- package/dist/run-workflow-5WX7DK5T.js +36 -0
- package/dist/{search-traces-SOKAAMAR.js → search-traces-ZCHNOMTX.js} +3 -3
- package/dist/{set-model-provider-7MGULZDH.js → set-model-provider-6CFHMCNE.js} +4 -4
- package/dist/update-agent-UE6UWLZP.js +20 -0
- package/dist/update-dataset-4XQZZE4S.js +22 -0
- package/dist/update-dataset-record-GNNM5TKM.js +19 -0
- package/dist/{update-evaluator-A3XINFLJ.js → update-evaluator-LHVDXIZD.js} +4 -4
- package/dist/{update-prompt-IW7X2UQM.js → update-prompt-3XEHECJ7.js} +5 -3
- package/dist/{update-scenario-ZT7TOBFR.js → update-scenario-DFPREYCD.js} +4 -4
- package/dist/update-suite-RATU5THO.js +39 -0
- package/package.json +22 -8
- package/src/__tests__/all-tools.integration.test.ts +353 -0
- package/src/__tests__/config.unit.test.ts +81 -1
- package/src/__tests__/datasets.integration.test.ts +620 -0
- package/src/__tests__/datasets.unit.test.ts +244 -0
- package/src/__tests__/docker.integration.test.ts +234 -0
- package/src/__tests__/http-transport.integration.test.ts +397 -0
- package/src/__tests__/langwatch-api.unit.test.ts +174 -0
- package/src/__tests__/scenario-tools.unit.test.ts +3 -0
- package/src/__tests__/tools.unit.test.ts +177 -0
- package/src/config.d.ts +15 -0
- package/src/config.ts +85 -7
- package/src/create-mcp-server.d.ts +6 -0
- package/src/create-mcp-server.ts +1799 -0
- package/src/http-server.ts +329 -0
- package/src/index.ts +22 -593
- package/src/langwatch-api-agents.ts +108 -0
- package/src/langwatch-api-annotations.ts +49 -0
- package/src/langwatch-api-dashboards.ts +48 -0
- package/src/langwatch-api-datasets.ts +191 -0
- package/src/langwatch-api-evaluators.ts +10 -0
- package/src/langwatch-api-monitors.ts +82 -0
- package/src/langwatch-api-secrets.ts +48 -0
- package/src/langwatch-api-simulation-runs.ts +51 -0
- package/src/langwatch-api-suites.ts +118 -0
- package/src/langwatch-api-triggers.ts +48 -0
- package/src/langwatch-api-workflows.ts +41 -0
- package/src/langwatch-api.ts +65 -3
- package/src/schemas/create-dataset.ts +48 -0
- package/src/schemas/index.ts +1 -0
- package/src/tools/archive-suite.ts +12 -0
- package/src/tools/assign-prompt-tag.ts +16 -0
- package/src/tools/create-agent.ts +15 -0
- package/src/tools/create-annotation.ts +18 -0
- package/src/tools/create-dashboard.ts +7 -0
- package/src/tools/create-dataset-records.ts +23 -0
- package/src/tools/create-dataset.ts +22 -0
- package/src/tools/create-prompt-tag.ts +12 -0
- package/src/tools/create-prompt.ts +4 -0
- package/src/tools/create-suite.ts +42 -0
- package/src/tools/delete-agent.ts +7 -0
- package/src/tools/delete-annotation.ts +7 -0
- package/src/tools/delete-dashboard.ts +7 -0
- package/src/tools/delete-dataset-records.ts +22 -0
- package/src/tools/delete-dataset.ts +20 -0
- package/src/tools/delete-evaluator.ts +12 -0
- package/src/tools/delete-prompt-tag.ts +12 -0
- package/src/tools/delete-workflow.ts +7 -0
- package/src/tools/format-dataset-mutation.ts +25 -0
- package/src/tools/get-agent.ts +21 -0
- package/src/tools/get-analytics.ts +30 -3
- package/src/tools/get-dashboard.ts +22 -0
- package/src/tools/get-dataset.ts +63 -0
- package/src/tools/get-prompt.ts +2 -1
- package/src/tools/get-simulation-run.ts +60 -0
- package/src/tools/get-suite.ts +47 -0
- package/src/tools/get-workflow.ts +17 -0
- package/src/tools/list-agents.ts +27 -0
- package/src/tools/list-annotations.ts +33 -0
- package/src/tools/list-dashboards.ts +27 -0
- package/src/tools/list-dataset-records.ts +46 -0
- package/src/tools/list-datasets.ts +80 -0
- package/src/tools/list-prompt-tags.ts +21 -0
- package/src/tools/list-simulation-runs.ts +49 -0
- package/src/tools/list-suites.ts +43 -0
- package/src/tools/list-workflows.ts +31 -0
- package/src/tools/rename-prompt-tag.ts +14 -0
- package/src/tools/run-agent.ts +36 -0
- package/src/tools/run-evaluation.ts +87 -0
- package/src/tools/run-suite.ts +28 -0
- package/src/tools/run-workflow.ts +36 -0
- package/src/tools/update-agent.ts +13 -0
- package/src/tools/update-dataset-record.ts +21 -0
- package/src/tools/update-dataset.ts +23 -0
- package/src/tools/update-prompt.ts +3 -0
- package/src/tools/update-suite.ts +44 -0
- package/src/utils/escape-markdown.ts +12 -0
- package/uv.lock +188 -530
- package/build.js +0 -12
- package/dist/archive-scenario-YFD5THOR.js.map +0 -1
- package/dist/chunk-5UOPNRXW.js.map +0 -1
- package/dist/chunk-6U4TCGFC.js.map +0 -1
- package/dist/chunk-AAQNA53E.js +0 -28
- package/dist/chunk-AAQNA53E.js.map +0 -1
- package/dist/chunk-IX6QJKAD.js.map +0 -1
- package/dist/chunk-LLRQIF52.js.map +0 -1
- package/dist/chunk-ZXKLPC2E.js.map +0 -1
- package/dist/config-FIQWQRUB.js +0 -11
- package/dist/config-FIQWQRUB.js.map +0 -1
- package/dist/create-evaluator-E5X5ZP3B.js.map +0 -1
- package/dist/create-prompt-7Z35MIL6.js.map +0 -1
- package/dist/create-scenario-DIMPJRPY.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-analytics-4YJW4S5L.js.map +0 -1
- package/dist/get-evaluator-WDEH2F7M.js.map +0 -1
- package/dist/get-prompt-F6PDVC76.js.map +0 -1
- package/dist/get-scenario-H24ZYNT5.js.map +0 -1
- package/dist/get-trace-27USKGO7.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/list-evaluators-KRGI72EH.js.map +0 -1
- package/dist/list-model-providers-A5YCFTPI.js.map +0 -1
- package/dist/list-prompts-LKJSE7XN.js.map +0 -1
- package/dist/list-scenarios-ZK5CMGC4.js.map +0 -1
- package/dist/search-traces-SOKAAMAR.js.map +0 -1
- package/dist/set-model-provider-7MGULZDH.js.map +0 -1
- package/dist/update-evaluator-A3XINFLJ.js.map +0 -1
- package/dist/update-prompt-IW7X2UQM.js.map +0 -1
- package/dist/update-scenario-ZT7TOBFR.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 -10
- package/vitest.config.js +0 -7
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { createServer, type Server } from "http";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { initConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
// --- Canned responses for dataset API endpoints ---
|
|
6
|
+
|
|
7
|
+
const CANNED_DATASETS_LIST = {
|
|
8
|
+
data: [
|
|
9
|
+
{
|
|
10
|
+
id: "ds_abc123",
|
|
11
|
+
name: "User Feedback",
|
|
12
|
+
slug: "user-feedback",
|
|
13
|
+
columnTypes: [
|
|
14
|
+
{ name: "input", type: "string" },
|
|
15
|
+
{ name: "output", type: "string" },
|
|
16
|
+
],
|
|
17
|
+
recordCount: 25,
|
|
18
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
19
|
+
updatedAt: "2025-01-02T00:00:00.000Z",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "ds_def456",
|
|
23
|
+
name: "Training Data",
|
|
24
|
+
slug: "training-data",
|
|
25
|
+
columnTypes: [{ name: "prompt", type: "string" }],
|
|
26
|
+
recordCount: 100,
|
|
27
|
+
createdAt: "2025-01-03T00:00:00.000Z",
|
|
28
|
+
updatedAt: "2025-01-04T00:00:00.000Z",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
pagination: { total: 2, page: 1, limit: 50, totalPages: 1 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CANNED_DATASETS_EMPTY = {
|
|
35
|
+
data: [],
|
|
36
|
+
pagination: { total: 0, page: 1, limit: 50, totalPages: 0 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CANNED_DATASET_DETAIL = {
|
|
40
|
+
id: "ds_abc123",
|
|
41
|
+
name: "My Dataset",
|
|
42
|
+
slug: "my-dataset",
|
|
43
|
+
columnTypes: [
|
|
44
|
+
{ name: "input", type: "string" },
|
|
45
|
+
{ name: "output", type: "string" },
|
|
46
|
+
],
|
|
47
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
48
|
+
updatedAt: "2025-01-02T00:00:00.000Z",
|
|
49
|
+
data: [
|
|
50
|
+
{ id: "rec-1", entry: { input: "hello", output: "world" } },
|
|
51
|
+
{ id: "rec-2", entry: { input: "foo", output: "bar" } },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const CANNED_DATASET_CREATED = {
|
|
56
|
+
id: "ds_new789",
|
|
57
|
+
name: "Test Data",
|
|
58
|
+
slug: "test-data",
|
|
59
|
+
columnTypes: [
|
|
60
|
+
{ name: "input", type: "string" },
|
|
61
|
+
{ name: "output", type: "string" },
|
|
62
|
+
],
|
|
63
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
64
|
+
updatedAt: "2025-01-01T00:00:00.000Z",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const CANNED_DATASET_CREATED_EMPTY = {
|
|
68
|
+
id: "ds_new790",
|
|
69
|
+
name: "Empty Schema",
|
|
70
|
+
slug: "empty-schema",
|
|
71
|
+
columnTypes: [],
|
|
72
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
73
|
+
updatedAt: "2025-01-01T00:00:00.000Z",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const CANNED_DATASET_UPDATED = {
|
|
77
|
+
id: "ds_abc123",
|
|
78
|
+
name: "New Name",
|
|
79
|
+
slug: "new-name",
|
|
80
|
+
columnTypes: [
|
|
81
|
+
{ name: "input", type: "string" },
|
|
82
|
+
{ name: "output", type: "string" },
|
|
83
|
+
],
|
|
84
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
85
|
+
updatedAt: "2025-01-05T00:00:00.000Z",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const CANNED_DATASET_UPDATED_COLUMNS = {
|
|
89
|
+
id: "ds_abc123",
|
|
90
|
+
name: "My Dataset",
|
|
91
|
+
slug: "my-dataset",
|
|
92
|
+
columnTypes: [{ name: "question", type: "string" }],
|
|
93
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
94
|
+
updatedAt: "2025-01-05T00:00:00.000Z",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const CANNED_DATASET_ARCHIVED = {
|
|
98
|
+
id: "ds_abc123",
|
|
99
|
+
archived: true,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const CANNED_RECORDS_CREATED = {
|
|
103
|
+
data: [
|
|
104
|
+
{ id: "rec-new-1", entry: { input: "hello", output: "world" } },
|
|
105
|
+
{ id: "rec-new-2", entry: { input: "foo", output: "bar" } },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const CANNED_RECORD_UPDATED = {
|
|
110
|
+
id: "rec-123",
|
|
111
|
+
entry: { input: "updated" },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const CANNED_RECORDS_DELETED = {
|
|
115
|
+
deletedCount: 2,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// --- Mock HTTP Server ---
|
|
119
|
+
|
|
120
|
+
let emptyListMode = false;
|
|
121
|
+
|
|
122
|
+
function createMockServer(): Server {
|
|
123
|
+
return createServer((req, res) => {
|
|
124
|
+
const authToken = req.headers["x-auth-token"];
|
|
125
|
+
if (authToken !== "test-integration-key") {
|
|
126
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
127
|
+
res.end(JSON.stringify({ message: "Invalid auth token." }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let body = "";
|
|
132
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
133
|
+
req.on("end", () => {
|
|
134
|
+
const url = req.url ?? "";
|
|
135
|
+
res.setHeader("Content-Type", "application/json");
|
|
136
|
+
|
|
137
|
+
// GET /api/dataset - list datasets
|
|
138
|
+
if (url.match(/^\/api\/dataset(\?|$)/) && req.method === "GET") {
|
|
139
|
+
if (emptyListMode) {
|
|
140
|
+
res.writeHead(200);
|
|
141
|
+
res.end(JSON.stringify(CANNED_DATASETS_EMPTY));
|
|
142
|
+
} else {
|
|
143
|
+
res.writeHead(200);
|
|
144
|
+
res.end(JSON.stringify(CANNED_DATASETS_LIST));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// GET /api/dataset/my-dataset - get dataset detail
|
|
148
|
+
else if (
|
|
149
|
+
url.match(/^\/api\/dataset\/my-dataset(\?|$)/) &&
|
|
150
|
+
req.method === "GET"
|
|
151
|
+
) {
|
|
152
|
+
res.writeHead(200);
|
|
153
|
+
res.end(JSON.stringify(CANNED_DATASET_DETAIL));
|
|
154
|
+
}
|
|
155
|
+
// GET /api/dataset/does-not-exist - not found
|
|
156
|
+
else if (
|
|
157
|
+
url.match(/^\/api\/dataset\/does-not-exist(\?|$)/) &&
|
|
158
|
+
req.method === "GET"
|
|
159
|
+
) {
|
|
160
|
+
res.writeHead(404);
|
|
161
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
162
|
+
}
|
|
163
|
+
// POST /api/dataset - create dataset
|
|
164
|
+
else if (url === "/api/dataset" && req.method === "POST") {
|
|
165
|
+
const parsed = JSON.parse(body);
|
|
166
|
+
if (
|
|
167
|
+
parsed.columnTypes &&
|
|
168
|
+
Array.isArray(parsed.columnTypes) &&
|
|
169
|
+
parsed.columnTypes.length > 0
|
|
170
|
+
) {
|
|
171
|
+
res.writeHead(201);
|
|
172
|
+
res.end(JSON.stringify(CANNED_DATASET_CREATED));
|
|
173
|
+
} else {
|
|
174
|
+
res.writeHead(201);
|
|
175
|
+
res.end(JSON.stringify(CANNED_DATASET_CREATED_EMPTY));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// PATCH /api/dataset/old-name - update dataset name
|
|
179
|
+
else if (
|
|
180
|
+
url.match(/^\/api\/dataset\/old-name$/) &&
|
|
181
|
+
req.method === "PATCH"
|
|
182
|
+
) {
|
|
183
|
+
res.writeHead(200);
|
|
184
|
+
res.end(JSON.stringify(CANNED_DATASET_UPDATED));
|
|
185
|
+
}
|
|
186
|
+
// PATCH /api/dataset/my-dataset - update dataset columns
|
|
187
|
+
else if (
|
|
188
|
+
url.match(/^\/api\/dataset\/my-dataset$/) &&
|
|
189
|
+
req.method === "PATCH"
|
|
190
|
+
) {
|
|
191
|
+
res.writeHead(200);
|
|
192
|
+
res.end(JSON.stringify(CANNED_DATASET_UPDATED_COLUMNS));
|
|
193
|
+
}
|
|
194
|
+
// PATCH /api/dataset/ghost - not found
|
|
195
|
+
else if (
|
|
196
|
+
url.match(/^\/api\/dataset\/ghost$/) &&
|
|
197
|
+
req.method === "PATCH"
|
|
198
|
+
) {
|
|
199
|
+
res.writeHead(404);
|
|
200
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
201
|
+
}
|
|
202
|
+
// DELETE /api/dataset/to-delete - archive dataset
|
|
203
|
+
else if (
|
|
204
|
+
url.match(/^\/api\/dataset\/to-delete$/) &&
|
|
205
|
+
req.method === "DELETE"
|
|
206
|
+
) {
|
|
207
|
+
res.writeHead(200);
|
|
208
|
+
res.end(JSON.stringify(CANNED_DATASET_ARCHIVED));
|
|
209
|
+
}
|
|
210
|
+
// DELETE /api/dataset/ghost - not found
|
|
211
|
+
else if (
|
|
212
|
+
url.match(/^\/api\/dataset\/ghost$/) &&
|
|
213
|
+
req.method === "DELETE"
|
|
214
|
+
) {
|
|
215
|
+
res.writeHead(404);
|
|
216
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
217
|
+
}
|
|
218
|
+
// POST /api/dataset/my-dataset/records - create records
|
|
219
|
+
else if (
|
|
220
|
+
url.match(/^\/api\/dataset\/my-dataset\/records$/) &&
|
|
221
|
+
req.method === "POST"
|
|
222
|
+
) {
|
|
223
|
+
res.writeHead(201);
|
|
224
|
+
res.end(JSON.stringify(CANNED_RECORDS_CREATED));
|
|
225
|
+
}
|
|
226
|
+
// POST /api/dataset/ghost/records - not found
|
|
227
|
+
else if (
|
|
228
|
+
url.match(/^\/api\/dataset\/ghost\/records$/) &&
|
|
229
|
+
req.method === "POST"
|
|
230
|
+
) {
|
|
231
|
+
res.writeHead(404);
|
|
232
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
233
|
+
}
|
|
234
|
+
// PATCH /api/dataset/my-dataset/records/rec-123 - update record
|
|
235
|
+
else if (
|
|
236
|
+
url.match(/^\/api\/dataset\/my-dataset\/records\/rec-123$/) &&
|
|
237
|
+
req.method === "PATCH"
|
|
238
|
+
) {
|
|
239
|
+
res.writeHead(200);
|
|
240
|
+
res.end(JSON.stringify(CANNED_RECORD_UPDATED));
|
|
241
|
+
}
|
|
242
|
+
// PATCH /api/dataset/ghost/records/rec-1 - not found
|
|
243
|
+
else if (
|
|
244
|
+
url.match(/^\/api\/dataset\/ghost\/records\//) &&
|
|
245
|
+
req.method === "PATCH"
|
|
246
|
+
) {
|
|
247
|
+
res.writeHead(404);
|
|
248
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
249
|
+
}
|
|
250
|
+
// DELETE /api/dataset/my-dataset/records - delete records
|
|
251
|
+
else if (
|
|
252
|
+
url.match(/^\/api\/dataset\/my-dataset\/records$/) &&
|
|
253
|
+
req.method === "DELETE"
|
|
254
|
+
) {
|
|
255
|
+
res.writeHead(200);
|
|
256
|
+
res.end(JSON.stringify(CANNED_RECORDS_DELETED));
|
|
257
|
+
}
|
|
258
|
+
// DELETE /api/dataset/ghost/records - not found
|
|
259
|
+
else if (
|
|
260
|
+
url.match(/^\/api\/dataset\/ghost\/records$/) &&
|
|
261
|
+
req.method === "DELETE"
|
|
262
|
+
) {
|
|
263
|
+
res.writeHead(404);
|
|
264
|
+
res.end(JSON.stringify({ message: "Dataset not found" }));
|
|
265
|
+
} else {
|
|
266
|
+
res.writeHead(404);
|
|
267
|
+
res.end(
|
|
268
|
+
JSON.stringify({ message: `Not found: ${req.method} ${url}` }),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Integration Tests ---
|
|
276
|
+
|
|
277
|
+
describe("MCP dataset tools integration", () => {
|
|
278
|
+
let server: Server;
|
|
279
|
+
let port: number;
|
|
280
|
+
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
emptyListMode = false;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
beforeAll(async () => {
|
|
286
|
+
server = createMockServer();
|
|
287
|
+
await new Promise<void>((resolve) => {
|
|
288
|
+
server.listen(0, () => {
|
|
289
|
+
const addr = server.address();
|
|
290
|
+
port = typeof addr === "object" && addr ? addr.port : 0;
|
|
291
|
+
initConfig({
|
|
292
|
+
apiKey: "test-integration-key",
|
|
293
|
+
endpoint: `http://localhost:${port}`,
|
|
294
|
+
});
|
|
295
|
+
resolve();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
afterAll(async () => {
|
|
301
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── List Datasets ──────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
describe("platform_list_datasets", () => {
|
|
307
|
+
describe("when the project has datasets", () => {
|
|
308
|
+
/** @scenario "List datasets returns a formatted summary of all datasets" */
|
|
309
|
+
it("returns a formatted list showing both datasets with their names, slugs, and record counts", async () => {
|
|
310
|
+
const { handleListDatasets } = await import(
|
|
311
|
+
"../tools/list-datasets.js"
|
|
312
|
+
);
|
|
313
|
+
const result = await handleListDatasets();
|
|
314
|
+
expect(result).toContain("User Feedback");
|
|
315
|
+
expect(result).toContain("Training Data");
|
|
316
|
+
expect(result).toContain("user-feedback");
|
|
317
|
+
expect(result).toContain("training-data");
|
|
318
|
+
expect(result).toContain("25");
|
|
319
|
+
expect(result).toContain("100");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("when the project has no datasets", () => {
|
|
324
|
+
/** @scenario "List datasets returns a helpful message when none exist" */
|
|
325
|
+
it("returns a helpful message indicating no datasets were found", async () => {
|
|
326
|
+
emptyListMode = true;
|
|
327
|
+
const { handleListDatasets } = await import(
|
|
328
|
+
"../tools/list-datasets.js"
|
|
329
|
+
);
|
|
330
|
+
const result = await handleListDatasets();
|
|
331
|
+
expect(result).toContain("No datasets found");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("suggests using platform_create_dataset", async () => {
|
|
335
|
+
emptyListMode = true;
|
|
336
|
+
const { handleListDatasets } = await import(
|
|
337
|
+
"../tools/list-datasets.js"
|
|
338
|
+
);
|
|
339
|
+
const result = await handleListDatasets();
|
|
340
|
+
expect(result).toContain("platform_create_dataset");
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("when format is json", () => {
|
|
345
|
+
it("returns parseable JSON containing all datasets and total", async () => {
|
|
346
|
+
const { handleListDatasets } = await import(
|
|
347
|
+
"../tools/list-datasets.js"
|
|
348
|
+
);
|
|
349
|
+
const result = await handleListDatasets({ format: "json" });
|
|
350
|
+
const parsed = JSON.parse(result);
|
|
351
|
+
expect(parsed.data).toEqual(CANNED_DATASETS_LIST.data);
|
|
352
|
+
expect(parsed.total).toBe(CANNED_DATASETS_LIST.pagination.total);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── Get Dataset ────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
describe("platform_get_dataset", () => {
|
|
360
|
+
describe("when the dataset exists", () => {
|
|
361
|
+
/** @scenario "Get dataset by slug returns metadata and a preview of records" */
|
|
362
|
+
it("returns the dataset name, slug, and column definitions", async () => {
|
|
363
|
+
const { handleGetDataset } = await import(
|
|
364
|
+
"../tools/get-dataset.js"
|
|
365
|
+
);
|
|
366
|
+
const result = await handleGetDataset({ slugOrId: "my-dataset" });
|
|
367
|
+
expect(result).toContain("My Dataset");
|
|
368
|
+
expect(result).toContain("my-dataset");
|
|
369
|
+
expect(result).toContain("input");
|
|
370
|
+
expect(result).toContain("output");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns a preview of records", async () => {
|
|
374
|
+
const { handleGetDataset } = await import(
|
|
375
|
+
"../tools/get-dataset.js"
|
|
376
|
+
);
|
|
377
|
+
const result = await handleGetDataset({ slugOrId: "my-dataset" });
|
|
378
|
+
expect(result).toContain("hello");
|
|
379
|
+
expect(result).toContain("world");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("when format is json", () => {
|
|
384
|
+
it("returns parseable JSON matching the API response", async () => {
|
|
385
|
+
const { handleGetDataset } = await import(
|
|
386
|
+
"../tools/get-dataset.js"
|
|
387
|
+
);
|
|
388
|
+
const result = await handleGetDataset({ slugOrId: "my-dataset", format: "json" });
|
|
389
|
+
expect(JSON.parse(result)).toEqual(CANNED_DATASET_DETAIL);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("when the dataset does not exist", () => {
|
|
394
|
+
/** @scenario "Get dataset with non-existent slug returns an error" */
|
|
395
|
+
it("propagates the 404 error", async () => {
|
|
396
|
+
const { handleGetDataset } = await import(
|
|
397
|
+
"../tools/get-dataset.js"
|
|
398
|
+
);
|
|
399
|
+
await expect(
|
|
400
|
+
handleGetDataset({ slugOrId: "does-not-exist" }),
|
|
401
|
+
).rejects.toThrow("404");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── Create Dataset ─────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
describe("platform_create_dataset", () => {
|
|
409
|
+
describe("when creating with name and columns", () => {
|
|
410
|
+
/** @scenario "Create a dataset with name and columns" */
|
|
411
|
+
it("returns confirmation including the generated slug", async () => {
|
|
412
|
+
const { handleCreateDataset } = await import(
|
|
413
|
+
"../tools/create-dataset.js"
|
|
414
|
+
);
|
|
415
|
+
const result = await handleCreateDataset({
|
|
416
|
+
name: "Test Data",
|
|
417
|
+
columnTypes: [
|
|
418
|
+
{ name: "input", type: "string" },
|
|
419
|
+
{ name: "output", type: "string" },
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
expect(result).toContain("test-data");
|
|
423
|
+
expect(result).toContain("created");
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe("when creating with only a name", () => {
|
|
428
|
+
/** @scenario "Create a dataset with only a name and no columns" */
|
|
429
|
+
it("returns confirmation including the slug", async () => {
|
|
430
|
+
const { handleCreateDataset } = await import(
|
|
431
|
+
"../tools/create-dataset.js"
|
|
432
|
+
);
|
|
433
|
+
const result = await handleCreateDataset({
|
|
434
|
+
name: "Empty Schema",
|
|
435
|
+
});
|
|
436
|
+
expect(result).toContain("empty-schema");
|
|
437
|
+
expect(result).toContain("created");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ── Update Dataset ─────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
describe("platform_update_dataset", () => {
|
|
445
|
+
describe("when updating the dataset name", () => {
|
|
446
|
+
/** @scenario "Update a dataset name" */
|
|
447
|
+
it("returns confirmation reflecting the new name", async () => {
|
|
448
|
+
const { handleUpdateDataset } = await import(
|
|
449
|
+
"../tools/update-dataset.js"
|
|
450
|
+
);
|
|
451
|
+
const result = await handleUpdateDataset({
|
|
452
|
+
slugOrId: "old-name",
|
|
453
|
+
name: "New Name",
|
|
454
|
+
});
|
|
455
|
+
expect(result).toContain("New Name");
|
|
456
|
+
expect(result).toContain("updated");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("when updating dataset column types", () => {
|
|
461
|
+
/** @scenario "Update a dataset column types" */
|
|
462
|
+
it("returns confirmation reflecting the new columns", async () => {
|
|
463
|
+
const { handleUpdateDataset } = await import(
|
|
464
|
+
"../tools/update-dataset.js"
|
|
465
|
+
);
|
|
466
|
+
const result = await handleUpdateDataset({
|
|
467
|
+
slugOrId: "my-dataset",
|
|
468
|
+
columnTypes: [{ name: "question", type: "string" }],
|
|
469
|
+
});
|
|
470
|
+
expect(result).toContain("question");
|
|
471
|
+
expect(result).toContain("updated");
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("when the dataset does not exist", () => {
|
|
476
|
+
/** @scenario "Update a non-existent dataset returns an error" */
|
|
477
|
+
it("propagates the 404 error", async () => {
|
|
478
|
+
const { handleUpdateDataset } = await import(
|
|
479
|
+
"../tools/update-dataset.js"
|
|
480
|
+
);
|
|
481
|
+
await expect(
|
|
482
|
+
handleUpdateDataset({ slugOrId: "ghost", name: "Whatever" }),
|
|
483
|
+
).rejects.toThrow("404");
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ── Delete Dataset ─────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
describe("platform_delete_dataset", () => {
|
|
491
|
+
describe("when the dataset exists", () => {
|
|
492
|
+
/** @scenario "Delete a dataset archives it" */
|
|
493
|
+
it("returns confirmation that the dataset was deleted", async () => {
|
|
494
|
+
const { handleDeleteDataset } = await import(
|
|
495
|
+
"../tools/delete-dataset.js"
|
|
496
|
+
);
|
|
497
|
+
const result = await handleDeleteDataset({ slugOrId: "to-delete" });
|
|
498
|
+
expect(result).toContain("deleted");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("when the dataset does not exist", () => {
|
|
503
|
+
/** @scenario "Delete a non-existent dataset returns an error" */
|
|
504
|
+
it("propagates the 404 error", async () => {
|
|
505
|
+
const { handleDeleteDataset } = await import(
|
|
506
|
+
"../tools/delete-dataset.js"
|
|
507
|
+
);
|
|
508
|
+
await expect(
|
|
509
|
+
handleDeleteDataset({ slugOrId: "ghost" }),
|
|
510
|
+
).rejects.toThrow("404");
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ── Create Records ─────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
describe("platform_create_dataset_records", () => {
|
|
518
|
+
describe("when the dataset exists", () => {
|
|
519
|
+
/** @scenario "Add records to a dataset" */
|
|
520
|
+
it("returns confirmation with the count of records created", async () => {
|
|
521
|
+
const { handleCreateDatasetRecords } = await import(
|
|
522
|
+
"../tools/create-dataset-records.js"
|
|
523
|
+
);
|
|
524
|
+
const result = await handleCreateDatasetRecords({
|
|
525
|
+
slugOrId: "my-dataset",
|
|
526
|
+
entries: [
|
|
527
|
+
{ input: "hello", output: "world" },
|
|
528
|
+
{ input: "foo", output: "bar" },
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
expect(result).toContain("2");
|
|
532
|
+
expect(result).toContain("created");
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe("when the dataset does not exist", () => {
|
|
537
|
+
/** @scenario "Add records to a non-existent dataset returns an error" */
|
|
538
|
+
it("propagates the 404 error", async () => {
|
|
539
|
+
const { handleCreateDatasetRecords } = await import(
|
|
540
|
+
"../tools/create-dataset-records.js"
|
|
541
|
+
);
|
|
542
|
+
await expect(
|
|
543
|
+
handleCreateDatasetRecords({
|
|
544
|
+
slugOrId: "ghost",
|
|
545
|
+
entries: [{ input: "hello" }],
|
|
546
|
+
}),
|
|
547
|
+
).rejects.toThrow("404");
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ── Update Record ──────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
describe("platform_update_dataset_record", () => {
|
|
555
|
+
describe("when the record exists", () => {
|
|
556
|
+
/** @scenario "Update a single record entry" */
|
|
557
|
+
it("returns confirmation that the record was updated", async () => {
|
|
558
|
+
const { handleUpdateDatasetRecord } = await import(
|
|
559
|
+
"../tools/update-dataset-record.js"
|
|
560
|
+
);
|
|
561
|
+
const result = await handleUpdateDatasetRecord({
|
|
562
|
+
slugOrId: "my-dataset",
|
|
563
|
+
recordId: "rec-123",
|
|
564
|
+
entry: { input: "updated" },
|
|
565
|
+
});
|
|
566
|
+
expect(result).toContain("updated");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("when the dataset does not exist", () => {
|
|
571
|
+
/** @scenario "Update a record in a non-existent dataset returns an error" */
|
|
572
|
+
it("propagates the 404 error", async () => {
|
|
573
|
+
const { handleUpdateDatasetRecord } = await import(
|
|
574
|
+
"../tools/update-dataset-record.js"
|
|
575
|
+
);
|
|
576
|
+
await expect(
|
|
577
|
+
handleUpdateDatasetRecord({
|
|
578
|
+
slugOrId: "ghost",
|
|
579
|
+
recordId: "rec-1",
|
|
580
|
+
entry: { input: "x" },
|
|
581
|
+
}),
|
|
582
|
+
).rejects.toThrow("404");
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ── Delete Records ─────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
describe("platform_delete_dataset_records", () => {
|
|
590
|
+
describe("when the dataset exists", () => {
|
|
591
|
+
/** @scenario "Delete records by IDs" */
|
|
592
|
+
it("returns confirmation with the count of records deleted", async () => {
|
|
593
|
+
const { handleDeleteDatasetRecords } = await import(
|
|
594
|
+
"../tools/delete-dataset-records.js"
|
|
595
|
+
);
|
|
596
|
+
const result = await handleDeleteDatasetRecords({
|
|
597
|
+
slugOrId: "my-dataset",
|
|
598
|
+
recordIds: ["rec-1", "rec-2"],
|
|
599
|
+
});
|
|
600
|
+
expect(result).toContain("2");
|
|
601
|
+
expect(result).toContain("deleted");
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe("when the dataset does not exist", () => {
|
|
606
|
+
/** @scenario "Delete records from a non-existent dataset returns an error" */
|
|
607
|
+
it("propagates the 404 error", async () => {
|
|
608
|
+
const { handleDeleteDatasetRecords } = await import(
|
|
609
|
+
"../tools/delete-dataset-records.js"
|
|
610
|
+
);
|
|
611
|
+
await expect(
|
|
612
|
+
handleDeleteDatasetRecords({
|
|
613
|
+
slugOrId: "ghost",
|
|
614
|
+
recordIds: ["rec-1"],
|
|
615
|
+
}),
|
|
616
|
+
).rejects.toThrow("404");
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|