Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions e2e/mcp.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { expect, test } from '@playwright/test';
import { resetE2EState } from './helpers';
import { createPublicClient, E2E_OWNER_EMAIL, E2E_OWNER_PASSWORD, E2E_TEAM_ID } from './local-fixtures';
import {
createPublicClient,
E2E_NODE_RECEIVE_ID,
E2E_OWNER_EMAIL,
E2E_OWNER_PASSWORD,
E2E_PROCESS_FLOW_ID,
E2E_STORY_MAP_ID,
E2E_TEAM_ID,
} from './local-fixtures';

test.beforeEach(async () => {
await resetE2EState();
Expand Down Expand Up @@ -49,9 +57,17 @@ test('serves authenticated MCP tools over the v2 HTTP transport', async ({ baseU

expect(client.getServerVersion()).toMatchObject({ name: 'beemspec' });
expect(client.getNegotiatedProtocolVersion()).toBe('2026-07-28');
expect(client.getInstructions()).toContain('processflow_nodes_mutate');

const { tools } = await client.listTools();
expect(tools).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'team_list' })]));
const toolNames = new Set(tools.map((tool) => tool.name));
expect(toolNames.has('team_list')).toBe(true);
expect(toolNames.has('storymap_workflow_guide')).toBe(false);
expect(toolNames.has('processflow_workflow_guide')).toBe(false);
expect(toolNames.has('processflow_nodes_mutate')).toBe(true);
expect(toolNames.has('processflow_edges_mutate')).toBe(true);
expect(toolNames.has('processflow_autolayout')).toBe(true);
expect(tools.every((tool) => tool.outputSchema)).toBe(true);

const teamList = await client.callTool({ name: 'team_list', arguments: {} });
expect(teamList.isError).not.toBe(true);
Expand All @@ -65,6 +81,64 @@ test('serves authenticated MCP tools over the v2 HTTP transport', async ({ baseU
},
]),
});

const storyMap = await client.callTool({
name: 'storymap_get',
arguments: { story_map_id: E2E_STORY_MAP_ID },
});
expect(storyMap.isError).not.toBe(true);
expect(storyMap.structuredContent).toMatchObject({
ok: true,
data: { id: E2E_STORY_MAP_ID },
});

const processFlow = await client.callTool({
name: 'processflow_get',
arguments: { process_flow_id: E2E_PROCESS_FLOW_ID },
});
expect(processFlow.isError).not.toBe(true);
expect(processFlow.structuredContent).toMatchObject({
ok: true,
data: { id: E2E_PROCESS_FLOW_ID },
});

const batchResult = await client.callTool({
name: 'processflow_nodes_mutate',
arguments: {
process_flow_id: E2E_PROCESS_FLOW_ID,
mutations: [
{
action: 'update',
id: E2E_NODE_RECEIVE_ID,
payload: { data: { label: 'Receive and validate invoice' } },
},
],
},
});
expect(batchResult.isError).not.toBe(true);
expect(batchResult.structuredContent).toMatchObject({
ok: true,
data: {
updated: [
{
id: E2E_NODE_RECEIVE_ID,
data: { label: 'Receive and validate invoice' },
},
],
},
});

const layoutResult = await client.callTool({
name: 'processflow_autolayout',
arguments: { process_flow_id: E2E_PROCESS_FLOW_ID },
});
expect(layoutResult.isError).not.toBe(true);
expect(layoutResult.structuredContent).toMatchObject({
ok: true,
data: {
nodes: expect.arrayContaining([expect.objectContaining({ id: E2E_NODE_RECEIVE_ID })]),
},
});
} finally {
await client.close();
const { error: signOutError } = await supabase.auth.signOut();
Expand Down
45 changes: 45 additions & 0 deletions src/domain/process-flow/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import {
batchMutateProcessFlowEdgesSchema,
batchMutateProcessFlowNodesSchema,
processFlowNodeDataSchema,
updateProcessFlowEdgeToolSchema,
updateProcessFlowNodeToolSchema,
updateProcessFlowToolSchema,
} from './schemas';

const id = (value: number) => `00000000-0000-4000-8000-${value.toString().padStart(12, '0')}`;

describe('process flow model-facing schemas', () => {
it.each([
['process flow', updateProcessFlowToolSchema, { process_flow_id: id(1) }],
['node', updateProcessFlowNodeToolSchema, { process_flow_id: id(1), node_id: id(2) }],
['edge', updateProcessFlowEdgeToolSchema, { process_flow_id: id(1), edge_id: id(2) }],
])('requires a real change when updating a %s', (_entity, schema, input) => {
expect(schema.safeParse(input).success).toBe(false);
expect(schema.description).toContain('at least one change is required');
});

it('accepts documented operational node data', () => {
expect(
processFlowNodeDataSchema.safeParse({
label: 'Approve request',
owner_role: 'Finance',
systems: ['ERP'],
inputs: ['Purchase request'],
outputs: ['Approval'],
frequency: 'Daily',
estimated_duration: '15 minutes',
}).success,
).toBe(true);
});

it.each([
['node', batchMutateProcessFlowNodesSchema],
['edge', batchMutateProcessFlowEdgesSchema],
])('limits %s mutation batches', (_entity, schema) => {
const mutations = Array.from({ length: 101 }, (_, index) => ({ action: 'delete' as const, id: id(index + 1) }));

expect(schema.safeParse({ process_flow_id: id(200), mutations }).success).toBe(false);
});
});
Loading