Files
ccs/tests/unit/hooks/browser-mcp-orchestration-and-artifacts.test.ts
T

1766 lines
53 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { runMcpRequests, getResponseText, createReplayStep, createOrchestrationBlock, mkdtempSync, writeFileSync, tmpdir, join } from './browser-mcp-test-harness';
import type { MockPageState } from './browser-mcp-test-harness';
describe('ccs-browser MCP server - orchestration and artifacts', () => {
it('starts orchestration, reports progress, and completes a wait_then_click flow', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Orchestration Page',
currentUrl: 'https://example.com/orchestration',
click: {
'#menu': {},
},
wait: {
selectorSnapshots: {
'#menu': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
query: {
'#menu': {
exists: true,
connected: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1201,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'wait_for_then_click',
args: {
wait: {
selector: '#menu',
condition: { kind: 'visibility' },
timeoutMs: 1000,
},
click: {
selector: '#menu',
},
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1202,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
expect(getResponseText(responses.find((message) => message.id === 1201))).toContain('status: completed');
expect(getResponseText(responses.find((message) => message.id === 1202))).toContain('completedBlocks: 1');
expect(getResponseText(responses.find((message) => message.id === 1202))).toContain('status: completed');
});
it('cancels orchestration before later blocks run', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Orchestration Cancel Page',
currentUrl: 'https://example.com/orchestration-cancel',
query: {
'#handle': {
exists: true,
connected: true,
rect: {
x: 20,
y: 30,
width: 40,
height: 40,
top: 30,
right: 60,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
'#status': {
exists: true,
connected: true,
innerText: 'ready',
textContent: 'ready',
rect: {
x: 80,
y: 30,
width: 80,
height: 20,
top: 30,
right: 160,
bottom: 50,
left: 80,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1202_1,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'run_replay_sequence',
args: {
steps: [
createReplayStep({
type: 'pointer_action',
pageId: 'page-1',
args: {
actions: [
{ type: 'move', selector: '#handle' },
{ type: 'pause', durationMs: 400 },
],
},
}),
],
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'equals', value: 'ready' }],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1202_2,
method: 'tools/call',
params: { name: 'browser_cancel_orchestration', arguments: {} },
},
{
jsonrpc: '2.0',
id: 1202_3,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
expect(getResponseText(responses.find((message) => message.id === 1202_1))).toContain(
'status: running'
);
const text = getResponseText(responses.find((message) => message.id === 1202_3));
expect(text).toContain('status: canceled');
expect(text).not.toContain('completedBlocks: 2');
});
it('completes a wait_then_type flow', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Type Orchestration Page',
currentUrl: 'https://example.com/type-orchestration',
wait: {
selectorSnapshots: {
'#email': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
type: {
'#email': { kind: 'input', inputType: 'email', value: '' },
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1203,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'wait_for_then_type',
args: {
wait: { selector: '#email', condition: { kind: 'visibility' }, timeoutMs: 1000 },
type: { selector: '#email', text: 'walker@example.com', clearFirst: true },
},
}),
],
},
},
},
]);
expect(getResponseText(responses.find((message) => message.id === 1203))).toContain('status: completed');
expect(getResponseText(responses.find((message) => message.id === 1203))).toContain('completedBlocks: 1');
});
it('completes a run_replay_sequence block', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Replay Sequence Orchestration Page',
currentUrl: 'https://example.com/orchestration-replay',
click: { '#submit': {} },
query: {
'#submit': {
exists: true,
connected: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1211,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'run_replay_sequence',
args: {
steps: [createReplayStep({ type: 'click', pageId: 'page-1', selector: '#submit', args: {} })],
},
}),
],
},
},
},
]);
expect(getResponseText(responses.find((message) => message.id === 1211))).toContain('status: completed');
});
it('completes an assert_query block', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Assert Query Page',
currentUrl: 'https://example.com/assert-query',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready',
textContent: 'ready',
display: 'block',
visibility: 'visible',
opacity: '1',
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1212,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assert: { field: 'innerText', equals: 'ready' },
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1212))).toContain('status: completed');
});
it('fails orchestration on the first failing block', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Failure Page', currentUrl: 'https://example.com/orchestration-failure' }],
[
{
jsonrpc: '2.0',
id: 1213,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#missing', fields: ['exists'] },
assert: { field: 'exists', equals: 'true' },
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1214,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]
);
const text = getResponseText(responses.find((message) => message.id === 1214));
expect(text).toContain('status: failed');
expect(text).toContain('failedBlockIndex: 0');
});
it('runs a single-layer sequence with two successful blocks', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Sequence Page',
currentUrl: 'https://example.com/sequence',
click: {
'#menu': {},
},
wait: {
selectorSnapshots: {
'#menu': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
query: {
'#menu': {
exists: true,
connected: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1401,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'sequence',
args: {
steps: [
createOrchestrationBlock({
type: 'wait_for_then_click',
args: {
wait: { selector: '#menu', condition: { kind: 'visibility' }, timeoutMs: 1000 },
click: { selector: '#menu' },
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
}),
],
},
},
},
]);
expect(getResponseText(responses.find((message) => message.id === 1401))).toContain('status: completed');
expect(getResponseText(responses.find((message) => message.id === 1401))).toContain('completedBlocks: 1');
});
it('reports failedSequenceStepIndex when a sequence step fails', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Sequence Failure Page',
currentUrl: 'https://example.com/sequence-failure',
click: {
'#menu': {},
},
wait: {
selectorSnapshots: {
'#menu': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1402,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'sequence',
args: {
steps: [
createOrchestrationBlock({
type: 'wait_for_then_click',
args: {
wait: { selector: '#menu', condition: { kind: 'visibility' }, timeoutMs: 1000 },
click: { selector: '#menu' },
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#missing', fields: ['exists'] },
assertions: [{ field: 'exists', op: 'equals', value: true }],
},
}),
],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1403,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
const text = getResponseText(responses.find((message) => message.id === 1403));
expect(text).toContain('status: failed');
expect(text).toContain('failedBlockIndex: 0');
expect(text).toContain('failedSequenceStepIndex: 1');
});
it('rejects empty sequence steps and nested sequence blocks', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Sequence Validation Page', currentUrl: 'https://example.com/sequence-validation' }],
[
{
jsonrpc: '2.0',
id: 1404,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [createOrchestrationBlock({ type: 'sequence', args: { steps: [] } })],
},
},
},
{
jsonrpc: '2.0',
id: 1405,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'sequence',
args: {
steps: [createOrchestrationBlock({ type: 'sequence', args: { steps: [] } })],
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1404))).toContain('sequence steps must be a non-empty array');
expect(getResponseText(responses.find((message) => message.id === 1405))).toContain('sequence does not support nested sequence blocks');
});
it('runs a top-level block followed by a sequence in order', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Mixed Sequence Page',
currentUrl: 'https://example.com/mixed-sequence',
click: {
'#menu': {},
},
wait: {
selectorSnapshots: {
'#menu': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1406,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
createOrchestrationBlock({
type: 'sequence',
args: {
steps: [
createOrchestrationBlock({
type: 'wait_for_then_click',
args: {
wait: { selector: '#menu', condition: { kind: 'visibility' }, timeoutMs: 1000 },
click: { selector: '#menu' },
},
}),
],
},
}),
],
},
},
},
]);
expect(getResponseText(responses.find((message) => message.id === 1406))).toContain('status: completed');
expect(getResponseText(responses.find((message) => message.id === 1406))).toContain('completedBlocks: 2');
});
it('passes assert_query with multiple structured assertions', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Assert Query Page',
currentUrl: 'https://example.com/assert-query',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '0.95',
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1301,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['exists', 'innerText', 'opacity'] },
assertions: [
{ field: 'exists', op: 'equals', value: true },
{ field: 'innerText', op: 'contains', value: 'ready' },
{ field: 'opacity', op: 'gte', value: 0.9 },
],
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1301))).toContain('status: completed');
});
it('returns failedAssertionIndex, expected, and actual when assert_query fails', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Assert Failure Page',
currentUrl: 'https://example.com/assert-failure',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'loading',
textContent: 'loading',
display: 'block',
visibility: 'visible',
opacity: '0.42',
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1302,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText', 'opacity'] },
assertions: [
{ field: 'innerText', op: 'contains', value: 'ready' },
{ field: 'opacity', op: 'gte', value: 0.9 },
],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1303,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]
);
const text = getResponseText(responses.find((message) => message.id === 1303));
expect(text).toContain('status: failed');
expect(text).toContain('failedBlockIndex: 0');
expect(text).toContain('failedAssertionIndex: 0');
expect(text).toContain('field: innerText');
expect(text).toContain('expected: "ready"');
expect(text).toContain('actual: "loading"');
});
it('fails when numeric comparison is used on a non-number-like field or when op is unsupported', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Assert Type Error Page',
currentUrl: 'https://example.com/assert-type-error',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'loading',
textContent: 'loading',
opacity: '0.42',
display: 'block',
visibility: 'visible',
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1304,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'gte', value: 1 }],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1305,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['opacity'] },
assertions: [{ field: 'opacity', op: 'matches', value: '.*' }],
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1304))).toContain('numeric comparison expects a number-like field: innerText');
expect(getResponseText(responses.find((message) => message.id === 1305))).toContain('unsupported assertion operator');
});
it('passes numeric gte and lt assertions', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Numeric Assertion Page',
currentUrl: 'https://example.com/assert-numeric',
query: {
'#status': {
exists: true,
connected: true,
opacity: '0.95',
display: 'block',
visibility: 'visible',
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1306,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['opacity'] },
assertions: [
{ field: 'opacity', op: 'gte', value: 0.9 },
{ field: 'opacity', op: 'lt', value: 1.0 },
],
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1306))).toContain('status: completed');
});
it('continues to the next top-level block when continueOnError is true', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Policy Page',
currentUrl: 'https://example.com/policy',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1501,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
continueOnError: true,
args: {
query: { selector: '#missing', fields: ['exists'] },
assertions: [{ field: 'exists', op: 'equals', value: true }],
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1502,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
const text = getResponseText(responses.find((message) => message.id === 1502));
expect(text).toContain('status: completed_with_failures');
expect(text).toContain('completedBlocks: 2');
expect(text).toContain('failedCount: 1');
expect(text).toContain('failure[0].blockIndex: 0');
});
it('continues inside sequence when a step has continueOnError', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Sequence Policy Page',
currentUrl: 'https://example.com/sequence-policy',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1503,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'sequence',
args: {
steps: [
createOrchestrationBlock({
type: 'assert_query',
continueOnError: true,
args: {
query: { selector: '#missing', fields: ['exists'] },
assertions: [{ field: 'exists', op: 'equals', value: true }],
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1504,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
const text = getResponseText(responses.find((message) => message.id === 1504));
expect(text).toContain('status: completed_with_failures');
expect(text).toContain('completedBlocks: 1');
expect(text).toContain('failedCount: 1');
expect(text).toContain('failure[0].sequenceStepIndex: 0');
});
it('still stops immediately when continueOnError is not enabled', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Strict Stop Page', currentUrl: 'https://example.com/strict-stop' }],
[
{
jsonrpc: '2.0',
id: 1505,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#missing', fields: ['exists'] },
assertions: [{ field: 'exists', op: 'equals', value: true }],
},
}),
createOrchestrationBlock({
type: 'assert_query',
continueOnError: true,
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
},
},
]
);
const text = getResponseText(responses.find((message) => message.id === 1505));
expect(text).toContain('status: failed');
expect(text).toContain('completedBlocks: 0');
});
it('continues to the next top-level block when a sequence block itself allows continueOnError', async () => {
const pages: MockPageState[] = [
{
id: 'page-1',
title: 'Sequence Top-Level Policy Page',
currentUrl: 'https://example.com/sequence-top-policy',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
];
const responses = await runMcpRequests(pages, [
{
jsonrpc: '2.0',
id: 1506,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'sequence',
continueOnError: true,
args: {
steps: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#missing', fields: ['exists'] },
assertions: [{ field: 'exists', op: 'equals', value: true }],
},
}),
],
},
}),
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1507,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]);
const text = getResponseText(responses.find((message) => message.id === 1507));
expect(text).toContain('status: completed_with_failures');
expect(text).toContain('completedBlocks: 2');
expect(text).toContain('failedCount: 1');
expect(text).toContain('failure[0].sequenceStepIndex: 0');
});
it('exports recording and replay artifacts and lists them', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Artifact Page',
currentUrl: 'https://example.com/artifact',
recording: {
events: [
{
kind: 'click',
selector: '#submit',
timestamp: 1710000000000,
},
],
},
click: {
'#submit': {},
},
query: {
'#submit': {
exists: true,
connected: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
},
},
],
[
{ jsonrpc: '2.0', id: 1601, method: 'tools/call', params: { name: 'browser_start_recording', arguments: {} } },
{ jsonrpc: '2.0', id: 1602, method: 'tools/call', params: { name: 'browser_stop_recording', arguments: {} } },
{
jsonrpc: '2.0',
id: 1603,
method: 'tools/call',
params: { name: 'browser_export_artifact', arguments: { kind: 'recording', name: 'rec-smoke' } },
},
{
jsonrpc: '2.0',
id: 1604,
method: 'tools/call',
params: {
name: 'browser_start_replay',
arguments: {
steps: [createReplayStep({ type: 'click', pageId: 'page-1', selector: '#submit', args: {} })],
},
},
},
{
jsonrpc: '2.0',
id: 1605,
method: 'tools/call',
params: { name: 'browser_export_artifact', arguments: { kind: 'replay', name: 'replay-smoke' } },
},
{
jsonrpc: '2.0',
id: 1606,
method: 'tools/call',
params: { name: 'browser_list_artifacts', arguments: {} },
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1603))).toContain('status: exported');
expect(getResponseText(responses.find((message) => message.id === 1605))).toContain('status: exported');
const listText = getResponseText(responses.find((message) => message.id === 1606));
expect(listText).toContain('name: rec-smoke');
expect(listText).toContain('name: replay-smoke');
});
it('imports and deletes orchestration artifacts', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Artifact Page',
currentUrl: 'https://example.com/artifact',
click: { '#submit': {} },
wait: {
selectorSnapshots: {
'#submit': [
[
{
exists: true,
rect: {
x: 20,
y: 30,
width: 100,
height: 40,
top: 30,
right: 120,
bottom: 70,
left: 20,
},
display: 'block',
visibility: 'visible',
opacity: '1',
},
],
],
},
},
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1607,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1608,
method: 'tools/call',
params: { name: 'browser_export_artifact', arguments: { kind: 'orchestration', name: 'orc-smoke' } },
},
{
jsonrpc: '2.0',
id: 1609,
method: 'tools/call',
params: { name: 'browser_list_artifacts', arguments: {} },
},
{
jsonrpc: '2.0',
id: 1610,
method: 'tools/call',
params: { name: 'browser_import_artifact', arguments: { path: 'artifact:orc-smoke' } },
},
{
jsonrpc: '2.0',
id: 1611,
method: 'tools/call',
params: { name: 'browser_delete_artifact', arguments: { name: 'orc-smoke' } },
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1608))).toContain('status: exported');
expect(getResponseText(responses.find((message) => message.id === 1610))).toContain('status: imported');
expect(getResponseText(responses.find((message) => message.id === 1611))).toContain('status: deleted');
});
it('rejects duplicate export, invalid import payload, and missing artifact delete target', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-artifact-failures-'));
const oversizedArtifactPath = join(tempDir, 'oversized-artifact.json');
writeFileSync(oversizedArtifactPath, Buffer.alloc(5 * 1024 * 1024 + 1, 'a'));
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Artifact Failure Page',
currentUrl: 'https://example.com/artifact-failure',
recording: {
events: [{ kind: 'click', selector: '#submit', timestamp: 1710000000000 }],
},
},
],
[
{ jsonrpc: '2.0', id: 1612, method: 'tools/call', params: { name: 'browser_start_recording', arguments: {} } },
{ jsonrpc: '2.0', id: 1613, method: 'tools/call', params: { name: 'browser_stop_recording', arguments: {} } },
{ jsonrpc: '2.0', id: 1614, method: 'tools/call', params: { name: 'browser_export_artifact', arguments: { kind: 'recording', name: 'dup-artifact' } } },
{ jsonrpc: '2.0', id: 1615, method: 'tools/call', params: { name: 'browser_export_artifact', arguments: { kind: 'recording', name: 'dup-artifact' } } },
{ jsonrpc: '2.0', id: 1616, method: 'tools/call', params: { name: 'browser_import_artifact', arguments: { path: 'artifact:invalid-json' } } },
{ jsonrpc: '2.0', id: 1617, method: 'tools/call', params: { name: 'browser_delete_artifact', arguments: { name: 'missing-artifact' } } },
{ jsonrpc: '2.0', id: 1618, method: 'tools/call', params: { name: 'browser_export_artifact', arguments: { kind: 'recording', name: '../escape' } } },
{ jsonrpc: '2.0', id: 1619, method: 'tools/call', params: { name: 'browser_import_artifact', arguments: { path: 'artifact:../escape' } } },
{ jsonrpc: '2.0', id: 1620, method: 'tools/call', params: { name: 'browser_import_artifact', arguments: { path: oversizedArtifactPath } } },
]
);
expect(getResponseText(responses.find((message) => message.id === 1615))).toContain('artifact already exists');
expect(getResponseText(responses.find((message) => message.id === 1616))).toContain('artifact file not found');
expect(getResponseText(responses.find((message) => message.id === 1617))).toContain('artifact not found');
expect(getResponseText(responses.find((message) => message.id === 1618))).toContain(
'artifact name must start with a letter or number'
);
expect(getResponseText(responses.find((message) => message.id === 1619))).toContain(
'artifact name must start with a letter or number'
);
expect(getResponseText(responses.find((message) => message.id === 1620))).toContain(
'artifact file exceeds maximum size'
);
});
it('runs select_page_then_run and executes the inner single-page block on the selected page', async () => {
const responses = await runMcpRequests(
[
{
id: 'page-1',
title: 'Home',
currentUrl: 'https://example.com/',
},
{
id: 'page-2',
title: 'Docs',
currentUrl: 'https://example.com/docs',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
],
[
{
jsonrpc: '2.0',
id: 1701,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'select_page_then_run',
args: {
select: { pageIndex: 1 },
run: {
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
},
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1701))).toContain('status: completed');
});
it('runs open_page_then_run and executes the inner block on the newly opened page', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
[
{
jsonrpc: '2.0',
id: 1702,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'open_page_then_run',
args: {
open: { url: 'https://example.com/docs' },
run: {
type: 'wait_for_then_press_key',
args: {
wait: { condition: { kind: 'text', includes: 'New page visible text' }, timeoutMs: 1000 },
pressKey: { key: 'Enter' },
},
},
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1702))).toContain('status: completed');
});
it('runs close_page_then_continue and then continues with the remaining blocks', async () => {
const responses = await runMcpRequests(
[
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
{
id: 'page-2',
title: 'Docs',
currentUrl: 'https://example.com/docs',
query: {
'#status': {
exists: true,
connected: true,
innerText: 'ready state',
textContent: 'ready state',
display: 'block',
visibility: 'visible',
opacity: '1',
rect: {
x: 20,
y: 80,
width: 100,
height: 40,
top: 80,
right: 120,
bottom: 120,
left: 20,
},
},
},
},
],
[
{ jsonrpc: '2.0', id: 1703, method: 'tools/call', params: { name: 'browser_select_page', arguments: { pageId: 'page-2' } } },
{
jsonrpc: '2.0',
id: 1704,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'close_page_then_continue',
args: { close: { pageId: 'page-2' } },
}),
createOrchestrationBlock({
type: 'select_page_then_run',
args: {
select: { pageIndex: 0 },
run: {
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
},
},
continueOnError: true,
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1704))).toContain('completedBlocks: 2');
});
it('rejects missing run payloads and nested cross-page blocks', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
[
{
jsonrpc: '2.0',
id: 1705,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [createOrchestrationBlock({ type: 'select_page_then_run', args: { select: { pageIndex: 0 } } })],
},
},
},
{
jsonrpc: '2.0',
id: 1706,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'select_page_then_run',
args: {
select: { pageIndex: 0 },
run: createOrchestrationBlock({
type: 'open_page_then_run',
args: { open: { url: 'https://example.com' }, run: { type: 'assert_query', args: {} } },
}),
},
}),
],
},
},
},
]
);
expect(getResponseText(responses.find((message) => message.id === 1705))).toContain('cross-page run block is required');
expect(getResponseText(responses.find((message) => message.id === 1706))).toContain('nested cross-page blocks are not supported');
});
it('fails when select_page_then_run targets a missing page', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
[
{
jsonrpc: '2.0',
id: 1707,
method: 'tools/call',
params: {
name: 'browser_start_orchestration',
arguments: {
blocks: [
createOrchestrationBlock({
type: 'select_page_then_run',
args: {
select: { pageId: 'page-9' },
run: {
type: 'assert_query',
args: {
query: { selector: '#status', fields: ['innerText'] },
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
},
},
},
}),
],
},
},
},
{
jsonrpc: '2.0',
id: 1708,
method: 'tools/call',
params: { name: 'browser_get_orchestration', arguments: {} },
},
]
);
const text = getResponseText(responses.find((message) => message.id === 1708));
expect(text).toContain('status: failed');
expect(text).toContain('page not found: page-9');
});
});