@autobest-ui/agent 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ npx --yes --package=@autobest-ui/agent@latest autobest-agent-sync react
25
25
  先启动 PRD Knowledge HTTP API,再验证 RAG MCP:
26
26
 
27
27
  ```bash
28
- RAG_API_BASE_URL="http://127.0.0.1:3000/api/knowledge" \
28
+ RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
29
29
  npx --yes \
30
30
  --package=@autobest-ui/agent@latest \
31
31
  autobest-rag-mcp
@@ -159,7 +159,7 @@ Common Skills 会写入 `~/.agents/skills`,React Skills 会写入目标项目
159
159
  RAG MCP:
160
160
 
161
161
  ```bash
162
- RAG_API_BASE_URL="http://127.0.0.1:3000/api/knowledge" \
162
+ RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
163
163
  npx --yes \
164
164
  --package="$AGENT_TARBALL_PATH" \
165
165
  autobest-rag-mcp
@@ -206,7 +206,7 @@ args = [
206
206
  ]
207
207
 
208
208
  [mcp_servers.rag-mcp-bridge.env]
209
- RAG_API_BASE_URL = "http://127.0.0.1:3000/api/knowledge"
209
+ RAG_API_BASE_URL = "http://192.168.1.12:3000/api/knowledge"
210
210
 
211
211
  [mcp_servers.azurepr-mcp-bridge]
212
212
  command = "npx"
@@ -1,4 +1,4 @@
1
- import { diffLines } from "diff";
1
+ import { diffLines } from 'diff';
2
2
 
3
3
  export const MAX_CONTENT_LENGTH = 120_000;
4
4
  const MAX_FILE_CONTENT_LENGTH = 500_000;
@@ -10,28 +10,25 @@ const HTTP_TIMEOUT_MS = 30_000;
10
10
  const FILE_FETCH_CONCURRENCY = 6;
11
11
 
12
12
  const ignoredBinaryExtensions = new Set([
13
- ".png",
14
- ".jpg",
15
- ".jpeg",
16
- ".gif",
17
- ".webp",
18
- ".ico",
19
- ".pdf",
20
- ".zip",
21
- ".woff",
22
- ".woff2",
23
- ".ttf",
24
- ".eot",
25
- ".mp3",
26
- ".mp4",
27
- ".mov",
13
+ '.png',
14
+ '.jpg',
15
+ '.jpeg',
16
+ '.gif',
17
+ '.webp',
18
+ '.ico',
19
+ '.pdf',
20
+ '.zip',
21
+ '.woff',
22
+ '.woff2',
23
+ '.ttf',
24
+ '.eot',
25
+ '.mp3',
26
+ '.mp4',
27
+ '.mov'
28
28
  ]);
29
29
 
30
30
  export function isAttachmentPath(filePath) {
31
- return (
32
- typeof filePath === "string" &&
33
- /(^|\/)\.attachments(?:\/|$)/i.test(filePath)
34
- );
31
+ return typeof filePath === 'string' && /(^|\/)\.attachments(?:\/|$)/i.test(filePath);
35
32
  }
36
33
 
37
34
  export function isBinaryPath(filePath) {
@@ -40,12 +37,10 @@ export function isBinaryPath(filePath) {
40
37
  }
41
38
 
42
39
  export function isInsideRequestedPath(filePath, requestedPath) {
43
- if (typeof filePath !== "string" || !filePath) return false;
44
- if (!requestedPath || requestedPath === "/") return true;
40
+ if (typeof filePath !== 'string' || !filePath) return false;
41
+ if (!requestedPath || requestedPath === '/') return true;
45
42
 
46
- const normalized = `/${requestedPath}`
47
- .replace(/\/{2,}/g, "/")
48
- .replace(/\/$/, "");
43
+ const normalized = `/${requestedPath}`.replace(/\/{2,}/g, '/').replace(/\/$/, '');
49
44
  return filePath === normalized || filePath.startsWith(`${normalized}/`);
50
45
  }
51
46
 
@@ -53,38 +48,36 @@ function decodePathSegment(value) {
53
48
  try {
54
49
  return decodeURIComponent(value);
55
50
  } catch {
56
- throw new Error("Azure DevOps PR 地址包含无效编码");
51
+ throw new Error('Azure DevOps PR 地址包含无效编码');
57
52
  }
58
53
  }
59
54
 
60
55
  export function parsePullRequestLocation(urlValue) {
61
56
  const url = new URL(urlValue);
62
- if (url.protocol !== "https:") {
63
- throw new Error("Azure DevOps PR 地址必须使用 HTTPS");
57
+ if (url.protocol !== 'https:') {
58
+ throw new Error('Azure DevOps PR 地址必须使用 HTTPS');
64
59
  }
65
60
 
66
61
  const hostname = url.hostname.toLowerCase();
67
- const isVisualStudio = hostname.endsWith(".visualstudio.com");
68
- const isDevAzure = hostname === "dev.azure.com";
62
+ const isVisualStudio = hostname.endsWith('.visualstudio.com');
63
+ const isDevAzure = hostname === 'dev.azure.com';
69
64
  if (!isVisualStudio && !isDevAzure) {
70
- throw new Error(
71
- "仅支持 dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址",
72
- );
65
+ throw new Error('仅支持 dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址');
73
66
  }
74
67
 
75
68
  const match = url.pathname.match(
76
69
  isVisualStudio
77
70
  ? /^\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
78
- : /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i,
71
+ : /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
79
72
  );
80
- if (!match) throw new Error("无法识别 Azure DevOps PR 地址");
73
+ if (!match) throw new Error('无法识别 Azure DevOps PR 地址');
81
74
 
82
75
  if (isVisualStudio) {
83
76
  const project = decodePathSegment(match[1]);
84
77
  return {
85
78
  apiRoot: `${url.origin}/${encodeURIComponent(project)}`,
86
79
  repository: decodePathSegment(match[2]),
87
- pullRequestId: match[3],
80
+ pullRequestId: match[3]
88
81
  };
89
82
  }
90
83
 
@@ -93,14 +86,14 @@ export function parsePullRequestLocation(urlValue) {
93
86
  return {
94
87
  apiRoot: `${url.origin}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}`,
95
88
  repository: decodePathSegment(match[3]),
96
- pullRequestId: match[4],
89
+ pullRequestId: match[4]
97
90
  };
98
91
  }
99
92
 
100
93
  async function azureGet(url, headers, optional = false) {
101
94
  const response = await fetch(url, {
102
95
  headers,
103
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
96
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
104
97
  });
105
98
  if (!response.ok) {
106
99
  if (optional) return undefined;
@@ -114,17 +107,13 @@ async function azureItemContent(repositoryRoot, filePath, commitId, headers) {
114
107
 
115
108
  const query = new URLSearchParams({
116
109
  path: filePath,
117
- includeContent: "true",
118
- "versionDescriptor.version": commitId,
119
- "versionDescriptor.versionType": "commit",
120
- "api-version": "7.1",
110
+ includeContent: 'true',
111
+ 'versionDescriptor.version': commitId,
112
+ 'versionDescriptor.versionType': 'commit',
113
+ 'api-version': '7.1'
121
114
  });
122
- const item = await azureGet(
123
- `${repositoryRoot}/items?${query.toString()}`,
124
- headers,
125
- true,
126
- );
127
- if (!item || item.isBinary || typeof item.content !== "string") {
115
+ const item = await azureGet(`${repositoryRoot}/items?${query.toString()}`, headers, true);
116
+ if (!item || item.isBinary || typeof item.content !== 'string') {
128
117
  return undefined;
129
118
  }
130
119
  if (item.content.length > MAX_FILE_CONTENT_LENGTH) return undefined;
@@ -132,19 +121,19 @@ async function azureItemContent(repositoryRoot, filePath, commitId, headers) {
132
121
  }
133
122
 
134
123
  export function extractAddedContent(previousContent, currentContent) {
135
- if (typeof currentContent !== "string") return undefined;
124
+ if (typeof currentContent !== 'string') return undefined;
136
125
 
137
126
  const additions =
138
- typeof previousContent === "string"
127
+ typeof previousContent === 'string'
139
128
  ? diffLines(previousContent, currentContent)
140
- .filter((part) => part.added)
141
- .map((part) => part.value)
142
- .join("")
129
+ .filter(part => part.added)
130
+ .map(part => part.value)
131
+ .join('')
143
132
  : currentContent;
144
133
 
145
134
  return {
146
135
  content: additions.slice(0, MAX_ADDED_CONTENT_LENGTH),
147
- truncated: additions.length > MAX_ADDED_CONTENT_LENGTH,
136
+ truncated: additions.length > MAX_ADDED_CONTENT_LENGTH
148
137
  };
149
138
  }
150
139
 
@@ -159,17 +148,12 @@ async function mapWithConcurrency(items, concurrency, mapper) {
159
148
  }
160
149
  }
161
150
 
162
- await Promise.all(
163
- Array.from(
164
- { length: Math.min(concurrency, items.length) },
165
- () => worker(),
166
- ),
167
- );
151
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
168
152
  return results;
169
153
  }
170
154
 
171
155
  function truncateText(value, maximumLength) {
172
- if (typeof value !== "string") return value;
156
+ if (typeof value !== 'string') return value;
173
157
  return value.length > maximumLength ? value.slice(0, maximumLength) : value;
174
158
  }
175
159
 
@@ -183,25 +167,19 @@ export function serializePullRequest(result) {
183
167
  const bounded = {
184
168
  ...result,
185
169
  changedFiles: sourceFiles.map(withoutAddedContent),
186
- addedContentOmittedDueToResponseLimit: 0,
170
+ addedContentOmittedDueToResponseLimit: 0
187
171
  };
188
172
 
189
173
  let omittedComments = 0;
190
174
  let omittedFiles = 0;
191
175
  let serialized = JSON.stringify(bounded, null, 2);
192
176
 
193
- while (
194
- serialized.length > MAX_CONTENT_LENGTH &&
195
- bounded.reviewComments.length > 0
196
- ) {
177
+ while (serialized.length > MAX_CONTENT_LENGTH && bounded.reviewComments.length > 0) {
197
178
  bounded.reviewComments.pop();
198
179
  omittedComments += 1;
199
180
  serialized = JSON.stringify(bounded, null, 2);
200
181
  }
201
- while (
202
- serialized.length > MAX_CONTENT_LENGTH &&
203
- bounded.changedFiles.length > 0
204
- ) {
182
+ while (serialized.length > MAX_CONTENT_LENGTH && bounded.changedFiles.length > 0) {
205
183
  bounded.changedFiles.pop();
206
184
  omittedFiles += 1;
207
185
  serialized = JSON.stringify(bounded, null, 2);
@@ -233,38 +211,34 @@ export function serializePullRequest(result) {
233
211
  title: truncateText(result.title, 500),
234
212
  status: result.status,
235
213
  responseTruncated: true,
236
- error: "PR 响应超过 MCP 内容上限",
214
+ error: 'PR 响应超过 MCP 内容上限'
237
215
  });
238
216
  }
239
217
 
240
218
  export async function getAzurePullRequest(urlValue, pat) {
241
219
  const inputUrl = new URL(urlValue);
242
- const requestedPath = inputUrl.searchParams.get("path");
243
- const { apiRoot, repository, pullRequestId } =
244
- parsePullRequestLocation(urlValue);
220
+ const requestedPath = inputUrl.searchParams.get('path');
221
+ const { apiRoot, repository, pullRequestId } = parsePullRequestLocation(urlValue);
245
222
  const encodedRepository = encodeURIComponent(repository);
246
223
  const repositoryRoot = `${apiRoot}/_apis/git/repositories/${encodedRepository}`;
247
224
  const root = `${repositoryRoot}/pullRequests/${pullRequestId}`;
248
225
  const headers = {
249
- Accept: "application/json",
250
- Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}`,
226
+ Accept: 'application/json',
227
+ Authorization: `Basic ${Buffer.from(`:${pat}`).toString('base64')}`
251
228
  };
252
229
 
253
230
  const [pullRequest, threads, workItems, iterations] = await Promise.all([
254
231
  azureGet(`${root}?api-version=7.1`, headers),
255
232
  azureGet(`${root}/threads?api-version=7.1`, headers, true),
256
233
  azureGet(`${root}/workitems?api-version=7.1`, headers, true),
257
- azureGet(`${root}/iterations?api-version=7.1`, headers, true),
234
+ azureGet(`${root}/iterations?api-version=7.1`, headers, true)
258
235
  ]);
259
- const latestIterationId = Math.max(
260
- 0,
261
- ...(iterations?.value || []).map(({ id }) => Number(id) || 0),
262
- );
236
+ const latestIterationId = Math.max(0, ...(iterations?.value || []).map(({ id }) => Number(id) || 0));
263
237
  const changes = latestIterationId
264
238
  ? await azureGet(
265
239
  `${root}/iterations/${latestIterationId}/changes?$top=${MAX_CHANGED_FILES}&api-version=7.1`,
266
240
  headers,
267
- true,
241
+ true
268
242
  )
269
243
  : undefined;
270
244
 
@@ -274,30 +248,26 @@ export async function getAzurePullRequest(urlValue, pat) {
274
248
  .filter(({ item }) => !isAttachmentPath(item?.path))
275
249
  .filter(({ item }) => isInsideRequestedPath(item?.path, requestedPath))
276
250
  .slice(0, MAX_CHANGED_FILES);
277
- const changedFiles = await mapWithConcurrency(
278
- changeEntries,
279
- FILE_FETCH_CONCURRENCY,
280
- async ({ changeType, item }) => {
281
- const filePath = item?.path;
282
- const deleted = String(changeType).toLowerCase().includes("delete");
283
- const isFile = !item?.gitObjectType || item.gitObjectType === "blob";
284
- if (!filePath || deleted || !isFile || isBinaryPath(filePath)) {
285
- return { changeType, path: filePath };
286
- }
287
-
288
- const [previousContent, currentContent] = await Promise.all([
289
- azureItemContent(repositoryRoot, filePath, targetCommitId, headers),
290
- azureItemContent(repositoryRoot, filePath, sourceCommitId, headers),
291
- ]);
292
- const additions = extractAddedContent(previousContent, currentContent);
293
- return {
294
- changeType,
295
- path: filePath,
296
- addedContent: additions?.content || undefined,
297
- addedContentTruncated: additions?.truncated || undefined,
298
- };
299
- },
300
- );
251
+ const changedFiles = await mapWithConcurrency(changeEntries, FILE_FETCH_CONCURRENCY, async ({ changeType, item }) => {
252
+ const filePath = item?.path;
253
+ const deleted = String(changeType).toLowerCase().includes('delete');
254
+ const isFile = !item?.gitObjectType || item.gitObjectType === 'blob';
255
+ if (!filePath || deleted || !isFile || isBinaryPath(filePath)) {
256
+ return { changeType, path: filePath };
257
+ }
258
+
259
+ const [previousContent, currentContent] = await Promise.all([
260
+ azureItemContent(repositoryRoot, filePath, targetCommitId, headers),
261
+ azureItemContent(repositoryRoot, filePath, sourceCommitId, headers)
262
+ ]);
263
+ const additions = extractAddedContent(previousContent, currentContent);
264
+ return {
265
+ changeType,
266
+ path: filePath,
267
+ addedContent: additions?.content || undefined,
268
+ addedContentTruncated: additions?.truncated || undefined
269
+ };
270
+ });
301
271
 
302
272
  return {
303
273
  title: truncateText(pullRequest.title, 2_000),
@@ -307,21 +277,21 @@ export async function getAzurePullRequest(urlValue, pat) {
307
277
  targetBranch: pullRequest.targetRefName,
308
278
  reviewers: (pullRequest.reviewers || []).map(({ displayName, vote }) => ({
309
279
  displayName: truncateText(displayName, 1_000),
310
- vote,
280
+ vote
311
281
  })),
312
282
  reviewComments: (threads?.value || [])
313
- .flatMap((thread) =>
283
+ .flatMap(thread =>
314
284
  (thread.comments || [])
315
- .filter((comment) => !comment.isDeleted && comment.content)
285
+ .filter(comment => !comment.isDeleted && comment.content)
316
286
  .map(({ content, author }) => ({
317
287
  content: truncateText(content, MAX_COMMENT_LENGTH),
318
- author: truncateText(author?.displayName, 1_000),
319
- })),
288
+ author: truncateText(author?.displayName, 1_000)
289
+ }))
320
290
  )
321
291
  .slice(0, MAX_REVIEW_COMMENTS),
322
292
  workItems: (workItems?.value || []).map(({ id, url }) => ({ id, url })),
323
293
  requestedPath: requestedPath || undefined,
324
294
  latestIterationId,
325
- changedFiles,
295
+ changedFiles
326
296
  };
327
297
  }
@@ -1,65 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { z } from "zod";
6
- import { getAzurePullRequest, serializePullRequest } from "./azure-devops.js";
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { getAzurePullRequest, serializePullRequest } from './azure-devops.js';
7
7
 
8
8
  const server = new McpServer(
9
- { name: "azurepr-mcp-bridge", version: "1.0.0" },
9
+ { name: 'azurepr-mcp-bridge', version: '1.0.0' },
10
10
  {
11
11
  instructions:
12
- "Use get_azure_pull_request to read Azure DevOps pull request metadata, review comments, linked work items, and changed text files. " +
13
- "The tool is read-only and requires an HTTPS PR URL from dev.azure.com or *.visualstudio.com. " +
14
- "AZURE_DEVOPS_PAT is used only for Azure DevOps API authentication and must never be exposed in output.",
15
- },
12
+ 'Use get_azure_pull_request to read Azure DevOps pull request metadata, review comments, linked work items, and changed text files. ' +
13
+ 'The tool is read-only and requires an HTTPS PR URL from dev.azure.com or *.visualstudio.com. ' +
14
+ 'AZURE_DEVOPS_PAT is used only for Azure DevOps API authentication and must never be exposed in output.'
15
+ }
16
16
  );
17
17
 
18
18
  server.registerTool(
19
- "get_azure_pull_request",
19
+ 'get_azure_pull_request',
20
20
  {
21
- title: "读取 Azure DevOps PR",
21
+ title: '读取 Azure DevOps PR',
22
22
  description:
23
- "读取 PR 基本信息、评审意见、关联工作项和变更文件。URL 可使用 path 查询参数限定仓库目录,例如 ?path=/src。",
23
+ '读取 PR 基本信息、评审意见、关联工作项和变更文件。URL 可使用 path 查询参数限定仓库目录,例如 ?path=/src。',
24
24
  inputSchema: {
25
- url: z
26
- .url()
27
- .describe("dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址"),
25
+ url: z.url().describe('dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址')
28
26
  },
29
27
  annotations: {
30
28
  readOnlyHint: true,
31
29
  destructiveHint: false,
32
30
  idempotentHint: true,
33
- openWorldHint: true,
34
- },
31
+ openWorldHint: true
32
+ }
35
33
  },
36
34
  async ({ url }) => {
37
35
  try {
38
36
  const pat = process.env.AZURE_DEVOPS_PAT?.trim();
39
37
  if (!pat) {
40
- throw new Error(
41
- "AZURE_DEVOPS_PAT 未配置,Stdio MCP 无法读取 Azure DevOps PR",
42
- );
38
+ throw new Error('AZURE_DEVOPS_PAT 未配置,Stdio MCP 无法读取 Azure DevOps PR');
43
39
  }
44
40
  const result = await getAzurePullRequest(url, pat);
45
41
  return {
46
- content: [{ type: "text", text: serializePullRequest(result) }],
42
+ content: [{ type: 'text', text: serializePullRequest(result) }]
47
43
  };
48
44
  } catch (error) {
49
45
  const message = error instanceof Error ? error.message : String(error);
50
46
  return {
51
47
  isError: true,
52
- content: [{ type: "text", text: message }],
48
+ content: [{ type: 'text', text: message }]
53
49
  };
54
50
  }
55
- },
51
+ }
56
52
  );
57
53
 
58
54
  async function main() {
59
55
  await server.connect(new StdioServerTransport());
60
56
  }
61
57
 
62
- main().catch((error) => {
63
- console.error("Failed to start azurepr-mcp-bridge:", error);
58
+ main().catch(error => {
59
+ console.error('Failed to start azurepr-mcp-bridge:', error);
64
60
  process.exitCode = 1;
65
61
  });
@@ -1,9 +1,9 @@
1
- import assert from "node:assert/strict";
2
- import path from "node:path";
3
- import test from "node:test";
4
- import { fileURLToPath } from "node:url";
5
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import test from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
7
  import {
8
8
  extractAddedContent,
9
9
  isAttachmentPath,
@@ -11,66 +11,56 @@ import {
11
11
  isInsideRequestedPath,
12
12
  MAX_CONTENT_LENGTH,
13
13
  parsePullRequestLocation,
14
- serializePullRequest,
15
- } from "./azure-devops.js";
14
+ serializePullRequest
15
+ } from './azure-devops.js';
16
16
 
17
17
  const directory = path.dirname(fileURLToPath(import.meta.url));
18
18
 
19
- test("parses supported Azure DevOps PR URLs", () => {
19
+ test('parses supported Azure DevOps PR URLs', () => {
20
20
  assert.deepEqual(
21
- parsePullRequestLocation(
22
- "https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
23
- ),
21
+ parsePullRequestLocation('https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'),
24
22
  {
25
- apiRoot: "https://autobest.visualstudio.com/AutoBestChina",
26
- repository: "web",
27
- pullRequestId: "42",
28
- },
23
+ apiRoot: 'https://autobest.visualstudio.com/AutoBestChina',
24
+ repository: 'web',
25
+ pullRequestId: '42'
26
+ }
29
27
  );
30
28
  assert.deepEqual(
31
- parsePullRequestLocation(
32
- "https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7",
33
- ),
29
+ parsePullRequestLocation('https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7'),
34
30
  {
35
- apiRoot: "https://dev.azure.com/example/My%20Project",
36
- repository: "front end",
37
- pullRequestId: "7",
38
- },
39
- );
40
- assert.throws(
41
- () =>
42
- parsePullRequestLocation(
43
- "https://example.com/org/project/_git/repo/pullrequest/1",
44
- ),
45
- /仅支持/,
31
+ apiRoot: 'https://dev.azure.com/example/My%20Project',
32
+ repository: 'front end',
33
+ pullRequestId: '7'
34
+ }
46
35
  );
36
+ assert.throws(() => parsePullRequestLocation('https://example.com/org/project/_git/repo/pullrequest/1'), /仅支持/);
47
37
  });
48
38
 
49
- test("filters paths and extracts added lines", () => {
50
- assert.equal(isAttachmentPath("/docs/.attachments/image.png"), true);
51
- assert.equal(isBinaryPath("/assets/logo.PNG"), true);
52
- assert.equal(isInsideRequestedPath("/src/app/index.js", "/src"), true);
53
- assert.equal(isInsideRequestedPath("/scripts/build.js", "/src"), false);
54
- assert.deepEqual(extractAddedContent("a\nb\n", "a\nc\nb\n"), {
55
- content: "c\n",
56
- truncated: false,
39
+ test('filters paths and extracts added lines', () => {
40
+ assert.equal(isAttachmentPath('/docs/.attachments/image.png'), true);
41
+ assert.equal(isBinaryPath('/assets/logo.PNG'), true);
42
+ assert.equal(isInsideRequestedPath('/src/app/index.js', '/src'), true);
43
+ assert.equal(isInsideRequestedPath('/scripts/build.js', '/src'), false);
44
+ assert.deepEqual(extractAddedContent('a\nb\n', 'a\nc\nb\n'), {
45
+ content: 'c\n',
46
+ truncated: false
57
47
  });
58
48
  });
59
49
 
60
- test("serializes oversized results as valid bounded JSON", () => {
50
+ test('serializes oversized results as valid bounded JSON', () => {
61
51
  const text = serializePullRequest({
62
- title: "Large PR",
63
- description: "description",
64
- status: "active",
52
+ title: 'Large PR',
53
+ description: 'description',
54
+ status: 'active',
65
55
  reviewers: [],
66
56
  reviewComments: [],
67
57
  workItems: [],
68
58
  latestIterationId: 1,
69
59
  changedFiles: Array.from({ length: 20 }, (_, index) => ({
70
- changeType: "edit",
60
+ changeType: 'edit',
71
61
  path: `/src/file-${index}.js`,
72
- addedContent: "x".repeat(20_000),
73
- })),
62
+ addedContent: 'x'.repeat(20_000)
63
+ }))
74
64
  });
75
65
 
76
66
  assert.ok(text.length <= MAX_CONTENT_LENGTH);
@@ -78,35 +68,36 @@ test("serializes oversized results as valid bounded JSON", () => {
78
68
  assert.ok(JSON.parse(text).addedContentOmittedDueToResponseLimit > 0);
79
69
  });
80
70
 
81
- test("initializes and exposes the Azure PR tool over stdio", async () => {
71
+ test('initializes and exposes the Azure PR tool over stdio', async () => {
82
72
  const client = new Client({
83
- name: "azurepr-mcp-bridge-test",
84
- version: "1.0.0",
73
+ name: 'azurepr-mcp-bridge-test',
74
+ version: '1.0.0'
85
75
  });
86
76
  const transport = new StdioClientTransport({
87
77
  command: process.execPath,
88
- args: [path.join(directory, "index.js")],
89
- cwd: path.resolve(directory, "../.."),
90
- stderr: "pipe",
78
+ args: [path.join(directory, 'index.js')],
79
+ cwd: path.resolve(directory, '../..'),
80
+ stderr: 'pipe'
91
81
  });
92
82
 
93
83
  try {
94
84
  await client.connect(transport);
95
- assert.equal(client.getServerVersion()?.name, "azurepr-mcp-bridge");
96
- assert.match(client.getInstructions() ?? "", /AZURE_DEVOPS_PAT/);
85
+ assert.equal(client.getServerVersion()?.name, 'azurepr-mcp-bridge');
86
+ assert.match(client.getInstructions() ?? '', /AZURE_DEVOPS_PAT/);
97
87
 
98
88
  const { tools } = await client.listTools();
99
- assert.deepEqual(tools.map(({ name }) => name), [
100
- "get_azure_pull_request",
101
- ]);
89
+ assert.deepEqual(
90
+ tools.map(({ name }) => name),
91
+ 'get_azure_pull_request'
92
+ );
102
93
  assert.equal(tools[0].annotations?.readOnlyHint, true);
103
94
  assert.equal(tools[0].annotations?.openWorldHint, true);
104
95
 
105
96
  const missingPatResult = await client.callTool({
106
- name: "get_azure_pull_request",
97
+ name: 'get_azure_pull_request',
107
98
  arguments: {
108
- url: "https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
109
- },
99
+ url: 'https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'
100
+ }
110
101
  });
111
102
  assert.equal(missingPatResult.isError, true);
112
103
  assert.match(missingPatResult.content[0].text, /AZURE_DEVOPS_PAT 未配置/);