Skip to content

Commit c5d24b5

Browse files
committed
test(examples): execute the real example files
`tests/examples/` reimplemented each example's logic against `../../src` and never imported `examples/*.ts`. An example could rot or break outright and CI stayed green — and four of the eight had no test at all. Examples do their work at import time via top-level await, so importing one runs it. `tests/examples/run-example.ts` makes that safe under vitest: - `STACKONE_BASE_URL` points the SDK at the MSW mock (toolsets.ts:630 reads it), so an example constructing `new StackOneToolSet()` with no explicit baseUrl still hits the mock rather than production. No example needed changing. - `process.exit` becomes a throw that `runExample` catches and reports as `exitCode`, so an example bailing on missing config is an assertable outcome instead of a torn-down vitest worker. Seven of the eight examples now execute for real. The eighth, claude-agent-sdk-integration, still cannot: `query()` spawns a claude-code subprocess that MSW cannot intercept. Its existing setup-level test is kept and the exclusion is now stated in the file rather than implied. Mock gaps that blocked execution, all of which made the examples exercise nothing: - The three `workday_*` actions every example filters on were absent from the MCP catalog, so the filters matched zero tools. - No Anthropic handler existed at all (mocks/handlers.anthropic.ts). - The OpenAI handlers keyed off prompts none of the examples send; the examples all prompt "List the first 5 employees". - Semantic search (`/actions/search`) was unmocked, so search-tools hit the real network. Added scoped to that test via `server.use` — a global handler pre-empts src/semantic-search.test.ts, which asserts against an unmocked endpoint. Tool-count assertions are exact rather than `> 0`. A `> 0` check cannot fail: `fetchTools` appends tool_feedback *after* filtering, so a filter matching nothing still returns one tool. Verified by mutation — pointing an example at a nonexistent action now fails with `expected 1 to be 4`, where before it passed. defender-config keeps all seven original wire-payload tests, which assert the exact RPC body per defender mode; the four execution tests are added alongside. fetch-tools and tanstack-ai-integration describe examples that do not exist. Relabelled in place rather than moved — the `examples` vitest project globs only `tests/examples/**` and `root` globs only `src/`+`scripts/`, so relocating them would have dropped them from both projects silently. 348 passing, up from 329. The one failure is pre-existing and environmental (`spawn tsc ENOENT`); it fails identically on origin/main.
1 parent 0466ab1 commit c5d24b5

15 files changed

Lines changed: 524 additions & 112 deletions

‎mocks/handlers.anthropic.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { http, HttpResponse } from 'msw';
2+
3+
/**
4+
* Anthropic Messages API, for anthropic-integration.ts.
5+
*/
6+
export const anthropicHandlers = [
7+
http.post('https://api.anthropic.com/v1/messages', async ({ request }) => {
8+
const body = (await request.json()) as {
9+
messages?: Array<{ content?: unknown; role?: string }>;
10+
tools?: Array<{ name?: string }>;
11+
};
12+
13+
const userMessage = body.messages
14+
?.filter((message) => message.role === 'user')
15+
.map((message) =>
16+
typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
17+
)
18+
.join(' ');
19+
const hasTools = Boolean(body.tools?.length);
20+
21+
if (hasTools && userMessage?.includes('List the first 5 employees')) {
22+
return HttpResponse.json({
23+
id: 'msg_mock_list',
24+
type: 'message',
25+
role: 'assistant',
26+
model: 'claude-haiku-4-5-20241022',
27+
content: [
28+
{
29+
type: 'tool_use',
30+
id: 'toolu_mock_list',
31+
name: 'workday_list_workers',
32+
input: { query: { limit: 5 } },
33+
},
34+
],
35+
stop_reason: 'tool_use',
36+
usage: { input_tokens: 100, output_tokens: 50 },
37+
});
38+
}
39+
40+
return HttpResponse.json({
41+
id: 'msg_default',
42+
type: 'message',
43+
role: 'assistant',
44+
model: 'claude-haiku-4-5-20241022',
45+
content: [{ type: 'text', text: 'Mock response' }],
46+
stop_reason: 'end_turn',
47+
usage: { input_tokens: 10, output_tokens: 10 },
48+
});
49+
}),
50+
];

‎mocks/handlers.openai.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ export const openaiHandlers = [
6565
});
6666
}
6767

68+
// For openai-responses-integration.ts and search-tools.ts, which prompt with
69+
// the same sentence and expect a tool call back.
70+
if (hasTools && userMessage.includes('List the first 5 employees')) {
71+
return HttpResponse.json({
72+
id: 'resp_mock_list',
73+
object: 'response',
74+
created_at: Date.now(),
75+
model: 'gpt-5',
76+
status: 'completed',
77+
output: [
78+
{
79+
type: 'function_call',
80+
id: 'call_mock_list',
81+
call_id: 'call_mock_list',
82+
name: 'workday_list_workers',
83+
arguments: JSON.stringify({ query: { limit: 5 } }),
84+
status: 'completed',
85+
},
86+
],
87+
usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 },
88+
});
89+
}
90+
6891
// For human-in-the-loop.ts
6992
if (hasTools && userMessage.includes('Create a new employee')) {
7093
return HttpResponse.json({
@@ -158,6 +181,38 @@ export const openaiHandlers = [
158181
});
159182
}
160183

184+
// For openai-integration.ts, which filters to workday actions and prompts
185+
// with this sentence.
186+
if (hasTools && userMessage.includes('List the first 5 employees')) {
187+
return HttpResponse.json({
188+
id: 'chatcmpl-mock-list',
189+
object: 'chat.completion',
190+
created: Date.now(),
191+
model: 'gpt-5',
192+
choices: [
193+
{
194+
index: 0,
195+
message: {
196+
role: 'assistant',
197+
content: null,
198+
tool_calls: [
199+
{
200+
id: 'call_mock_list',
201+
type: 'function',
202+
function: {
203+
name: 'workday_list_workers',
204+
arguments: JSON.stringify({ query: { limit: 5 } }),
205+
},
206+
},
207+
],
208+
},
209+
finish_reason: 'tool_calls',
210+
},
211+
],
212+
usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
213+
});
214+
}
215+
161216
// For human-in-the-loop.ts - returns tool call for bamboohr_create_employee
162217
if (hasTools && userMessage.includes('Create a new employee')) {
163218
return HttpResponse.json({

‎mocks/handlers.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { anthropicHandlers } from './handlers.anthropic';
12
import { exampleApiHandlers } from './handlers.example-api';
23
import { mcpHandlers } from './handlers.mcp';
34
import { openaiHandlers } from './handlers.openai';
45
import { stackoneAiHandlers } from './handlers.stackone-ai';
56
import { stackoneRpcHandlers } from './handlers.stackone-rpc';
67

78
export const handlers = [
9+
...anthropicHandlers,
810
...openaiHandlers,
911
...stackoneRpcHandlers,
1012
...stackoneAiHandlers,

‎mocks/mcp-server.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,42 @@ export const exampleBamboohrTools = [
227227
required: ['name'],
228228
},
229229
},
230+
// The examples filter on these three action names. Without them the examples
231+
// execute against an empty toolset and assert nothing.
232+
{
233+
name: 'workday_list_workers',
234+
description: 'List workers from Workday',
235+
inputSchema: {
236+
type: 'object',
237+
properties: {
238+
query: {
239+
type: 'object',
240+
properties: {
241+
limit: { type: 'number', description: 'Limit the number of results' },
242+
},
243+
},
244+
},
245+
},
246+
},
247+
{
248+
name: 'workday_get_worker',
249+
description: 'Get a single worker by ID from Workday',
250+
inputSchema: {
251+
type: 'object',
252+
properties: {
253+
id: { type: 'string', description: 'The worker ID' },
254+
},
255+
required: ['id'],
256+
},
257+
},
258+
{
259+
name: 'workday_get_current_user',
260+
description: 'Get the currently authenticated Workday user',
261+
inputSchema: {
262+
type: 'object',
263+
properties: {},
264+
},
265+
},
230266
] as const satisfies McpToolDefinition[];
231267

232268
export const mixedProviderTools = [
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Executes examples/ai-sdk-integration.ts.
3+
*/
4+
5+
import { runExample, stubExampleEnv } from './run-example';
6+
7+
describe('ai-sdk-integration example', () => {
8+
beforeEach(() => {
9+
stubExampleEnv({ OPENAI_API_KEY: 'test-openai-key' });
10+
});
11+
12+
afterEach(() => {
13+
vi.unstubAllEnvs();
14+
});
15+
16+
it('loads tools and drives a Vercel AI SDK generation', async () => {
17+
const { stdout, exitCode } = await runExample('../../examples/ai-sdk-integration.ts');
18+
19+
expect(exitCode).toBeUndefined();
20+
21+
// 4: 3 workday actions the example filters to, plus the auto-appended tool_feedback.
22+
// A bare > 0 check cannot fail here — tool_feedback is appended after
23+
// filtering, so even a filter matching nothing still yields one tool.
24+
const loaded = stdout.match(/Loaded (\d+) tools/);
25+
expect(loaded).not.toBeNull();
26+
expect(Number(loaded?.[1])).toBe(4);
27+
});
28+
29+
it('skips cleanly when OPENAI_API_KEY is absent', async () => {
30+
vi.stubEnv('OPENAI_API_KEY', '');
31+
32+
const { stdout, exitCode } = await runExample('../../examples/ai-sdk-integration.ts');
33+
34+
expect(exitCode).toBe(0);
35+
});
36+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Executes examples/anthropic-integration.ts.
3+
*/
4+
5+
import { runExample, stubExampleEnv } from './run-example';
6+
7+
describe('anthropic-integration example', () => {
8+
beforeEach(() => {
9+
stubExampleEnv({ ANTHROPIC_API_KEY: 'test-anthropic-key' });
10+
});
11+
12+
afterEach(() => {
13+
vi.unstubAllEnvs();
14+
});
15+
16+
it('loads tools, calls Anthropic, and reports the tool use', async () => {
17+
const { stdout, exitCode } = await runExample('../../examples/anthropic-integration.ts');
18+
19+
expect(exitCode).toBeUndefined();
20+
21+
// 3: 2 tools matching the example's filters, plus the auto-appended tool_feedback.
22+
// A bare > 0 check cannot fail here — tool_feedback is appended after
23+
// filtering, so even a filter matching nothing still yields one tool.
24+
const loaded = stdout.match(/Loaded (\d+) tools/);
25+
expect(loaded).not.toBeNull();
26+
expect(Number(loaded?.[1])).toBe(3);
27+
28+
expect(stdout).toContain('workday_list_workers');
29+
});
30+
31+
it('skips cleanly when ANTHROPIC_API_KEY is absent', async () => {
32+
vi.stubEnv('ANTHROPIC_API_KEY', '');
33+
34+
const { stdout, exitCode } = await runExample('../../examples/anthropic-integration.ts');
35+
36+
expect(exitCode).toBe(0);
37+
});
38+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Executes examples/auth-management.ts.
3+
*/
4+
5+
import { runExample, stubExampleEnv } from './run-example';
6+
7+
describe('auth-management example', () => {
8+
beforeEach(() => {
9+
stubExampleEnv();
10+
});
11+
12+
afterEach(() => {
13+
vi.unstubAllEnvs();
14+
});
15+
16+
it('runs every auth pattern to completion', async () => {
17+
const { stdout, exitCode } = await runExample('../../examples/auth-management.ts');
18+
19+
expect(exitCode).toBeUndefined();
20+
expect(stdout).toContain('Done — all auth patterns demonstrated.');
21+
});
22+
23+
it('loads tools through each configuration path', async () => {
24+
const { stdout } = await runExample('../../examples/auth-management.ts');
25+
26+
for (const section of [
27+
'1a. API Key from environment',
28+
'1b. Explicit API key',
29+
'2. Account ID from environment',
30+
'3. Account ID in constructor',
31+
'4. setAccounts() — global account list',
32+
'5. Per-tool account override',
33+
]) {
34+
expect(stdout).toContain(section);
35+
}
36+
37+
// Every fetchTools() call must return a non-empty toolset; "Loaded 0 tools"
38+
// would mean the example ran but silently exercised nothing.
39+
const loaded = [...stdout.matchAll(/Loaded (\d+) tools/g)].map((match) => Number(match[1]));
40+
expect(loaded.length).toBeGreaterThan(0);
41+
expect(loaded.every((count) => count > 0)).toBe(true);
42+
});
43+
44+
it('exits non-zero when STACKONE_API_KEY is absent', async () => {
45+
vi.stubEnv('STACKONE_API_KEY', '');
46+
47+
const { exitCode } = await runExample('../../examples/auth-management.ts');
48+
49+
expect(exitCode).toBe(1);
50+
});
51+
});

‎tests/examples/claude-agent-sdk-integration.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
*
44
* Tests the setup of StackOne tools with Claude Agent SDK.
55
*
6-
* Note: The Claude Agent SDK spawns a subprocess to run claude-code, which
7-
* requires the ANTHROPIC_API_KEY environment variable and a running claude-code
8-
* installation. This test validates the tool setup and MCP server creation,
9-
* but does not test the actual query execution.
6+
* This is the one example NOT executed directly (see tests/examples/run-example.ts
7+
* for how the others are). `query()` spawns a claude-code subprocess, which MSW
8+
* cannot intercept and which needs a real ANTHROPIC_API_KEY and installation.
9+
* So this reimplements the example's setup — tool fetch, tool wrapper, MCP server
10+
* creation and handler execution — and stops short of the query call.
1011
*/
1112

1213
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';

‎tests/examples/defender-config.test.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
*/
99

1010
import { TEST_BASE_URL } from '../../mocks/constants';
11+
import { runExample, stubExampleEnv } from './run-example';
1112
import { DEFAULT_DEFENDER_CONFIG, StackOneToolSet, ToolSetConfigError } from '../../src';
1213

13-
describe('defender-config example e2e', () => {
14+
describe('defender-config wire payloads', () => {
1415
beforeEach(() => {
1516
vi.stubEnv('STACKONE_API_KEY', 'test-key');
1617
// Silence override warnings so they don't pollute test output.
@@ -153,3 +154,45 @@ describe('defender-config example e2e', () => {
153154
});
154155
});
155156
});
157+
158+
describe('defender-config example', () => {
159+
beforeEach(() => {
160+
// The example defaults section 8 to gmail_list_messages, which the MCP mock
161+
// does not serve; TOOL_NAME is the example's own knob for pointing it elsewhere.
162+
stubExampleEnv({ TOOL_NAME: 'workday_list_workers' });
163+
});
164+
165+
afterEach(() => {
166+
vi.unstubAllEnvs();
167+
});
168+
169+
it('demonstrates every defender pattern to completion', async () => {
170+
const { stdout, exitCode } = await runExample('../../examples/defender-config.ts');
171+
172+
expect(exitCode).toBeUndefined();
173+
expect(stdout).toContain('Done — defender patterns demonstrated.');
174+
});
175+
176+
it('shows both the omitted and the all-false payload shapes', async () => {
177+
const { stdout } = await runExample('../../examples/defender-config.ts');
178+
179+
expect(stdout).toContain('SDK adds no defender_config to the RPC payload.');
180+
expect(stdout).toContain('SDK sends defender_config with all fields false.');
181+
});
182+
183+
it('reaches the live section and gets defender metadata back', async () => {
184+
const { stdout } = await runExample('../../examples/defender-config.ts');
185+
186+
// With STACKONE_API_KEY stubbed the example runs section 8 rather than
187+
// printing its skip notice.
188+
expect(stdout).not.toContain('Skipping — set STACKONE_API_KEY to run this section.');
189+
expect(stdout).toContain('defenderMetadata:');
190+
});
191+
192+
it('still rejects an invalid defender combination', async () => {
193+
const { stdout } = await runExample('../../examples/defender-config.ts');
194+
195+
// The example prints this only if the invalid combo failed to throw.
196+
expect(stdout).not.toContain('(no throw — unexpected!)');
197+
});
198+
});

‎tests/examples/fetch-tools.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
/**
2-
* E2E test for fetch-tools.ts example
2+
* E2E coverage for fetching and filtering tools via MCP.
33
*
4-
* Tests the complete flow of fetching and filtering tools via MCP.
4+
* There is no `examples/fetch-tools.ts`; this exercises the discovery flow the
5+
* other examples build on, against the MCP mock.
56
*/
67

78
import { http, HttpResponse } from 'msw';
89
import { server } from '../../mocks/node';
910
import { TEST_BASE_URL } from '../../mocks/constants';
1011
import { StackOneToolSet } from '../../src';
1112

12-
describe('fetch-tools example e2e', () => {
13+
describe('fetch-tools e2e', () => {
1314
beforeEach(() => {
1415
vi.stubEnv('STACKONE_API_KEY', 'test-key');
1516
});

0 commit comments

Comments
 (0)