Skip to content

Commit a8f7273

Browse files
gaurav-singh-9227manoj-k04
authored andcommitted
Merge pull request #186 from manoj-k04/feature/edit-test-case-functionality
feat: Add updateTestCase functionality to Test Management
2 parents 94eb783 + 00ef595 commit a8f7273

File tree

12 files changed

+955
-0
lines changed

12 files changed

+955
-0
lines changed

package-lock.json

Lines changed: 430 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"csv-parse": "^5.6.0",
4343
"dotenv": "^16.5.0",
4444
"form-data": "^4.0.2",
45+
"octokit": "^5.0.5",
4546
"pino": "^9.6.0",
4647
"pino-pretty": "^13.0.0",
4748
"sharp": "^0.34.1",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// src/repo/GitHubRepositoryFetcher.ts
2+
3+
import { Octokit } from "octokit";
4+
import { RepositoryFetcher, RepoFileMeta } from "./RepositoryFetcher.js";
5+
6+
export class GitHubRepositoryFetcher implements RepositoryFetcher {
7+
private client: Octokit;
8+
9+
constructor(
10+
private owner: string,
11+
private repo: string,
12+
private branch: string,
13+
token: string,
14+
) {
15+
this.client = new Octokit({ auth: token });
16+
}
17+
18+
async getTree(): Promise<RepoFileMeta[]> {
19+
const treeRes = await this.client.rest.git.getTree({
20+
owner: this.owner,
21+
repo: this.repo,
22+
tree_sha: this.branch,
23+
recursive: "true",
24+
});
25+
26+
return treeRes.data.tree.map((item: any) => ({
27+
path: item.path!,
28+
sha: item.sha!,
29+
type: item.type as "blob" | "tree",
30+
size: item.size,
31+
}));
32+
}
33+
34+
async getFileContent(path: string): Promise<string> {
35+
const res = await this.client.rest.repos.getContent({
36+
owner: this.owner,
37+
repo: this.repo,
38+
path,
39+
ref: this.branch,
40+
});
41+
42+
if (!("content" in res.data)) {
43+
throw new Error("Not a file");
44+
}
45+
46+
return Buffer.from(res.data.content, "base64").toString("utf8");
47+
}
48+
}

src/repo/RemoteIndexer.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// src/repo/RemoteIndexer.ts
2+
3+
import { RepositoryFetcher, RepoFileMeta } from "./RepositoryFetcher.js";
4+
5+
export class RemoteIndexer {
6+
private index: Map<string, RepoFileMeta> = new Map();
7+
8+
constructor(private fetcher: RepositoryFetcher) {}
9+
10+
async buildIndex() {
11+
const tree = await this.fetcher.getTree();
12+
tree.forEach((item: RepoFileMeta) => this.index.set(item.path, item));
13+
}
14+
15+
getIndex() {
16+
return this.index;
17+
}
18+
19+
fileExists(path: string) {
20+
return this.index.has(path);
21+
}
22+
}

src/repo/RepositoryFetcher.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// src/repo/RepositoryFetcher.ts
2+
3+
export interface RepoFileMeta {
4+
path: string;
5+
sha: string;
6+
type: "blob" | "tree";
7+
size?: number;
8+
}
9+
10+
export interface RepositoryFetcher {
11+
getTree(): Promise<RepoFileMeta[]>; // indexing
12+
getFileContent(path: string): Promise<string>; // actual file fetching
13+
}

src/repo/repo-setup.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// src/repo/repo-setup.ts
2+
3+
import { GitHubRepositoryFetcher } from "./GitHubRepositoryFetcher.js";
4+
import { RemoteIndexer } from "./RemoteIndexer.js";
5+
import { createFetchFromRepoTool } from "../tools/fetch-from-repo.js";
6+
import { createDesignRepoContextResource } from "../resources/design-repo-context.js";
7+
8+
export interface RepoConfig {
9+
owner: string;
10+
repo: string;
11+
branch: string;
12+
token: string;
13+
}
14+
15+
export async function setupRemoteRepoMCP(config: RepoConfig) {
16+
const fetcher = new GitHubRepositoryFetcher(
17+
config.owner,
18+
config.repo,
19+
config.branch,
20+
config.token,
21+
);
22+
23+
const indexer = new RemoteIndexer(fetcher);
24+
await indexer.buildIndex(); // build repo map
25+
26+
const fetchFromRepoTool = createFetchFromRepoTool(indexer, fetcher);
27+
const contextResource = createDesignRepoContextResource(indexer, fetcher);
28+
29+
return { fetchFromRepoTool, contextResource, indexer };
30+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// src/resources/design-repo-context.ts
2+
3+
import { RemoteIndexer } from "../repo/RemoteIndexer.js";
4+
import { RepositoryFetcher } from "../repo/RepositoryFetcher.js";
5+
6+
export function createDesignRepoContextResource(
7+
indexer: RemoteIndexer,
8+
fetcher: RepositoryFetcher,
9+
) {
10+
return {
11+
uri: "repo://design-context",
12+
name: "Design Repository Context",
13+
description:
14+
"Provides context from the remote design repository including tokens, components, and documentation",
15+
mimeType: "text/plain",
16+
handler: async () => {
17+
const index = indexer.getIndex();
18+
19+
// Automatically select relevant files
20+
const relevantFiles = [...index.keys()].filter((path) =>
21+
path.match(/(tokens|design|components|docs).*\.(json|md|tsx?|css)$/),
22+
);
23+
24+
const parts: string[] = [];
25+
26+
// Limit for safety and performance
27+
for (const path of relevantFiles.slice(0, 10)) {
28+
try {
29+
const content = await fetcher.getFileContent(path);
30+
parts.push(`### File: ${path}\n\`\`\`\n${content}\n\`\`\``);
31+
} catch (error: any) {
32+
parts.push(`### File: ${path}\nError: ${error.message}`);
33+
}
34+
}
35+
36+
return {
37+
contents: [
38+
{
39+
uri: "repo://design-context",
40+
mimeType: "text/plain",
41+
text: `# Client Repository Context\n\n${parts.join("\n\n")}`,
42+
},
43+
],
44+
};
45+
},
46+
};
47+
}

src/server-factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import addBuildInsightsTools from "./tools/build-insights.js";
2020
import { setupOnInitialized } from "./oninitialized.js";
2121
import { BrowserStackConfig } from "./lib/types.js";
2222
import addRCATools from "./tools/rca-agent.js";
23+
import addRemoteRepoTools from "./tools/remote-repo.js";
2324

2425
/**
2526
* Wrapper class for BrowserStack MCP Server
@@ -61,6 +62,7 @@ export class BrowserStackMcpServer {
6162
addSelfHealTools,
6263
addBuildInsightsTools,
6364
addRCATools,
65+
addRemoteRepoTools,
6466
];
6567

6668
toolAdders.forEach((adder) => {

src/tools/fetch-from-repo.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// src/tools/fetch-from-repo.ts
2+
3+
import { z } from "zod";
4+
import { RemoteIndexer } from "../repo/RemoteIndexer.js";
5+
import { RepositoryFetcher } from "../repo/RepositoryFetcher.js";
6+
7+
export function createFetchFromRepoTool(
8+
indexer: RemoteIndexer,
9+
fetcher: RepositoryFetcher,
10+
) {
11+
return {
12+
name: "fetchFromRepo",
13+
description: "Fetch a file from a remote repository that has been indexed",
14+
inputSchema: z.object({
15+
path: z.string().describe("The file path in the repository"),
16+
}),
17+
handler: async ({ path }: { path: string }) => {
18+
if (!indexer.fileExists(path)) {
19+
return {
20+
content: [
21+
{
22+
type: "text" as const,
23+
text: `Error: File not found in index: ${path}`,
24+
},
25+
],
26+
};
27+
}
28+
try {
29+
const content = await fetcher.getFileContent(path);
30+
return {
31+
content: [
32+
{
33+
type: "text" as const,
34+
text: `File: ${path}\n\n${content}`,
35+
},
36+
],
37+
};
38+
} catch (error: any) {
39+
return {
40+
content: [
41+
{
42+
type: "text" as const,
43+
text: `Error fetching file: ${error.message}`,
44+
},
45+
],
46+
};
47+
}
48+
},
49+
};
50+
}

src/tools/remote-repo.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// src/tools/remote-repo.ts
2+
3+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4+
import { BrowserStackConfig } from "../lib/types.js";
5+
import { setupRemoteRepoMCP } from "../repo/repo-setup.js";
6+
import logger from "../logger.js";
7+
8+
/**
9+
* Adds remote repository tools (if configured)
10+
*/
11+
export default function addRemoteRepoTools(
12+
server: McpServer,
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
_config: BrowserStackConfig,
15+
): Record<string, any> {
16+
const tools: Record<string, any> = {};
17+
18+
// Check if GitHub token is configured
19+
const githubToken = process.env.GITHUB_TOKEN;
20+
const repoOwner = process.env.GITHUB_REPO_OWNER;
21+
const repoName = process.env.GITHUB_REPO_NAME;
22+
const repoBranch = process.env.GITHUB_REPO_BRANCH || "main";
23+
24+
if (!githubToken || !repoOwner || !repoName) {
25+
logger.info(
26+
"Remote repository integration not configured. Skipping remote repo tools.",
27+
);
28+
return tools;
29+
}
30+
31+
logger.info(
32+
"Setting up remote repository integration for %s/%s (branch: %s)",
33+
repoOwner,
34+
repoName,
35+
repoBranch,
36+
);
37+
38+
// Set up remote repo asynchronously
39+
setupRemoteRepoMCP({
40+
owner: repoOwner,
41+
repo: repoName,
42+
branch: repoBranch,
43+
token: githubToken,
44+
})
45+
.then(({ fetchFromRepoTool, contextResource, indexer }) => {
46+
// Register the fetch tool
47+
const registeredTool = server.tool(
48+
fetchFromRepoTool.name,
49+
fetchFromRepoTool.description,
50+
fetchFromRepoTool.inputSchema.shape,
51+
fetchFromRepoTool.handler,
52+
);
53+
54+
tools[fetchFromRepoTool.name] = registeredTool;
55+
56+
// Register the context resource
57+
server.resource(contextResource.name, contextResource.uri, async () =>
58+
contextResource.handler(),
59+
);
60+
61+
logger.info(
62+
"Remote repository tools registered successfully. Indexed %d files.",
63+
indexer.getIndex().size,
64+
);
65+
})
66+
.catch((error) => {
67+
logger.error(
68+
"Failed to set up remote repository integration: %s",
69+
error.message,
70+
);
71+
});
72+
73+
return tools;
74+
}

0 commit comments

Comments
 (0)