@skybridge/test 0.0.0 → 2.0.0-beta.68a71cb
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/chat.d.ts +40 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +113 -0
- package/dist/chat.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/session-registry.d.ts +59 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +38 -0
- package/dist/session-registry.js.map +1 -0
- package/package.json +45 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alpic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @skybridge/test
|
|
2
|
+
|
|
3
|
+
Evals for [Skybridge](https://skybridge.tech) MCP apps: they answer whether a
|
|
4
|
+
prompt reaches the right tool with the right arguments, which no unit test can
|
|
5
|
+
check.
|
|
6
|
+
|
|
7
|
+
Add the `evals` option to the Skybridge Vite plugin so `expect.chat` exists,
|
|
8
|
+
then write scenarios with vitest's own `it` and `expect`. Pass the app itself
|
|
9
|
+
to `start`: it is served in-process, dialing `app.fetchHandler` directly, and
|
|
10
|
+
the tool names and argument shapes come from the app value, so no type
|
|
11
|
+
parameter is needed.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
15
|
+
import { expect, it } from "vitest";
|
|
16
|
+
import { start } from "@skybridge/test";
|
|
17
|
+
import { app } from "../src/server.js";
|
|
18
|
+
|
|
19
|
+
it("finds products by category", async () => {
|
|
20
|
+
const chat = await start({ app, model: anthropic("claude-sonnet-4-5") });
|
|
21
|
+
await chat.send("I am looking for ski goggles");
|
|
22
|
+
|
|
23
|
+
expect.chat(chat).toHaveCalledToolWith("search-products", {
|
|
24
|
+
category: "goggles",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
There is no child process and no server to boot, so the evals run anywhere
|
|
30
|
+
vitest does. The caveat is that they bypass the Express layer, so nothing
|
|
31
|
+
verifies a bearer token; pass `authInfo` to claim an identity.
|
|
32
|
+
|
|
33
|
+
## Authenticated evals
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const chat = await start({
|
|
37
|
+
app,
|
|
38
|
+
model: anthropic("claude-sonnet-4-5"),
|
|
39
|
+
authInfo: {
|
|
40
|
+
token: "eval-token",
|
|
41
|
+
clientId: "eval-client",
|
|
42
|
+
scopes: ["checkout"],
|
|
43
|
+
extra: { sub: "user_123", email: "ada@example.com" },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`authInfo` injects a claimed identity: the app's own scheme and scope
|
|
49
|
+
enforcement runs for real, in-band challenges included, and only token
|
|
50
|
+
signature verification is skipped. Omit it to eval the anonymous path, which is
|
|
51
|
+
how you assert that a protected tool challenges instead of running.
|
|
52
|
+
|
|
53
|
+
## Choosing a model
|
|
54
|
+
|
|
55
|
+
`start` takes any AI SDK model instance, so the provider and its credentials
|
|
56
|
+
are the project's own. Install the provider package you want alongside `ai`.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
60
|
+
import { start } from "@skybridge/test";
|
|
61
|
+
|
|
62
|
+
const chat = await start({ app, model: anthropic("claude-sonnet-4-5") });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A local runtime or a company gateway works the same way, through the AI SDK's
|
|
66
|
+
own `createOpenAICompatible` or `customProvider`.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
70
|
+
|
|
71
|
+
const ollama = createOpenAICompatible({
|
|
72
|
+
name: "ollama",
|
|
73
|
+
baseURL: "http://localhost:11434/v1",
|
|
74
|
+
});
|
|
75
|
+
const chat = await start({ app, model: ollama("llama3.1") });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The plugin's `evals` option also carries the defaults every conversation starts
|
|
79
|
+
from, so a project can set them once instead of per scenario.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
skybridge({ evals: { temperature: 0, maxSteps: 6 } });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Matchers
|
|
86
|
+
|
|
87
|
+
Assertions on what the assistant said work the same way, though model prose
|
|
88
|
+
varies between runs, so keep them loose.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
expect.chat(chat).toHaveSaid("in stock");
|
|
92
|
+
expect.chat(chat).not.toHaveSaid("refund");
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See the [documentation](https://docs.skybridge.tech) for setup and the full
|
|
96
|
+
matcher list.
|
package/dist/chat.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ToolCall } from "@skybridge/vite-plugin/evals";
|
|
2
|
+
import { type LanguageModel } from "ai";
|
|
3
|
+
interface HostConfig {
|
|
4
|
+
model: LanguageModel;
|
|
5
|
+
temperature?: number;
|
|
6
|
+
systemPrompt?: string;
|
|
7
|
+
maxSteps?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A `fetch` replacement the transport dials instead of the network. Mirrors the
|
|
11
|
+
* MCP client's own `fetch` option so an in-process app can serve the session.
|
|
12
|
+
*/
|
|
13
|
+
export type TransportFetch = (url: string | URL, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
/**
|
|
15
|
+
* One conversation against one MCP session. A scenario asserts on
|
|
16
|
+
* {@link Chat.toolCalls}, the sequence of calls the model made.
|
|
17
|
+
*/
|
|
18
|
+
export declare class Chat<App = unknown> {
|
|
19
|
+
/** Type-only anchor so `expect.chat` infers `App` from the conversation. */
|
|
20
|
+
readonly $app: App;
|
|
21
|
+
/** Every call the model made, in order, as it crossed the wire. */
|
|
22
|
+
readonly toolCalls: ToolCall<App>[];
|
|
23
|
+
/** What the assistant said, one entry per turn, intermediate ones included. */
|
|
24
|
+
readonly assistantTurns: string[];
|
|
25
|
+
private readonly messages;
|
|
26
|
+
private readonly failures;
|
|
27
|
+
private readonly client;
|
|
28
|
+
private readonly transport;
|
|
29
|
+
private readonly host;
|
|
30
|
+
private tools;
|
|
31
|
+
private constructor();
|
|
32
|
+
static open<App>(host: HostConfig, fetchImpl: TransportFetch): Promise<Chat<App>>;
|
|
33
|
+
/** Takes a turn. Assert on the result with `expect.chat`. */
|
|
34
|
+
send(prompt: string): Promise<void>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
private failureFor;
|
|
37
|
+
private toolSet;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=chat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,IAAI,CAAC;AAEZ,UAAU,UAAU;IAClB,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;GAGG;AACH,qBAAa,IAAI,CAAC,GAAG,GAAG,OAAO;IAC7B,4EAA4E;IAC5E,SAAiB,IAAI,EAAE,GAAG,CAAC;IAE3B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAM;IAEzC,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAM;IAEvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,KAAK,CAAc;IAE3B,OAAO;WAUM,IAAI,CAAC,GAAG,EACnB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAkBrB,6DAA6D;IACvD,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,OAAO;CAqChB"}
|
package/dist/chat.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { dynamicTool, generateText, jsonSchema, stepCountIs, } from "ai";
|
|
3
|
+
const DEFAULT_MAX_STEPS = 8;
|
|
4
|
+
const IN_PROCESS_URL = "http://in-process.skybridge.test/mcp";
|
|
5
|
+
/**
|
|
6
|
+
* One conversation against one MCP session. A scenario asserts on
|
|
7
|
+
* {@link Chat.toolCalls}, the sequence of calls the model made.
|
|
8
|
+
*/
|
|
9
|
+
export class Chat {
|
|
10
|
+
/** Every call the model made, in order, as it crossed the wire. */
|
|
11
|
+
toolCalls = [];
|
|
12
|
+
/** What the assistant said, one entry per turn, intermediate ones included. */
|
|
13
|
+
assistantTurns = [];
|
|
14
|
+
messages = [];
|
|
15
|
+
failures = new Map();
|
|
16
|
+
client;
|
|
17
|
+
transport;
|
|
18
|
+
host;
|
|
19
|
+
tools = [];
|
|
20
|
+
constructor(client, transport, host) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.transport = transport;
|
|
23
|
+
this.host = host;
|
|
24
|
+
}
|
|
25
|
+
static async open(host, fetchImpl) {
|
|
26
|
+
const client = new Client({ name: "skybridge-eval", version: "0" });
|
|
27
|
+
const transport = new StreamableHTTPClientTransport(new URL(IN_PROCESS_URL), { fetch: fetchImpl });
|
|
28
|
+
await client.connect(transport);
|
|
29
|
+
const chat = new Chat(client, transport, host);
|
|
30
|
+
try {
|
|
31
|
+
const { tools } = await client.listTools();
|
|
32
|
+
chat.tools = tools;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
await transport.close();
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return chat;
|
|
39
|
+
}
|
|
40
|
+
/** Takes a turn. Assert on the result with `expect.chat`. */
|
|
41
|
+
async send(prompt) {
|
|
42
|
+
this.messages.push({ role: "user", content: prompt });
|
|
43
|
+
const result = await generateText({
|
|
44
|
+
model: this.host.model,
|
|
45
|
+
temperature: this.host.temperature ?? 0,
|
|
46
|
+
...(this.host.systemPrompt === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { system: this.host.systemPrompt }),
|
|
49
|
+
messages: this.messages,
|
|
50
|
+
tools: this.toolSet(),
|
|
51
|
+
stopWhen: stepCountIs(this.host.maxSteps ?? DEFAULT_MAX_STEPS),
|
|
52
|
+
});
|
|
53
|
+
for (const step of result.steps) {
|
|
54
|
+
for (const call of step.toolCalls) {
|
|
55
|
+
this.toolCalls.push({
|
|
56
|
+
name: call.toolName,
|
|
57
|
+
arguments: (call.input ?? {}),
|
|
58
|
+
...this.failureFor(call),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const step of result.steps) {
|
|
63
|
+
if (step.text !== "") {
|
|
64
|
+
this.assistantTurns.push(step.text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.messages.push(...result.response.messages);
|
|
68
|
+
}
|
|
69
|
+
async close() {
|
|
70
|
+
await this.transport.close();
|
|
71
|
+
}
|
|
72
|
+
failureFor(call) {
|
|
73
|
+
const reported = this.failures.get(call.toolCallId);
|
|
74
|
+
if (reported !== undefined) {
|
|
75
|
+
return { failed: reported };
|
|
76
|
+
}
|
|
77
|
+
if (call.invalid === true) {
|
|
78
|
+
return { failed: "the model's arguments could not be parsed" };
|
|
79
|
+
}
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
toolSet() {
|
|
83
|
+
const entries = this.tools.map((definition) => {
|
|
84
|
+
const { $schema: _ignored, ...parameters } = definition.inputSchema;
|
|
85
|
+
return [
|
|
86
|
+
definition.name,
|
|
87
|
+
dynamicTool({
|
|
88
|
+
description: definition.description ?? "",
|
|
89
|
+
inputSchema: jsonSchema(parameters),
|
|
90
|
+
execute: async (input, { toolCallId }) => {
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await this.client.callTool({
|
|
94
|
+
name: definition.name,
|
|
95
|
+
arguments: (input ?? {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.failures.set(toolCallId, `the call never reached the server: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
if (result.isError === true) {
|
|
103
|
+
this.failures.set(toolCallId, `the tool returned an error: ${JSON.stringify(result.content)}`);
|
|
104
|
+
}
|
|
105
|
+
return result.content ?? result;
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
];
|
|
109
|
+
});
|
|
110
|
+
return Object.fromEntries(entries);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=chat.js.map
|
package/dist/chat.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ChatAssertion, ChatMatchers, } from "@skybridge/vite-plugin/evals";
|
|
2
|
+
export type { Chat } from "./chat.js";
|
|
3
|
+
export type { EvalApp, EvalIdentity } from "./session-registry.js";
|
|
4
|
+
export { start } from "./session-registry.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,YAAY,GACb,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
2
|
+
import { Chat } from "./chat.js";
|
|
3
|
+
/**
|
|
4
|
+
* The minimum a Skybridge app has to expose to be served in-process: the
|
|
5
|
+
* `$types` marker the assertions infer tool names from, and the fetch handler
|
|
6
|
+
* the session is dialed through. Structural on purpose, so this package needs
|
|
7
|
+
* no runtime dependency on `skybridge`.
|
|
8
|
+
*/
|
|
9
|
+
export interface EvalApp {
|
|
10
|
+
readonly $types: {
|
|
11
|
+
readonly tools: object;
|
|
12
|
+
};
|
|
13
|
+
readonly fetchHandler: {
|
|
14
|
+
fetch: (request: Request, options?: {
|
|
15
|
+
authInfo?: EvalIdentity;
|
|
16
|
+
}) => Promise<Response>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The identity an in-process session claims, handed to the app as the
|
|
21
|
+
* request's `authInfo`. Mirrors the SDK's `AuthInfo` structurally; `extra`
|
|
22
|
+
* carries whatever claims the app's verifier would have produced.
|
|
23
|
+
*/
|
|
24
|
+
export interface EvalIdentity {
|
|
25
|
+
token: string;
|
|
26
|
+
clientId: string;
|
|
27
|
+
scopes: string[];
|
|
28
|
+
expiresAt?: number;
|
|
29
|
+
resource?: URL;
|
|
30
|
+
extra?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
interface StartOptions {
|
|
33
|
+
/** Any AI SDK model instance, built by the project. */
|
|
34
|
+
model: LanguageModel;
|
|
35
|
+
systemPrompt?: string;
|
|
36
|
+
temperature?: number;
|
|
37
|
+
maxSteps?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Opens a fresh MCP session and conversation against the app under test. The
|
|
41
|
+
* app is served in-process, and the session is closed when the current test
|
|
42
|
+
* finishes, so scenarios never leak sessions and never write teardown, and
|
|
43
|
+
* tests that run concurrently cannot close each other's sessions.
|
|
44
|
+
*
|
|
45
|
+
* The assertions are inferred from the app value itself, so `expect.chat` gets
|
|
46
|
+
* the project's tool names and argument shapes with no type parameter. The
|
|
47
|
+
* session talks straight to `app.fetchHandler`, which never closes: the
|
|
48
|
+
* handler is memoized on the app and shared by every test.
|
|
49
|
+
*
|
|
50
|
+
* `authInfo` claims an identity for the session: the app's per-tool scheme and
|
|
51
|
+
* scope enforcement runs against it for real, only token verification is
|
|
52
|
+
* skipped. Omit it to exercise the anonymous path, challenges included.
|
|
53
|
+
*/
|
|
54
|
+
export declare function start<App extends EvalApp>(options: StartOptions & {
|
|
55
|
+
app: App;
|
|
56
|
+
authInfo?: EvalIdentity;
|
|
57
|
+
}): Promise<Chat<App>>;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=session-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAExC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE;QACrB,KAAK,EAAE,CACL,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,YAAY,CAAA;SAAE,KAClC,OAAO,CAAC,QAAQ,CAAC,CAAC;KACxB,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,UAAU,YAAY;IACpB,uDAAuD;IACvD,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,KAAK,CAAC,GAAG,SAAS,OAAO,EAC7C,OAAO,EAAE,YAAY,GAAG;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CAAE,GAC5D,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAoBpB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { inject, onTestFinished } from "vitest";
|
|
2
|
+
import { Chat } from "./chat.js";
|
|
3
|
+
function sharedDefaults() {
|
|
4
|
+
try {
|
|
5
|
+
return inject("skybridgeEvals");
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Opens a fresh MCP session and conversation against the app under test. The
|
|
13
|
+
* app is served in-process, and the session is closed when the current test
|
|
14
|
+
* finishes, so scenarios never leak sessions and never write teardown, and
|
|
15
|
+
* tests that run concurrently cannot close each other's sessions.
|
|
16
|
+
*
|
|
17
|
+
* The assertions are inferred from the app value itself, so `expect.chat` gets
|
|
18
|
+
* the project's tool names and argument shapes with no type parameter. The
|
|
19
|
+
* session talks straight to `app.fetchHandler`, which never closes: the
|
|
20
|
+
* handler is memoized on the app and shared by every test.
|
|
21
|
+
*
|
|
22
|
+
* `authInfo` claims an identity for the session: the app's per-tool scheme and
|
|
23
|
+
* scope enforcement runs against it for real, only token verification is
|
|
24
|
+
* skipped. Omit it to exercise the anonymous path, challenges included.
|
|
25
|
+
*/
|
|
26
|
+
export async function start(options) {
|
|
27
|
+
const config = sharedDefaults();
|
|
28
|
+
const { app, authInfo } = options;
|
|
29
|
+
const chat = await Chat.open({
|
|
30
|
+
model: options.model,
|
|
31
|
+
temperature: options.temperature ?? config?.temperature,
|
|
32
|
+
systemPrompt: options.systemPrompt ?? config?.systemPrompt,
|
|
33
|
+
maxSteps: options.maxSteps ?? config?.maxSteps,
|
|
34
|
+
}, (url, init) => app.fetchHandler.fetch(new Request(url, init), authInfo === undefined ? undefined : { authInfo }));
|
|
35
|
+
onTestFinished(() => chat.close());
|
|
36
|
+
return chat;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=session-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAwCjC,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAA6D;IAE7D,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAChC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAElC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAC1B;QACE,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,MAAM,EAAE,WAAW;QACvD,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,MAAM,EAAE,YAAY;QAC1D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,EAAE,QAAQ;KAC/C,EACD,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CACZ,GAAG,CAAC,YAAY,CAAC,KAAK,CACpB,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,EACtB,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAClD,CACJ,CAAC;IAEF,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skybridge/test",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "2.0.0-beta.68a71cb",
|
|
4
|
+
"description": "Eval runner for Skybridge MCP apps: does a prompt reach the right tool with the right arguments",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/alpic-ai/skybridge.git"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
22
|
+
"@skybridge/vite-plugin": "2.0.0-beta.68a71cb"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"ai": "^5.0.0",
|
|
26
|
+
"skybridge": "^2.0.0",
|
|
27
|
+
"vitest": "^4.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^24.13.3",
|
|
31
|
+
"ai": "^5.0.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^4.1.10",
|
|
34
|
+
"zod": "^4.4.3",
|
|
35
|
+
"skybridge": "2.0.0-beta.68a71cb"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.23.1"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"format": "biome check --write --error-on-warnings",
|
|
43
|
+
"test": "pnpm run test:format && pnpm run test:types && pnpm run test:unit",
|
|
44
|
+
"test:format": "biome ci",
|
|
45
|
+
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
46
|
+
"test:unit": "vitest run"
|
|
6
47
|
}
|
|
7
|
-
}
|
|
48
|
+
}
|