mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
test(browser): 补充 Phase 7 回归覆盖并同步文档
补齐 richer request matching 的边界与顺序回归覆盖,锁定同优先级规则继续按创建顺序命中,并同步 Phase 7 文档说明。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd073acf47
commit
126c30ac1a
2 files changed
+179
-5
No files matched your search
@@ -63,10 +63,17 @@ Phase 6 capability details:
|
|||||||
- Phase 6 supports minimal request matching by `urlIncludes` and `method`
|
- Phase 6 supports minimal request matching by `urlIncludes` and `method`
|
||||||
- Phase 6A actions are limited to `continue` and `fail`
|
- Phase 6A actions are limited to `continue` and `fail`
|
||||||
- Phase 6B adds `fulfill` mock responses on top of the existing session-local interception model
|
- Phase 6B adds `fulfill` mock responses on top of the existing session-local interception model
|
||||||
- fulfill rules can return a custom status code, optional headers, and a UTF-8 response body
|
- fulfill rules can return a custom status code, optional response headers, and a UTF-8 response body
|
||||||
- `browser_list_requests` returns recent request summaries, not full bodies
|
- `browser_list_requests` returns recent request summaries, not full bodies
|
||||||
- response bodies are applied only to the matched request and are not persisted beyond the current MCP session
|
- response bodies are applied only to the matched request and are not persisted beyond the current MCP session
|
||||||
|
|
||||||
|
Phase 7 capability details:
|
||||||
|
|
||||||
|
- `browser_add_intercept_rule` also accepts `resourceType`, `urlPattern`, `urlRegex`, `headerMatchers`, and `priority`
|
||||||
|
- richer matching rules remain page-bound and session-local; creating a rule still binds it to the concrete page selected at creation time
|
||||||
|
- higher `priority` rules win before lower-priority rules, while equal-priority rules continue to follow creation order
|
||||||
|
- `headerMatchers` are request-matching conditions; `responseHeaders` on `fulfill` rules remain response headers
|
||||||
|
|
||||||
Minimal multi-tab workflow examples:
|
Minimal multi-tab workflow examples:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -91,12 +98,15 @@ Minimal multi-tab workflow examples:
|
|||||||
{
|
{
|
||||||
"name": "browser_add_intercept_rule",
|
"name": "browser_add_intercept_rule",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"urlIncludes": "/api/mock/users",
|
"resourceType": "XHR",
|
||||||
"method": "GET",
|
"headerMatchers": [
|
||||||
|
{ "name": "x-env", "valueIncludes": "staging" }
|
||||||
|
],
|
||||||
|
"priority": 10,
|
||||||
"action": "fulfill",
|
"action": "fulfill",
|
||||||
"statusCode": 200,
|
"statusCode": 200,
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\"users\":[1]}"
|
"body": "{\"ok\":true}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -113,7 +123,7 @@ Scoped selector notes:
|
|||||||
|
|
||||||
- `browser_click`, `browser_hover`, `browser_query`, `browser_wait_for`, and `browser_take_element_screenshot` accept optional `frameSelector` for same-origin iframes whose `contentDocument` is accessible
|
- `browser_click`, `browser_hover`, `browser_query`, `browser_wait_for`, and `browser_take_element_screenshot` accept optional `frameSelector` for same-origin iframes whose `contentDocument` is accessible
|
||||||
- the same selector-based tools accept optional `pierceShadow: true` for open shadow-root traversal
|
- the same selector-based tools accept optional `pierceShadow: true` for open shadow-root traversal
|
||||||
- closed shadow roots, frame-index routing, richer request matching, and download acceptance controls are still out of scope
|
- closed shadow roots, frame-index routing, cross-page shared rules, request body matching, advanced boolean matcher groups, and download acceptance controls are still out of scope
|
||||||
|
|
||||||
Example event wait:
|
Example event wait:
|
||||||
|
|
||||||
|
|||||||
@@ -1810,6 +1810,53 @@ describe('ccs-browser MCP server', () => {
|
|||||||
expect(listText).toContain('action: fail');
|
expect(listText).toContain('action: fail');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps richer matching rules bound to the original page after selected page changes', async () => {
|
||||||
|
const responses = await runMcpRequests(
|
||||||
|
[
|
||||||
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
||||||
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1003,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1004,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'browser_add_intercept_rule',
|
||||||
|
arguments: {
|
||||||
|
resourceType: 'XHR',
|
||||||
|
priority: 7,
|
||||||
|
action: 'continue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1005,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name: 'browser_select_page', arguments: { pageIndex: 0 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1006,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listText = getResponseText(responses.find((message) => message.id === 1006));
|
||||||
|
expect(listText).toContain('pageId: page-2');
|
||||||
|
expect(listText).toContain('resourceType: XHR');
|
||||||
|
expect(listText).toContain('priority: 7');
|
||||||
|
});
|
||||||
|
|
||||||
it('removes an interception rule by ruleId', async () => {
|
it('removes an interception rule by ruleId', async () => {
|
||||||
const responses = await runMcpRequests(
|
const responses = await runMcpRequests(
|
||||||
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
||||||
@@ -2269,6 +2316,75 @@ describe('ccs-browser MCP server', () => {
|
|||||||
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(202);
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(202);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps creation order when matched rules have the same priority', async () => {
|
||||||
|
const pages: MockPageState[] = [
|
||||||
|
{
|
||||||
|
id: 'page-1',
|
||||||
|
title: 'Home',
|
||||||
|
currentUrl: 'https://example.com/',
|
||||||
|
intercept: {
|
||||||
|
pausedRequests: [
|
||||||
|
{
|
||||||
|
requestId: 'req-same-priority',
|
||||||
|
url: 'https://example.com/api/orders',
|
||||||
|
method: 'GET',
|
||||||
|
resourceType: 'XHR',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const responses = await runMcpRequests(
|
||||||
|
pages,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 9911,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'browser_add_intercept_rule',
|
||||||
|
arguments: {
|
||||||
|
urlIncludes: '/api',
|
||||||
|
priority: 5,
|
||||||
|
action: 'fulfill',
|
||||||
|
statusCode: 201,
|
||||||
|
body: 'first',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 9912,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'browser_add_intercept_rule',
|
||||||
|
arguments: {
|
||||||
|
resourceType: 'XHR',
|
||||||
|
priority: 5,
|
||||||
|
action: 'fulfill',
|
||||||
|
statusCode: 202,
|
||||||
|
body: 'second',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 9913,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name: 'browser_list_requests', arguments: {} },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
responseTimeoutMs: 12000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const listText = getResponseText(responses.find((message) => message.id === 9913));
|
||||||
|
expect(listText).toContain('requestId: req-same-priority');
|
||||||
|
expect(listText).toContain('matchedRuleId: rule-1');
|
||||||
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(201);
|
||||||
|
});
|
||||||
|
|
||||||
it('matches urlPattern rules with wildcard syntax', async () => {
|
it('matches urlPattern rules with wildcard syntax', async () => {
|
||||||
const pages: MockPageState[] = [
|
const pages: MockPageState[] = [
|
||||||
{
|
{
|
||||||
@@ -2763,6 +2879,54 @@ describe('ccs-browser MCP server', () => {
|
|||||||
expect(getResponseText(response)).toContain('Browser MCP failed: body must be a string');
|
expect(getResponseText(response)).toContain('Browser MCP failed: body must be a string');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects intercept rules when urlRegex is invalid', async () => {
|
||||||
|
const responses = await runMcpRequests(
|
||||||
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1001,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'browser_add_intercept_rule',
|
||||||
|
arguments: {
|
||||||
|
urlRegex: '[',
|
||||||
|
action: 'continue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = responses.find((message) => message.id === 1001);
|
||||||
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
||||||
|
expect(getResponseText(response)).toContain('Browser MCP failed: urlRegex must be a valid regular expression');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects intercept rules when headerMatchers.valueRegex is invalid', async () => {
|
||||||
|
const responses = await runMcpRequests(
|
||||||
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1002,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'browser_add_intercept_rule',
|
||||||
|
arguments: {
|
||||||
|
headerMatchers: [{ name: 'x-env', valueRegex: '[' }],
|
||||||
|
action: 'continue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = responses.find((message) => message.id === 1002);
|
||||||
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
||||||
|
expect(getResponseText(response)).toContain('Browser MCP failed: headerMatchers.valueRegex must be a valid regular expression');
|
||||||
|
});
|
||||||
|
|
||||||
it('removes fulfill rules and request summaries after the bound page is closed', async () => {
|
it('removes fulfill rules and request summaries after the bound page is closed', async () => {
|
||||||
const responses = await runMcpRequests(
|
const responses = await runMcpRequests(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in new issue
Block a user