@langwatch/mcp-server 0.4.0 → 0.6.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/CHANGELOG.md +44 -0
- package/dist/archive-scenario-YFD5THOR.js +19 -0
- package/dist/archive-scenario-YFD5THOR.js.map +1 -0
- package/dist/chunk-5UOPNRXW.js +37 -0
- package/dist/chunk-5UOPNRXW.js.map +1 -0
- package/dist/chunk-6U4TCGFC.js +40 -0
- package/dist/chunk-6U4TCGFC.js.map +1 -0
- package/dist/chunk-IX6QJKAD.js +22 -0
- package/dist/chunk-IX6QJKAD.js.map +1 -0
- package/dist/{chunk-HOPTUDCZ.js → chunk-LLRQIF52.js} +5 -12
- package/dist/chunk-LLRQIF52.js.map +1 -0
- package/dist/create-evaluator-E5X5ZP3B.js +27 -0
- package/dist/create-evaluator-E5X5ZP3B.js.map +1 -0
- package/dist/create-prompt-7Z35MIL6.js +36 -0
- package/dist/create-prompt-7Z35MIL6.js.map +1 -0
- package/dist/create-scenario-DIMPJRPY.js +26 -0
- package/dist/create-scenario-DIMPJRPY.js.map +1 -0
- package/dist/discover-evaluator-schema-H23XCLNE.js +1402 -0
- package/dist/discover-evaluator-schema-H23XCLNE.js.map +1 -0
- package/dist/discover-scenario-schema-MEEEVND7.js +65 -0
- package/dist/discover-scenario-schema-MEEEVND7.js.map +1 -0
- package/dist/{get-analytics-3IFTN6MY.js → get-analytics-4YJW4S5L.js} +2 -2
- package/dist/get-evaluator-WDEH2F7M.js +47 -0
- package/dist/get-evaluator-WDEH2F7M.js.map +1 -0
- package/dist/{get-prompt-2ZB5B3QC.js → get-prompt-F6PDVC76.js} +2 -5
- package/dist/get-prompt-F6PDVC76.js.map +1 -0
- package/dist/get-scenario-H24ZYNT5.js +33 -0
- package/dist/get-scenario-H24ZYNT5.js.map +1 -0
- package/dist/{get-trace-7IXKKCJJ.js → get-trace-27USKGO7.js} +2 -2
- package/dist/index.js +27066 -8845
- package/dist/index.js.map +1 -1
- package/dist/list-evaluators-KRGI72EH.js +34 -0
- package/dist/list-evaluators-KRGI72EH.js.map +1 -0
- package/dist/list-model-providers-A5YCFTPI.js +35 -0
- package/dist/list-model-providers-A5YCFTPI.js.map +1 -0
- package/dist/{list-prompts-J72LTP7Z.js → list-prompts-LKJSE7XN.js} +6 -7
- package/dist/list-prompts-LKJSE7XN.js.map +1 -0
- package/dist/list-scenarios-ZK5CMGC4.js +40 -0
- package/dist/list-scenarios-ZK5CMGC4.js.map +1 -0
- package/dist/{search-traces-RW2NDHN5.js → search-traces-SOKAAMAR.js} +2 -2
- package/dist/set-model-provider-7MGULZDH.js +33 -0
- package/dist/set-model-provider-7MGULZDH.js.map +1 -0
- package/dist/update-evaluator-A3XINFLJ.js +24 -0
- package/dist/update-evaluator-A3XINFLJ.js.map +1 -0
- package/dist/update-prompt-IW7X2UQM.js +22 -0
- package/dist/update-prompt-IW7X2UQM.js.map +1 -0
- package/dist/update-scenario-ZT7TOBFR.js +27 -0
- package/dist/update-scenario-ZT7TOBFR.js.map +1 -0
- package/package.json +11 -11
- package/src/__tests__/all-tools.integration.test.ts +1337 -0
- package/src/__tests__/discover-evaluator-schema.unit.test.ts +89 -0
- package/src/__tests__/evaluator-tools.unit.test.ts +262 -0
- package/src/__tests__/integration.integration.test.ts +9 -34
- package/src/__tests__/langwatch-api.unit.test.ts +4 -32
- package/src/__tests__/model-provider-tools.unit.test.ts +190 -0
- package/src/__tests__/scenario-tools.integration.test.ts +286 -0
- package/src/__tests__/scenario-tools.unit.test.ts +185 -0
- package/src/__tests__/tools.unit.test.ts +59 -65
- package/src/index.ts +338 -48
- package/src/langwatch-api-evaluators.ts +70 -0
- package/src/langwatch-api-model-providers.ts +41 -0
- package/src/langwatch-api-scenarios.ts +67 -0
- package/src/langwatch-api.ts +6 -30
- package/src/tools/archive-scenario.ts +19 -0
- package/src/tools/create-evaluator.ts +33 -0
- package/src/tools/create-prompt.ts +30 -5
- package/src/tools/create-scenario.ts +30 -0
- package/src/tools/discover-evaluator-schema.ts +143 -0
- package/src/tools/discover-scenario-schema.ts +71 -0
- package/src/tools/get-evaluator.ts +53 -0
- package/src/tools/get-prompt.ts +1 -4
- package/src/tools/get-scenario.ts +36 -0
- package/src/tools/list-evaluators.ts +37 -0
- package/src/tools/list-model-providers.ts +40 -0
- package/src/tools/list-prompts.ts +5 -6
- package/src/tools/list-scenarios.ts +47 -0
- package/src/tools/set-model-provider.ts +46 -0
- package/src/tools/update-evaluator.ts +30 -0
- package/src/tools/update-prompt.ts +9 -25
- package/src/tools/update-scenario.ts +32 -0
- package/uv.lock +1788 -1322
- package/dist/chunk-HOPTUDCZ.js.map +0 -1
- package/dist/create-prompt-UBC537BJ.js +0 -22
- package/dist/create-prompt-UBC537BJ.js.map +0 -1
- package/dist/get-prompt-2ZB5B3QC.js.map +0 -1
- package/dist/list-prompts-J72LTP7Z.js.map +0 -1
- package/dist/update-prompt-G6HHZSUM.js +0 -31
- package/dist/update-prompt-G6HHZSUM.js.map +0 -1
- /package/dist/{get-analytics-3IFTN6MY.js.map → get-analytics-4YJW4S5L.js.map} +0 -0
- /package/dist/{get-trace-7IXKKCJJ.js.map → get-trace-27USKGO7.js.map} +0 -0
- /package/dist/{search-traces-RW2NDHN5.js.map → search-traces-SOKAAMAR.js.map} +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../langwatch-api-model-providers.js", () => ({
|
|
4
|
+
listModelProviders: vi.fn(),
|
|
5
|
+
setModelProvider: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
listModelProviders,
|
|
10
|
+
setModelProvider,
|
|
11
|
+
} from "../langwatch-api-model-providers.js";
|
|
12
|
+
|
|
13
|
+
import { handleListModelProviders } from "../tools/list-model-providers.js";
|
|
14
|
+
import { handleSetModelProvider } from "../tools/set-model-provider.js";
|
|
15
|
+
|
|
16
|
+
const mockListModelProviders = vi.mocked(listModelProviders);
|
|
17
|
+
const mockSetModelProvider = vi.mocked(setModelProvider);
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("handleListModelProviders()", () => {
|
|
24
|
+
const sampleProviders = {
|
|
25
|
+
openai: {
|
|
26
|
+
provider: "openai",
|
|
27
|
+
enabled: true,
|
|
28
|
+
customKeys: { OPENAI_API_KEY: "HAS_KEY" },
|
|
29
|
+
models: ["gpt-4o", "gpt-4o-mini"],
|
|
30
|
+
embeddingsModels: ["text-embedding-3-small"],
|
|
31
|
+
deploymentMapping: null,
|
|
32
|
+
extraHeaders: [],
|
|
33
|
+
},
|
|
34
|
+
anthropic: {
|
|
35
|
+
provider: "anthropic",
|
|
36
|
+
enabled: false,
|
|
37
|
+
customKeys: null,
|
|
38
|
+
models: ["claude-sonnet-4-5-20250929"],
|
|
39
|
+
embeddingsModels: null,
|
|
40
|
+
deploymentMapping: null,
|
|
41
|
+
extraHeaders: [],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("when providers exist", () => {
|
|
46
|
+
let result: string;
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
mockListModelProviders.mockResolvedValue(sampleProviders);
|
|
50
|
+
result = await handleListModelProviders();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("includes the total count header", () => {
|
|
54
|
+
expect(result).toContain("# Model Providers (2 total)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("includes provider name", () => {
|
|
58
|
+
expect(result).toContain("## openai");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows enabled status", () => {
|
|
62
|
+
expect(result).toContain("**Status**: enabled");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shows disabled status", () => {
|
|
66
|
+
expect(result).toContain("**Status**: disabled");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("shows key fields that are set", () => {
|
|
70
|
+
expect(result).toContain("OPENAI_API_KEY: set");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows model count", () => {
|
|
74
|
+
expect(result).toContain("2 available");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("when no providers exist", () => {
|
|
79
|
+
let result: string;
|
|
80
|
+
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
mockListModelProviders.mockResolvedValue({});
|
|
83
|
+
result = await handleListModelProviders();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns a no-providers message", () => {
|
|
87
|
+
expect(result).toContain("No model providers configured");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("includes a tip to use platform_set_model_provider", () => {
|
|
91
|
+
expect(result).toContain("platform_set_model_provider");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("handleSetModelProvider()", () => {
|
|
97
|
+
describe("when update succeeds", () => {
|
|
98
|
+
let result: string;
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
mockSetModelProvider.mockResolvedValue({
|
|
102
|
+
openai: {
|
|
103
|
+
provider: "openai",
|
|
104
|
+
enabled: true,
|
|
105
|
+
customKeys: { OPENAI_API_KEY: "HAS_KEY" },
|
|
106
|
+
models: ["gpt-4o"],
|
|
107
|
+
embeddingsModels: null,
|
|
108
|
+
deploymentMapping: null,
|
|
109
|
+
extraHeaders: [],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
result = await handleSetModelProvider({
|
|
113
|
+
provider: "openai",
|
|
114
|
+
enabled: true,
|
|
115
|
+
customKeys: { OPENAI_API_KEY: "sk-test123" },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("confirms update", () => {
|
|
120
|
+
expect(result).toContain("Model provider updated successfully!");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("includes provider name", () => {
|
|
124
|
+
expect(result).toContain("**Provider**: openai");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("shows enabled status", () => {
|
|
128
|
+
expect(result).toContain("**Status**: enabled");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("shows key fields", () => {
|
|
132
|
+
expect(result).toContain("OPENAI_API_KEY: set");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("when setting default model without provider prefix", () => {
|
|
137
|
+
let result: string;
|
|
138
|
+
|
|
139
|
+
beforeEach(async () => {
|
|
140
|
+
mockSetModelProvider.mockResolvedValue({
|
|
141
|
+
openai: {
|
|
142
|
+
provider: "openai",
|
|
143
|
+
enabled: true,
|
|
144
|
+
customKeys: null,
|
|
145
|
+
models: ["gpt-4o"],
|
|
146
|
+
embeddingsModels: null,
|
|
147
|
+
deploymentMapping: null,
|
|
148
|
+
extraHeaders: [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
result = await handleSetModelProvider({
|
|
152
|
+
provider: "openai",
|
|
153
|
+
enabled: true,
|
|
154
|
+
defaultModel: "gpt-4o",
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("prepends provider prefix in response", () => {
|
|
159
|
+
expect(result).toContain("**Default Model**: openai/gpt-4o");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("when setting default model with provider prefix already", () => {
|
|
164
|
+
let result: string;
|
|
165
|
+
|
|
166
|
+
beforeEach(async () => {
|
|
167
|
+
mockSetModelProvider.mockResolvedValue({
|
|
168
|
+
openai: {
|
|
169
|
+
provider: "openai",
|
|
170
|
+
enabled: true,
|
|
171
|
+
customKeys: null,
|
|
172
|
+
models: ["gpt-4o"],
|
|
173
|
+
embeddingsModels: null,
|
|
174
|
+
deploymentMapping: null,
|
|
175
|
+
extraHeaders: [],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
result = await handleSetModelProvider({
|
|
179
|
+
provider: "openai",
|
|
180
|
+
enabled: true,
|
|
181
|
+
defaultModel: "openai/gpt-4o",
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("keeps the prefix as-is", () => {
|
|
186
|
+
expect(result).toContain("**Default Model**: openai/gpt-4o");
|
|
187
|
+
expect(result).not.toContain("openai/openai/");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createServer, type Server } from "http";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { initConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
// --- Canned responses for scenario API endpoints ---
|
|
6
|
+
|
|
7
|
+
const CANNED_SCENARIOS_LIST = [
|
|
8
|
+
{
|
|
9
|
+
id: "scen_abc123",
|
|
10
|
+
name: "Login Flow Happy Path",
|
|
11
|
+
situation: "User attempts to log in with valid credentials",
|
|
12
|
+
criteria: ["Responds with a welcome message", "Includes user name in greeting"],
|
|
13
|
+
labels: ["auth", "happy-path"],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "scen_def456",
|
|
17
|
+
name: "Password Reset",
|
|
18
|
+
situation: "User requests a password reset link",
|
|
19
|
+
criteria: ["Sends reset email"],
|
|
20
|
+
labels: ["auth"],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const CANNED_SCENARIO_DETAIL = {
|
|
25
|
+
id: "scen_abc123",
|
|
26
|
+
name: "Login Flow Happy Path",
|
|
27
|
+
situation: "User attempts to log in with valid credentials",
|
|
28
|
+
criteria: ["Responds with a welcome message", "Includes user name in greeting"],
|
|
29
|
+
labels: ["auth", "happy-path"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const CANNED_SCENARIO_CREATED = {
|
|
33
|
+
id: "scen_new789",
|
|
34
|
+
name: "Login Flow Happy Path",
|
|
35
|
+
situation: "User attempts to log in with valid creds",
|
|
36
|
+
criteria: ["Responds with a welcome message", "Includes user name in greeting"],
|
|
37
|
+
labels: ["auth", "happy-path"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const CANNED_SCENARIO_UPDATED = {
|
|
41
|
+
id: "scen_abc123",
|
|
42
|
+
name: "Login Flow - Valid Credentials",
|
|
43
|
+
situation: "User logs in with correct email and pass",
|
|
44
|
+
criteria: [
|
|
45
|
+
"Responds with welcome message",
|
|
46
|
+
"Sets session cookie",
|
|
47
|
+
"Redirects to dashboard",
|
|
48
|
+
],
|
|
49
|
+
labels: ["auth", "happy-path"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const CANNED_SCENARIO_ARCHIVED = {
|
|
53
|
+
id: "scen_abc123",
|
|
54
|
+
archived: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// --- Mock HTTP Server ---
|
|
58
|
+
|
|
59
|
+
function createMockServer(): Server {
|
|
60
|
+
return createServer((req, res) => {
|
|
61
|
+
const authToken = req.headers["x-auth-token"];
|
|
62
|
+
if (authToken !== "test-integration-key") {
|
|
63
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
64
|
+
res.end(JSON.stringify({ message: "Invalid auth token." }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let body = "";
|
|
69
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
70
|
+
req.on("end", () => {
|
|
71
|
+
const url = req.url ?? "";
|
|
72
|
+
res.setHeader("Content-Type", "application/json");
|
|
73
|
+
|
|
74
|
+
// GET /api/scenarios - list all scenarios
|
|
75
|
+
if (url === "/api/scenarios" && req.method === "GET") {
|
|
76
|
+
res.writeHead(200);
|
|
77
|
+
res.end(JSON.stringify(CANNED_SCENARIOS_LIST));
|
|
78
|
+
}
|
|
79
|
+
// GET /api/scenarios/:id - get scenario detail
|
|
80
|
+
else if (
|
|
81
|
+
url.match(/^\/api\/scenarios\/scen_abc123(\?|$)/) &&
|
|
82
|
+
req.method === "GET"
|
|
83
|
+
) {
|
|
84
|
+
res.writeHead(200);
|
|
85
|
+
res.end(JSON.stringify(CANNED_SCENARIO_DETAIL));
|
|
86
|
+
}
|
|
87
|
+
// GET /api/scenarios/:id - not found
|
|
88
|
+
else if (
|
|
89
|
+
url.match(/^\/api\/scenarios\/scen_nonexistent(\?|$)/) &&
|
|
90
|
+
req.method === "GET"
|
|
91
|
+
) {
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end(JSON.stringify({ message: "Scenario not found" }));
|
|
94
|
+
}
|
|
95
|
+
// POST /api/scenarios - create scenario
|
|
96
|
+
else if (url === "/api/scenarios" && req.method === "POST") {
|
|
97
|
+
const parsed = JSON.parse(body);
|
|
98
|
+
if (!parsed.name) {
|
|
99
|
+
res.writeHead(400);
|
|
100
|
+
res.end(JSON.stringify({ message: "Validation error: name is required" }));
|
|
101
|
+
} else {
|
|
102
|
+
res.writeHead(201);
|
|
103
|
+
res.end(JSON.stringify(CANNED_SCENARIO_CREATED));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// PUT /api/scenarios/:id - update scenario
|
|
107
|
+
else if (
|
|
108
|
+
url.match(/^\/api\/scenarios\/scen_abc123$/) &&
|
|
109
|
+
req.method === "PUT"
|
|
110
|
+
) {
|
|
111
|
+
res.writeHead(200);
|
|
112
|
+
res.end(JSON.stringify(CANNED_SCENARIO_UPDATED));
|
|
113
|
+
}
|
|
114
|
+
// PUT /api/scenarios/:id - not found
|
|
115
|
+
else if (
|
|
116
|
+
url.match(/^\/api\/scenarios\/scen_nonexistent$/) &&
|
|
117
|
+
req.method === "PUT"
|
|
118
|
+
) {
|
|
119
|
+
res.writeHead(404);
|
|
120
|
+
res.end(JSON.stringify({ message: "Scenario not found" }));
|
|
121
|
+
}
|
|
122
|
+
// DELETE /api/scenarios/:id - archive scenario
|
|
123
|
+
else if (
|
|
124
|
+
url.match(/^\/api\/scenarios\/scen_abc123$/) &&
|
|
125
|
+
req.method === "DELETE"
|
|
126
|
+
) {
|
|
127
|
+
res.writeHead(200);
|
|
128
|
+
res.end(JSON.stringify(CANNED_SCENARIO_ARCHIVED));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
res.writeHead(404);
|
|
132
|
+
res.end(
|
|
133
|
+
JSON.stringify({ message: `Not found: ${req.method} ${url}` })
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Integration Tests ---
|
|
141
|
+
// These verify that MCP tool handlers correctly communicate with the REST API
|
|
142
|
+
// (auth, HTTP methods, status codes, error propagation).
|
|
143
|
+
// Formatting/digest logic is tested in scenario-tools.unit.test.ts.
|
|
144
|
+
|
|
145
|
+
describe("MCP scenario tools integration", () => {
|
|
146
|
+
let server: Server;
|
|
147
|
+
let port: number;
|
|
148
|
+
|
|
149
|
+
beforeAll(async () => {
|
|
150
|
+
server = createMockServer();
|
|
151
|
+
await new Promise<void>((resolve) => {
|
|
152
|
+
server.listen(0, () => {
|
|
153
|
+
const addr = server.address();
|
|
154
|
+
port = typeof addr === "object" && addr ? addr.port : 0;
|
|
155
|
+
initConfig({
|
|
156
|
+
apiKey: "test-integration-key",
|
|
157
|
+
endpoint: `http://localhost:${port}`,
|
|
158
|
+
});
|
|
159
|
+
resolve();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterAll(async () => {
|
|
165
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("platform_list_scenarios", () => {
|
|
169
|
+
describe("when the API returns scenarios", () => {
|
|
170
|
+
it("returns a non-empty result", async () => {
|
|
171
|
+
const { handleListScenarios } = await import(
|
|
172
|
+
"../tools/list-scenarios.js"
|
|
173
|
+
);
|
|
174
|
+
const result = await handleListScenarios({});
|
|
175
|
+
expect(result.length).toBeGreaterThan(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("when format is json", () => {
|
|
180
|
+
it("returns parseable JSON matching the API response", async () => {
|
|
181
|
+
const { handleListScenarios } = await import(
|
|
182
|
+
"../tools/list-scenarios.js"
|
|
183
|
+
);
|
|
184
|
+
const result = await handleListScenarios({ format: "json" });
|
|
185
|
+
expect(JSON.parse(result)).toEqual(CANNED_SCENARIOS_LIST);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("platform_get_scenario", () => {
|
|
191
|
+
describe("when the scenario exists", () => {
|
|
192
|
+
it("returns a non-empty result", async () => {
|
|
193
|
+
const { handleGetScenario } = await import(
|
|
194
|
+
"../tools/get-scenario.js"
|
|
195
|
+
);
|
|
196
|
+
const result = await handleGetScenario({ scenarioId: "scen_abc123" });
|
|
197
|
+
expect(result.length).toBeGreaterThan(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("when the scenario does not exist", () => {
|
|
202
|
+
it("propagates the 404 error", async () => {
|
|
203
|
+
const { handleGetScenario } = await import(
|
|
204
|
+
"../tools/get-scenario.js"
|
|
205
|
+
);
|
|
206
|
+
await expect(
|
|
207
|
+
handleGetScenario({ scenarioId: "scen_nonexistent" })
|
|
208
|
+
).rejects.toThrow("404");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("platform_create_scenario", () => {
|
|
214
|
+
describe("when valid data is provided", () => {
|
|
215
|
+
it("returns confirmation with new scenario ID", async () => {
|
|
216
|
+
const { handleCreateScenario } = await import(
|
|
217
|
+
"../tools/create-scenario.js"
|
|
218
|
+
);
|
|
219
|
+
const result = await handleCreateScenario({
|
|
220
|
+
name: "Login Flow Happy Path",
|
|
221
|
+
situation: "User attempts to log in with valid creds",
|
|
222
|
+
criteria: ["Responds with a welcome message", "Includes user name in greeting"],
|
|
223
|
+
labels: ["auth", "happy-path"],
|
|
224
|
+
});
|
|
225
|
+
expect(result).toContain("scen_new789");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("when name is empty", () => {
|
|
230
|
+
it("propagates the validation error", async () => {
|
|
231
|
+
const { handleCreateScenario } = await import(
|
|
232
|
+
"../tools/create-scenario.js"
|
|
233
|
+
);
|
|
234
|
+
await expect(
|
|
235
|
+
handleCreateScenario({
|
|
236
|
+
name: "",
|
|
237
|
+
situation: "Some situation",
|
|
238
|
+
})
|
|
239
|
+
).rejects.toThrow();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("platform_update_scenario", () => {
|
|
245
|
+
describe("when the scenario exists", () => {
|
|
246
|
+
it("returns a non-empty result", async () => {
|
|
247
|
+
const { handleUpdateScenario } = await import(
|
|
248
|
+
"../tools/update-scenario.js"
|
|
249
|
+
);
|
|
250
|
+
const result = await handleUpdateScenario({
|
|
251
|
+
scenarioId: "scen_abc123",
|
|
252
|
+
name: "Login Flow - Valid Credentials",
|
|
253
|
+
});
|
|
254
|
+
expect(result.length).toBeGreaterThan(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("when the scenario does not exist", () => {
|
|
259
|
+
it("propagates the 404 error", async () => {
|
|
260
|
+
const { handleUpdateScenario } = await import(
|
|
261
|
+
"../tools/update-scenario.js"
|
|
262
|
+
);
|
|
263
|
+
await expect(
|
|
264
|
+
handleUpdateScenario({
|
|
265
|
+
scenarioId: "scen_nonexistent",
|
|
266
|
+
name: "Updated Name",
|
|
267
|
+
})
|
|
268
|
+
).rejects.toThrow("404");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("platform_archive_scenario", () => {
|
|
274
|
+
describe("when the scenario exists", () => {
|
|
275
|
+
it("returns confirmation that scenario was archived", async () => {
|
|
276
|
+
const { handleArchiveScenario } = await import(
|
|
277
|
+
"../tools/archive-scenario.js"
|
|
278
|
+
);
|
|
279
|
+
const result = await handleArchiveScenario({
|
|
280
|
+
scenarioId: "scen_abc123",
|
|
281
|
+
});
|
|
282
|
+
expect(result).toContain("archived");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../langwatch-api-scenarios.js", () => ({
|
|
4
|
+
listScenarios: vi.fn(),
|
|
5
|
+
getScenario: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
listScenarios,
|
|
10
|
+
getScenario,
|
|
11
|
+
} from "../langwatch-api-scenarios.js";
|
|
12
|
+
|
|
13
|
+
import { handleListScenarios } from "../tools/list-scenarios.js";
|
|
14
|
+
import { handleGetScenario } from "../tools/get-scenario.js";
|
|
15
|
+
import { formatScenarioSchema } from "../tools/discover-scenario-schema.js";
|
|
16
|
+
|
|
17
|
+
const mockListScenarios = vi.mocked(listScenarios);
|
|
18
|
+
const mockGetScenario = vi.mocked(getScenario);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("handleListScenarios()", () => {
|
|
25
|
+
const sampleScenarios = [
|
|
26
|
+
{
|
|
27
|
+
id: "scen_abc123",
|
|
28
|
+
name: "Login Flow Happy Path",
|
|
29
|
+
situation:
|
|
30
|
+
"User attempts to log in with valid credentials and expects a welcome message back from the system",
|
|
31
|
+
criteria: [
|
|
32
|
+
"Responds with a welcome message",
|
|
33
|
+
"Includes user name in greeting",
|
|
34
|
+
"Sets session cookie",
|
|
35
|
+
],
|
|
36
|
+
labels: ["auth", "happy-path"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "scen_def456",
|
|
40
|
+
name: "Error Handling",
|
|
41
|
+
situation: "User sends malformed input",
|
|
42
|
+
criteria: ["Returns 400 status"],
|
|
43
|
+
labels: ["error"],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
describe("when scenarios exist (digest mode)", () => {
|
|
48
|
+
let result: string;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
mockListScenarios.mockResolvedValue(sampleScenarios);
|
|
52
|
+
result = await handleListScenarios({});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("includes scenario id", () => {
|
|
56
|
+
expect(result).toContain("scen_abc123");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("includes scenario name", () => {
|
|
60
|
+
expect(result).toContain("Login Flow Happy Path");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("includes truncated situation preview", () => {
|
|
64
|
+
expect(result).toContain("User attempts to log in");
|
|
65
|
+
expect(result).not.toContain(
|
|
66
|
+
"User attempts to log in with valid credentials and expects a welcome message back from the system"
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows criteria count per scenario", () => {
|
|
71
|
+
expect(result).toContain("3 criteria");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("includes labels", () => {
|
|
75
|
+
expect(result).toContain("auth");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("includes all scenarios in the list", () => {
|
|
79
|
+
expect(result).toContain("scen_def456");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("includes the total count header", () => {
|
|
83
|
+
expect(result).toContain("# Scenarios (2 total)");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("when no scenarios exist", () => {
|
|
88
|
+
let result: string;
|
|
89
|
+
|
|
90
|
+
beforeEach(async () => {
|
|
91
|
+
mockListScenarios.mockResolvedValue([]);
|
|
92
|
+
result = await handleListScenarios({});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns a no-scenarios message", () => {
|
|
96
|
+
expect(result).toContain("No scenarios found");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes a tip to use platform_create_scenario", () => {
|
|
100
|
+
expect(result).toContain("platform_create_scenario");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("when format is json", () => {
|
|
105
|
+
it("returns valid parseable JSON matching the scenario structure", async () => {
|
|
106
|
+
mockListScenarios.mockResolvedValue(sampleScenarios);
|
|
107
|
+
const result = await handleListScenarios({ format: "json" });
|
|
108
|
+
expect(JSON.parse(result)).toEqual(sampleScenarios);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("handleGetScenario()", () => {
|
|
114
|
+
const sampleScenario = {
|
|
115
|
+
id: "scen_abc123",
|
|
116
|
+
name: "Login Flow Happy Path",
|
|
117
|
+
situation: "User attempts to log in with valid credentials",
|
|
118
|
+
criteria: [
|
|
119
|
+
"Responds with a welcome message",
|
|
120
|
+
"Includes user name in greeting",
|
|
121
|
+
],
|
|
122
|
+
labels: ["auth", "happy-path"],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
describe("when format is digest", () => {
|
|
126
|
+
let result: string;
|
|
127
|
+
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
mockGetScenario.mockResolvedValue(sampleScenario);
|
|
130
|
+
result = await handleGetScenario({ scenarioId: "scen_abc123" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("includes the scenario name in the heading", () => {
|
|
134
|
+
expect(result).toContain("# Scenario: Login Flow Happy Path");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("includes the situation", () => {
|
|
138
|
+
expect(result).toContain("User attempts to log in with valid credentials");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("includes each criteria item", () => {
|
|
142
|
+
expect(result).toContain("- Responds with a welcome message");
|
|
143
|
+
expect(result).toContain("- Includes user name in greeting");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("includes labels", () => {
|
|
147
|
+
expect(result).toContain("auth, happy-path");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("when format is json", () => {
|
|
152
|
+
it("returns valid parseable JSON matching the scenario structure", async () => {
|
|
153
|
+
mockGetScenario.mockResolvedValue(sampleScenario);
|
|
154
|
+
const result = await handleGetScenario({
|
|
155
|
+
scenarioId: "scen_abc123",
|
|
156
|
+
format: "json",
|
|
157
|
+
});
|
|
158
|
+
expect(JSON.parse(result)).toEqual(sampleScenario);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("formatScenarioSchema()", () => {
|
|
164
|
+
it("includes field descriptions with required/optional annotations", () => {
|
|
165
|
+
const result = formatScenarioSchema();
|
|
166
|
+
expect(result).toContain("**name** (required)");
|
|
167
|
+
expect(result).toContain("**situation** (required)");
|
|
168
|
+
expect(result).toContain("**criteria** (array of strings)");
|
|
169
|
+
expect(result).toContain("**labels** (array of strings)");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("includes all target types with descriptions", () => {
|
|
173
|
+
const result = formatScenarioSchema();
|
|
174
|
+
expect(result).toContain("**prompt**: Test a prompt template");
|
|
175
|
+
expect(result).toContain("**http**: Test an HTTP endpoint");
|
|
176
|
+
expect(result).toContain("**code**: Test a code function");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("includes authoring guidance for situations and criteria", () => {
|
|
180
|
+
const result = formatScenarioSchema();
|
|
181
|
+
expect(result).toContain("## Writing a Good Situation");
|
|
182
|
+
expect(result).toContain("## Writing Good Criteria");
|
|
183
|
+
expect(result).toContain("Specific and testable");
|
|
184
|
+
});
|
|
185
|
+
});
|