mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(browser): 完成 Phase 10C orchestration 能力
新增固定流程块 orchestration 闭环,支持 wait_then_click、wait_then_type、run_replay_sequence 与 assert_query。 并补充 block 级失败定位与相关回归覆盖。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2099145e2c
commit
77855035f4
@@ -69,6 +69,9 @@ const TOOL_CLEAR_RECORDING = 'browser_clear_recording';
|
||||
const TOOL_START_REPLAY = 'browser_start_replay';
|
||||
const TOOL_GET_REPLAY = 'browser_get_replay';
|
||||
const TOOL_CANCEL_REPLAY = 'browser_cancel_replay';
|
||||
const TOOL_START_ORCHESTRATION = 'browser_start_orchestration';
|
||||
const TOOL_GET_ORCHESTRATION = 'browser_get_orchestration';
|
||||
const TOOL_CANCEL_ORCHESTRATION = 'browser_cancel_orchestration';
|
||||
const TOOL_TAKE_SCREENSHOT = 'browser_take_screenshot';
|
||||
const TOOL_WAIT_FOR = 'browser_wait_for';
|
||||
const TOOL_EVAL = 'browser_eval';
|
||||
@@ -107,6 +110,9 @@ const TOOL_NAMES = [
|
||||
TOOL_START_REPLAY,
|
||||
TOOL_GET_REPLAY,
|
||||
TOOL_CANCEL_REPLAY,
|
||||
TOOL_START_ORCHESTRATION,
|
||||
TOOL_GET_ORCHESTRATION,
|
||||
TOOL_CANCEL_ORCHESTRATION,
|
||||
TOOL_TAKE_SCREENSHOT,
|
||||
TOOL_WAIT_FOR,
|
||||
TOOL_EVAL,
|
||||
@@ -148,6 +154,9 @@ let latestRecordingSession = null;
|
||||
let nextReplayCounter = 1;
|
||||
let activeReplaySession = null;
|
||||
let latestReplaySession = null;
|
||||
let nextOrchestrationCounter = 1;
|
||||
let activeOrchestrationSession = null;
|
||||
let latestOrchestrationSession = null;
|
||||
const interceptRules = [];
|
||||
const recentRequests = [];
|
||||
const recentDownloads = [];
|
||||
@@ -800,6 +809,38 @@ function getTools() {
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_START_ORCHESTRATION,
|
||||
description: 'Start an orchestration session that runs fixed browser workflow blocks on the selected page.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
blocks: { type: 'array', items: { type: 'object' } },
|
||||
pageIndex: { type: 'integer', minimum: 0 },
|
||||
pageId: { type: 'string' },
|
||||
},
|
||||
required: ['blocks'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_GET_ORCHESTRATION,
|
||||
description: 'Read the current orchestration status and result summary from session-local state.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_CANCEL_ORCHESTRATION,
|
||||
description: 'Cancel the active orchestration session and keep the summary in session-local state.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_TAKE_SCREENSHOT,
|
||||
description:
|
||||
@@ -2351,6 +2392,12 @@ function formatRecordingSummary(session) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function createOrchestrationId() {
|
||||
const orchestrationId = `orc_${String(nextOrchestrationCounter).padStart(4, '0')}`;
|
||||
nextOrchestrationCounter += 1;
|
||||
return orchestrationId;
|
||||
}
|
||||
|
||||
function requireLatestReplay() {
|
||||
if (!latestReplaySession) {
|
||||
throw new Error('no replay available');
|
||||
@@ -2358,6 +2405,13 @@ function requireLatestReplay() {
|
||||
return latestReplaySession;
|
||||
}
|
||||
|
||||
function requireLatestOrchestration() {
|
||||
if (!latestOrchestrationSession) {
|
||||
throw new Error('no orchestration available');
|
||||
}
|
||||
return latestOrchestrationSession;
|
||||
}
|
||||
|
||||
function formatReplaySummary(session) {
|
||||
return [
|
||||
`replayId: ${session.replayId}`,
|
||||
@@ -2372,6 +2426,20 @@ function formatReplaySummary(session) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function formatOrchestrationSummary(session) {
|
||||
return [
|
||||
`orchestrationId: ${session.orchestrationId}`,
|
||||
`pageId: ${session.pageId}`,
|
||||
`pageIndex: ${session.pageIndex}`,
|
||||
`blockCount: ${session.blockCount}`,
|
||||
`completedBlocks: ${session.completedBlocks}`,
|
||||
`currentBlockIndex: ${session.currentBlockIndex}`,
|
||||
`failedBlockIndex: ${session.failedBlockIndex === null ? 'none' : session.failedBlockIndex}`,
|
||||
`error: ${session.error || 'none'}`,
|
||||
`status: ${session.status}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function formatRecordingDetail(session) {
|
||||
const lines = [formatRecordingSummary(session), 'steps:'];
|
||||
if (session.steps.length === 0) {
|
||||
@@ -2441,6 +2509,31 @@ function validateReplayStep(step, replayPageId) {
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_ORCHESTRATION_BLOCK_TYPES = new Set([
|
||||
'wait_for_then_click',
|
||||
'wait_for_then_type',
|
||||
'wait_for_then_press_key',
|
||||
'run_replay_sequence',
|
||||
'assert_query',
|
||||
]);
|
||||
|
||||
function requireOrchestrationBlocks(toolArgs) {
|
||||
const blocks = Array.isArray(toolArgs.blocks) ? toolArgs.blocks : null;
|
||||
if (!blocks || blocks.length === 0) {
|
||||
throw new Error('blocks must be a non-empty array');
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function validateOrchestrationBlock(block) {
|
||||
if (!block || typeof block !== 'object') {
|
||||
throw new Error('invalid orchestration block payload');
|
||||
}
|
||||
if (!SUPPORTED_ORCHESTRATION_BLOCK_TYPES.has(block.type)) {
|
||||
throw new Error('unsupported orchestration block type');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRecordedEvent(pageId, event) {
|
||||
if (!event || typeof event !== 'object') {
|
||||
return null;
|
||||
@@ -4843,6 +4936,118 @@ async function handleCancelReplay() {
|
||||
return formatReplaySummary(canceled);
|
||||
}
|
||||
|
||||
async function executeOrchestrationBlock(session, block) {
|
||||
if (block.type === 'wait_for_then_click') {
|
||||
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
|
||||
await handleClick({ pageId: session.pageId, ...block.args.click });
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'wait_for_then_type') {
|
||||
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
|
||||
await handleType({ pageId: session.pageId, ...block.args.type });
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'wait_for_then_press_key') {
|
||||
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
|
||||
await handlePressKey({ pageId: session.pageId, ...block.args.pressKey });
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'run_replay_sequence') {
|
||||
const replayResultText = await handleStartReplay({ pageId: session.pageId, steps: block.args.steps });
|
||||
if (replayResultText.includes('status: failed')) {
|
||||
throw new Error(replayResultText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.type === 'assert_query') {
|
||||
const queryText = await handleQuery({ pageId: session.pageId, ...block.args.query });
|
||||
const expectedField = String(block.args.assert.field || '');
|
||||
const expectedValue = String(block.args.assert.equals || '');
|
||||
if (!queryText.includes(`${expectedField}: ${expectedValue}`)) {
|
||||
throw new Error(`assert_query failed for field ${expectedField}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('unsupported orchestration block type');
|
||||
}
|
||||
|
||||
async function executeOrchestrationBlocks(session) {
|
||||
for (let index = 0; index < session.blocks.length; index += 1) {
|
||||
const block = session.blocks[index];
|
||||
session.currentBlockIndex = index;
|
||||
validateOrchestrationBlock(block);
|
||||
await executeOrchestrationBlock(session, block);
|
||||
session.completedBlocks = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartOrchestration(toolArgs) {
|
||||
if (activeOrchestrationSession) {
|
||||
throw new Error('orchestration already active');
|
||||
}
|
||||
|
||||
const pages = await listPageTargets();
|
||||
if (pages.length === 0) {
|
||||
throw new Error('Browser MCP did not find any page targets in the current Chrome session.');
|
||||
}
|
||||
|
||||
const { page, pageIndex } = resolveTargetPage(pages, toolArgs, selectedPageId);
|
||||
if (!page.webSocketDebuggerUrl) {
|
||||
throw new Error(`Browser MCP page ${pageIndex} does not expose a websocket debugger URL.`);
|
||||
}
|
||||
|
||||
const blocks = requireOrchestrationBlocks(toolArgs);
|
||||
const session = {
|
||||
orchestrationId: createOrchestrationId(),
|
||||
pageId: page.id,
|
||||
pageIndex,
|
||||
status: 'running',
|
||||
blockCount: blocks.length,
|
||||
completedBlocks: 0,
|
||||
currentBlockIndex: 0,
|
||||
failedBlockIndex: null,
|
||||
error: null,
|
||||
blocks,
|
||||
};
|
||||
|
||||
latestOrchestrationSession = session;
|
||||
activeOrchestrationSession = session;
|
||||
|
||||
try {
|
||||
await executeOrchestrationBlocks(session);
|
||||
session.status = 'completed';
|
||||
} catch (error) {
|
||||
session.status = 'failed';
|
||||
session.failedBlockIndex = session.currentBlockIndex;
|
||||
session.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
activeOrchestrationSession = null;
|
||||
latestOrchestrationSession = session;
|
||||
return formatOrchestrationSummary(session);
|
||||
}
|
||||
|
||||
async function handleGetOrchestration() {
|
||||
const session = requireLatestOrchestration();
|
||||
return formatOrchestrationSummary(session);
|
||||
}
|
||||
|
||||
async function handleCancelOrchestration() {
|
||||
if (!activeOrchestrationSession) {
|
||||
throw new Error('no active orchestration');
|
||||
}
|
||||
activeOrchestrationSession.status = 'canceled';
|
||||
const canceled = activeOrchestrationSession;
|
||||
activeOrchestrationSession = null;
|
||||
latestOrchestrationSession = canceled;
|
||||
return formatOrchestrationSummary(canceled);
|
||||
}
|
||||
|
||||
async function handleToolCall(message) {
|
||||
const id = message.id;
|
||||
const params = message.params || {};
|
||||
@@ -5087,6 +5292,30 @@ async function handleToolCall(message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_START_ORCHESTRATION) {
|
||||
const text = await handleStartOrchestration(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_GET_ORCHESTRATION) {
|
||||
const text = await handleGetOrchestration(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_CANCEL_ORCHESTRATION) {
|
||||
const text = await handleCancelOrchestration(toolArgs);
|
||||
writeResponse(id, {
|
||||
content: [{ type: 'text', text }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === TOOL_TAKE_SCREENSHOT) {
|
||||
const text = await handleScreenshot(toolArgs);
|
||||
writeResponse(id, {
|
||||
|
||||
@@ -439,6 +439,10 @@ function createReplayStep(step: Record<string, unknown>): Record<string, unknown
|
||||
return step;
|
||||
}
|
||||
|
||||
function createOrchestrationBlock(block: Record<string, unknown>): Record<string, unknown> {
|
||||
return block;
|
||||
}
|
||||
|
||||
function parseJsonArgument(expression: string, key: string): string | undefined {
|
||||
const marker = `const ${key} = JSON.parse(`;
|
||||
const start = expression.indexOf(marker);
|
||||
@@ -1836,6 +1840,9 @@ describe('ccs-browser MCP server', () => {
|
||||
'browser_start_replay',
|
||||
'browser_get_replay',
|
||||
'browser_cancel_replay',
|
||||
'browser_start_orchestration',
|
||||
'browser_get_orchestration',
|
||||
'browser_cancel_orchestration',
|
||||
'browser_take_screenshot',
|
||||
'browser_wait_for',
|
||||
'browser_eval',
|
||||
@@ -1968,6 +1975,17 @@ describe('ccs-browser MCP server', () => {
|
||||
const cancelReplayTool = tools.find((tool) => tool.name === 'browser_cancel_replay');
|
||||
expect(cancelReplayTool?.inputSchema).toMatchObject({ type: 'object' });
|
||||
|
||||
const startOrchestrationTool = tools.find((tool) => tool.name === 'browser_start_orchestration');
|
||||
expect(startOrchestrationTool?.inputSchema?.properties?.blocks).toMatchObject({ type: 'array' });
|
||||
expect(startOrchestrationTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
||||
expect(startOrchestrationTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
||||
|
||||
const getOrchestrationTool = tools.find((tool) => tool.name === 'browser_get_orchestration');
|
||||
expect(getOrchestrationTool?.inputSchema).toMatchObject({ type: 'object' });
|
||||
|
||||
const cancelOrchestrationTool = tools.find((tool) => tool.name === 'browser_cancel_orchestration');
|
||||
expect(cancelOrchestrationTool?.inputSchema).toMatchObject({ type: 'object' });
|
||||
|
||||
const queryTool = tools.find((tool) => tool.name === 'browser_query');
|
||||
expect(queryTool?.inputSchema?.properties?.fields).toMatchObject({
|
||||
type: 'array',
|
||||
@@ -6100,6 +6118,298 @@ describe('ccs-browser MCP server', () => {
|
||||
expect(getResponseText(responses.find((message) => message.id === 1121))).toContain('completedSteps: 2');
|
||||
});
|
||||
|
||||
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('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('drags local files onto normal, frame, and shadow dropzones', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-drag-files-'));
|
||||
const invoicePath = join(tempDir, 'invoice.pdf');
|
||||
|
||||
Reference in New Issue
Block a user