Add project files
This commit is contained in:
92
testing/fake-llm-server/README.md
Normal file
92
testing/fake-llm-server/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Fake LLM Server
|
||||
|
||||
A simple server that mimics the OpenAI streaming chat completions API for testing purposes.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements a basic version of the OpenAI chat completions API
|
||||
- Supports both streaming and non-streaming responses
|
||||
- Always responds with "hello world" message
|
||||
- Configurable through environment variables
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Start the server:
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
npm run dev
|
||||
|
||||
# Production mode
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Example usage
|
||||
|
||||
```
|
||||
curl -X POST http://localhost:3500/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"messages":[{"role":"user","content":"Say something"}],"model":"any-model","stream":true}'
|
||||
```
|
||||
|
||||
The server will be available at http://localhost:3500 by default.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST /v1/chat/completions
|
||||
|
||||
This endpoint mimics OpenAI's chat completions API.
|
||||
|
||||
#### Request Format
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [{ "role": "user", "content": "Your prompt here" }],
|
||||
"model": "any-model",
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
- Set `stream: true` to receive a streaming response
|
||||
- Set `stream: false` or omit it for a regular JSON response
|
||||
|
||||
#### Response
|
||||
|
||||
For non-streaming requests, you'll get a standard JSON response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-123456789",
|
||||
"object": "chat.completion",
|
||||
"created": 1699000000,
|
||||
"model": "fake-model",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "hello world"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For streaming requests, you'll receive a series of server-sent events (SSE), each containing a chunk of the response.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the server by modifying the `PORT` variable in the code.
|
||||
|
||||
## Use Case
|
||||
|
||||
This server is primarily intended for testing applications that integrate with OpenAI's API, allowing you to develop and test without making actual API calls to OpenAI.
|
||||
255
testing/fake-llm-server/chatCompletionHandler.ts
Normal file
255
testing/fake-llm-server/chatCompletionHandler.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { Request, Response } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { CANNED_MESSAGE, createStreamChunk } from ".";
|
||||
|
||||
let globalCounter = 0;
|
||||
|
||||
export const createChatCompletionHandler =
|
||||
(prefix: string) => (req: Request, res: Response) => {
|
||||
const { stream = false, messages = [] } = req.body;
|
||||
console.log("* Received messages", messages);
|
||||
|
||||
// Disabled rate limit simulation
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
let messageContent = CANNED_MESSAGE;
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
Array.isArray(lastMessage.content) &&
|
||||
lastMessage.content.some(
|
||||
(part: { type: string; text: string }) =>
|
||||
part.type === "text" &&
|
||||
part.text.includes("[[UPLOAD_IMAGE_TO_CODEBASE]]"),
|
||||
)
|
||||
) {
|
||||
messageContent = `Uploading image to codebase
|
||||
<dyad-write path="new/image/file.png" description="Uploaded image to codebase">
|
||||
DYAD_ATTACHMENT_0
|
||||
</dyad-write>
|
||||
`;
|
||||
messageContent += "\n\n" + generateDump(req);
|
||||
}
|
||||
|
||||
// TS auto-fix prefixes
|
||||
if (
|
||||
lastMessage &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.startsWith(
|
||||
"Fix these 2 TypeScript compile-time error",
|
||||
)
|
||||
) {
|
||||
// Fix errors in create-ts-errors.md and introduce a new error
|
||||
messageContent = `
|
||||
<dyad-write path="src/bad-file.ts" description="Fix 2 errors and introduce a new error.">
|
||||
// Import doesn't exist
|
||||
// import NonExistentClass from 'non-existent-class';
|
||||
|
||||
|
||||
const x = new Object();
|
||||
x.nonExistentMethod2();
|
||||
</dyad-write>
|
||||
|
||||
`;
|
||||
}
|
||||
if (
|
||||
lastMessage &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.startsWith(
|
||||
"Fix these 1 TypeScript compile-time error",
|
||||
)
|
||||
) {
|
||||
// Fix errors in create-ts-errors.md and introduce a new error
|
||||
messageContent = `
|
||||
<dyad-write path="src/bad-file.ts" description="Fix remaining error.">
|
||||
// Import doesn't exist
|
||||
// import NonExistentClass from 'non-existent-class';
|
||||
|
||||
|
||||
const x = new Object();
|
||||
x.toString(); // replaced with existing method
|
||||
</dyad-write>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.includes("TypeScript compile-time error")
|
||||
) {
|
||||
messageContent += "\n\n" + generateDump(req);
|
||||
}
|
||||
if (
|
||||
lastMessage &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.startsWith("Fix error: Error Line 6 error")
|
||||
) {
|
||||
messageContent = `
|
||||
Fixing the error...
|
||||
<dyad-write path="src/pages/Index.tsx">
|
||||
|
||||
|
||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">No more errors!</h1>
|
||||
</div>
|
||||
<MadeWithDyad />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
||||
</dyad-write>
|
||||
`;
|
||||
}
|
||||
console.error("LASTMESSAGE", lastMessage);
|
||||
// Check if the last message is "[dump]" to write messages to file and return path
|
||||
if (
|
||||
lastMessage &&
|
||||
(Array.isArray(lastMessage.content)
|
||||
? lastMessage.content.some(
|
||||
(part: { type: string; text: string }) =>
|
||||
part.type === "text" && part.text.includes("[dump]"),
|
||||
)
|
||||
: lastMessage.content.includes("[dump]"))
|
||||
) {
|
||||
messageContent = generateDump(req);
|
||||
}
|
||||
|
||||
if (lastMessage && lastMessage.content === "[increment]") {
|
||||
globalCounter++;
|
||||
messageContent = `counter=${globalCounter}`;
|
||||
}
|
||||
|
||||
// Check if the last message starts with "tc=" to load test case file
|
||||
if (
|
||||
lastMessage &&
|
||||
lastMessage.content &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.startsWith("tc=")
|
||||
) {
|
||||
const testCaseName = lastMessage.content.slice(3); // Remove "tc=" prefix
|
||||
const testFilePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"e2e-tests",
|
||||
"fixtures",
|
||||
prefix,
|
||||
`${testCaseName}.md`,
|
||||
);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(testFilePath)) {
|
||||
messageContent = fs.readFileSync(testFilePath, "utf-8");
|
||||
console.log(`* Loaded test case: ${testCaseName}`);
|
||||
} else {
|
||||
console.log(`* Test case file not found: ${testFilePath}`);
|
||||
messageContent = `Error: Test case file not found: ${testCaseName}.md`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`* Error reading test case file: ${error}`);
|
||||
messageContent = `Error: Could not read test case file: ${testCaseName}.md`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
lastMessage.content &&
|
||||
typeof lastMessage.content === "string" &&
|
||||
lastMessage.content.trim().endsWith("[[STRING_TO_BE_FINISHED]]")
|
||||
) {
|
||||
messageContent = `[[STRING_IS_FINISHED]]";</dyad-write>\nFinished writing file.`;
|
||||
messageContent += "\n\n" + generateDump(req);
|
||||
}
|
||||
|
||||
// Non-streaming response
|
||||
if (!stream) {
|
||||
return res.json({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: "fake-model",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: messageContent,
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Streaming response
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
|
||||
// Split the message into characters to simulate streaming
|
||||
const message = messageContent;
|
||||
const messageChars = message.split("");
|
||||
|
||||
// Stream each character with a delay
|
||||
let index = 0;
|
||||
const batchSize = 8;
|
||||
|
||||
// Send role first
|
||||
res.write(createStreamChunk("", "assistant"));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (index < messageChars.length) {
|
||||
// Get the next batch of characters (up to batchSize)
|
||||
const batch = messageChars.slice(index, index + batchSize).join("");
|
||||
res.write(createStreamChunk(batch));
|
||||
index += batchSize;
|
||||
} else {
|
||||
// Send the final chunk
|
||||
res.write(createStreamChunk("", "assistant", true));
|
||||
clearInterval(interval);
|
||||
res.end();
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
function generateDump(req: Request) {
|
||||
const timestamp = Date.now();
|
||||
const generatedDir = path.join(__dirname, "generated");
|
||||
|
||||
// Create generated directory if it doesn't exist
|
||||
if (!fs.existsSync(generatedDir)) {
|
||||
fs.mkdirSync(generatedDir, { recursive: true });
|
||||
}
|
||||
|
||||
const dumpFilePath = path.join(generatedDir, `${timestamp}.json`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
dumpFilePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
body: req.body,
|
||||
headers: { authorization: req.headers["authorization"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
).replace(/\r\n/g, "\n"),
|
||||
"utf-8",
|
||||
);
|
||||
console.log(`* Dumped messages to: ${dumpFilePath}`);
|
||||
return `[[dyad-dump-path=${dumpFilePath}]]`;
|
||||
} catch (error) {
|
||||
console.error(`* Error writing dump file: ${error}`);
|
||||
return `Error: Could not write dump file: ${error}`;
|
||||
}
|
||||
}
|
||||
418
testing/fake-llm-server/githubHandler.ts
Normal file
418
testing/fake-llm-server/githubHandler.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { Request, Response } from "express";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
const gitHttpMiddlewareFactory = require("git-http-mock-server/middleware");
|
||||
|
||||
// Push event tracking for tests
|
||||
interface PushEvent {
|
||||
timestamp: Date;
|
||||
repo: string;
|
||||
branch: string;
|
||||
operation: "push" | "create" | "delete";
|
||||
commitSha?: string;
|
||||
}
|
||||
|
||||
const pushEvents: PushEvent[] = [];
|
||||
|
||||
// Mock data for testing
|
||||
const mockAccessToken = "fake_access_token_12345";
|
||||
const mockDeviceCode = "fake_device_code_12345";
|
||||
const mockUserCode = "FAKE-CODE";
|
||||
const mockUser = {
|
||||
login: "testuser",
|
||||
id: 12345,
|
||||
email: "testuser@example.com",
|
||||
};
|
||||
|
||||
const mockRepos = [
|
||||
{
|
||||
id: 1,
|
||||
name: "test-repo-1",
|
||||
full_name: "testuser/test-repo-1",
|
||||
private: false,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "test-repo-2",
|
||||
full_name: "testuser/test-repo-2",
|
||||
private: true,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "existing-app",
|
||||
full_name: "testuser/existing-app",
|
||||
private: false,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
];
|
||||
|
||||
const mockBranches = [
|
||||
{ name: "main", commit: { sha: "abc123" } },
|
||||
{ name: "develop", commit: { sha: "def456" } },
|
||||
{ name: "feature/test", commit: { sha: "ghi789" } },
|
||||
];
|
||||
|
||||
// Store device flow state
|
||||
let deviceFlowState = {
|
||||
deviceCode: mockDeviceCode,
|
||||
userCode: mockUserCode,
|
||||
authorized: false,
|
||||
pollCount: 0,
|
||||
};
|
||||
|
||||
// GitHub Device Flow - Step 1: Get device code
|
||||
export function handleDeviceCode(req: Request, res: Response) {
|
||||
console.log("* GitHub Device Code requested");
|
||||
|
||||
// Reset state for new flow
|
||||
deviceFlowState = {
|
||||
deviceCode: mockDeviceCode,
|
||||
userCode: mockUserCode,
|
||||
authorized: false,
|
||||
pollCount: 0,
|
||||
};
|
||||
|
||||
res.json({
|
||||
device_code: mockDeviceCode,
|
||||
user_code: mockUserCode,
|
||||
verification_uri: "https://github.com/login/device",
|
||||
verification_uri_complete: `https://github.com/login/device?user_code=${mockUserCode}`,
|
||||
expires_in: 900,
|
||||
interval: 1, // Short interval for testing
|
||||
});
|
||||
}
|
||||
|
||||
// GitHub Device Flow - Step 2: Poll for access token
|
||||
export function handleAccessToken(req: Request, res: Response) {
|
||||
console.log("* GitHub Access Token polling", {
|
||||
pollCount: deviceFlowState.pollCount,
|
||||
});
|
||||
|
||||
const { device_code } = req.body;
|
||||
|
||||
if (device_code !== mockDeviceCode) {
|
||||
return res.status(400).json({
|
||||
error: "invalid_request",
|
||||
error_description: "Invalid device code",
|
||||
});
|
||||
}
|
||||
|
||||
deviceFlowState.pollCount++;
|
||||
|
||||
// Simulate authorization after 3 polls (for testing)
|
||||
if (deviceFlowState.pollCount >= 3) {
|
||||
deviceFlowState.authorized = true;
|
||||
return res.json({
|
||||
access_token: mockAccessToken,
|
||||
token_type: "bearer",
|
||||
scope: "repo,user,workflow",
|
||||
});
|
||||
}
|
||||
|
||||
// Return pending status
|
||||
res.status(400).json({
|
||||
error: "authorization_pending",
|
||||
error_description: "The authorization request is still pending",
|
||||
});
|
||||
}
|
||||
|
||||
// Get authenticated user info
|
||||
export function handleUser(req: Request, res: Response) {
|
||||
console.log("* GitHub User info requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(mockUser);
|
||||
}
|
||||
|
||||
// Get user emails
|
||||
export function handleUserEmails(req: Request, res: Response) {
|
||||
console.log("* GitHub User emails requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
res.json([
|
||||
{
|
||||
email: "testuser@example.com",
|
||||
primary: true,
|
||||
verified: true,
|
||||
visibility: "public",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// List user repositories
|
||||
export function handleUserRepos(req: Request, res: Response) {
|
||||
console.log("* GitHub User repos requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
// List repos
|
||||
res.json(mockRepos);
|
||||
} else if (req.method === "POST") {
|
||||
// Create repo
|
||||
const { name, private: isPrivate } = req.body;
|
||||
console.log("* Creating repository:", name);
|
||||
|
||||
// Check if repo already exists
|
||||
const existingRepo = mockRepos.find((repo) => repo.name === name);
|
||||
if (existingRepo) {
|
||||
return res.status(422).json({
|
||||
message: "Repository creation failed.",
|
||||
errors: [
|
||||
{
|
||||
resource: "Repository",
|
||||
code: "already_exists",
|
||||
field: "name",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Create new repo
|
||||
const newRepo = {
|
||||
id: mockRepos.length + 1,
|
||||
name,
|
||||
full_name: `${mockUser.login}/${name}`,
|
||||
private: !!isPrivate,
|
||||
owner: { login: mockUser.login },
|
||||
default_branch: "main",
|
||||
};
|
||||
|
||||
res.status(201).json(newRepo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get repository info
|
||||
export function handleRepo(req: Request, res: Response) {
|
||||
console.log("* GitHub Repo info requested");
|
||||
|
||||
const { owner, repo } = req.params;
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
|
||||
|
||||
if (!foundRepo) {
|
||||
return res.status(404).json({
|
||||
message: "Not Found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(foundRepo);
|
||||
}
|
||||
|
||||
// Get repository branches
|
||||
export function handleRepoBranches(req: Request, res: Response) {
|
||||
console.log("* GitHub Repo branches requested");
|
||||
|
||||
const { owner, repo } = req.params;
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
|
||||
|
||||
if (!foundRepo) {
|
||||
return res.status(404).json({
|
||||
message: "Not Found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(mockBranches);
|
||||
}
|
||||
|
||||
// Create repository for organization (not implemented in mock)
|
||||
export function handleOrgRepos(req: Request, res: Response) {
|
||||
console.log("* GitHub Org repos requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
// For simplicity, just redirect to user repos for mock
|
||||
handleUserRepos(req, res);
|
||||
}
|
||||
|
||||
// Push event management functions for testing
|
||||
export function handleGetPushEvents(req: Request, res: Response) {
|
||||
console.log("* Getting push events");
|
||||
const { repo } = req.query;
|
||||
|
||||
const events = repo ? pushEvents.filter((e) => e.repo === repo) : pushEvents;
|
||||
|
||||
res.json(events);
|
||||
}
|
||||
|
||||
export function handleClearPushEvents(req: Request, res: Response) {
|
||||
console.log("* Clearing push events");
|
||||
pushEvents.length = 0;
|
||||
res.json({ cleared: true, timestamp: new Date() });
|
||||
}
|
||||
|
||||
// Handle Git operations (push, pull, clone, etc.) using git-http-mock-server
|
||||
export function handleGitPush(req: Request, res: Response, next?: Function) {
|
||||
console.log("* GitHub Git operation requested:", req.method, req.url);
|
||||
|
||||
// Log request headers to see git operation details
|
||||
console.log("* Git Headers:", {
|
||||
"git-protocol": req.headers["git-protocol"],
|
||||
"content-type": req.headers["content-type"],
|
||||
"user-agent": req.headers["user-agent"],
|
||||
});
|
||||
|
||||
// Create a unique temporary directory for this request
|
||||
const mockReposRoot = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
"dyad-git-mock-" + Math.random().toString(36).substring(2, 15),
|
||||
),
|
||||
);
|
||||
console.error(`* Created temporary git repos directory: ${mockReposRoot}`);
|
||||
|
||||
// Create git middleware instance for this request
|
||||
const gitHttpMiddleware = gitHttpMiddlewareFactory({
|
||||
root: mockReposRoot,
|
||||
route: "/github/git",
|
||||
glob: "*.git",
|
||||
});
|
||||
|
||||
// Extract repo name from URL path like /github/git/testuser/test-repo.git
|
||||
// The middleware expects the repo name as the basename after the route
|
||||
const urlPath = req.url;
|
||||
const match = urlPath.match(/\/github\/git\/[^/]+\/([^/.]+)\.git/);
|
||||
const repoName = match?.[1];
|
||||
|
||||
if (repoName) {
|
||||
console.log(`* Git operation for repo: ${repoName}`);
|
||||
|
||||
// Track push events if this is a git-receive-pack (push) operation
|
||||
if (req.url.includes("/git-receive-pack") && req.method === "POST") {
|
||||
console.log("* Git PUSH operation detected for repo:", repoName);
|
||||
|
||||
// Collect request body to parse git protocol
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
// Parse git pack protocol for branch refs
|
||||
// Git protocol sends refs in format: "old-sha new-sha refs/heads/branch-name"
|
||||
const lines = body.split("\n");
|
||||
lines.forEach((line) => {
|
||||
// Look for lines containing refs/heads/
|
||||
const refMatch = line.match(
|
||||
// eslint-disable-next-line
|
||||
/([0-9a-f]{40})\s+([0-9a-f]{40})\s+refs\/heads\/([^\s\u0000]+)/,
|
||||
);
|
||||
if (refMatch) {
|
||||
const [, oldSha, newSha, branchName] = refMatch;
|
||||
const isDelete = newSha === "0".repeat(40);
|
||||
const isCreate = oldSha === "0".repeat(40);
|
||||
|
||||
let operation: "push" | "create" | "delete" = "push";
|
||||
if (isDelete) operation = "delete";
|
||||
else if (isCreate) operation = "create";
|
||||
|
||||
pushEvents.push({
|
||||
timestamp: new Date(),
|
||||
repo: repoName,
|
||||
branch: branchName,
|
||||
operation,
|
||||
commitSha: isDelete ? oldSha : newSha,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`* Recorded ${operation} to ${repoName}/${branchName}, commit: ${isDelete ? oldSha : newSha}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("* Error parsing git protocol:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the bare git repository exists for this repo
|
||||
const bareRepoPath = path.join(mockReposRoot, `${repoName}.git`);
|
||||
console.log(`* Creating bare git repository at: ${bareRepoPath}`);
|
||||
try {
|
||||
fs.mkdirSync(bareRepoPath, { recursive: true });
|
||||
// Initialize as bare repository
|
||||
const { execSync } = require("child_process");
|
||||
execSync(`git init --bare`, { cwd: bareRepoPath });
|
||||
console.log(
|
||||
`* Successfully created bare git repository: ${repoName}.git`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`* Failed to create bare git repository:`, error);
|
||||
return res.status(500).json({
|
||||
message: "Failed to initialize git repository",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
// Rewrite the URL to match what the middleware expects
|
||||
// Change /github/git/testuser/test-repo.git/... to /github/git/test-repo.git/...
|
||||
const rewrittenUrl = req.url.replace(
|
||||
/\/github\/git\/[^/]+\//,
|
||||
"/github/git/",
|
||||
);
|
||||
req.url = rewrittenUrl;
|
||||
console.log(`* Rewritten URL from ${urlPath} to ${rewrittenUrl}`);
|
||||
}
|
||||
|
||||
// Use git-http-mock-server middleware to handle the actual git operations
|
||||
gitHttpMiddleware(
|
||||
req,
|
||||
res,
|
||||
next ||
|
||||
(() => {
|
||||
// Fallback if middleware doesn't handle the request
|
||||
console.log(
|
||||
`* Git middleware did not handle request: ${req.method} ${req.url}`,
|
||||
);
|
||||
res.status(404).json({
|
||||
message: "Git operation not supported",
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
186
testing/fake-llm-server/index.ts
Normal file
186
testing/fake-llm-server/index.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import cors from "cors";
|
||||
import { createChatCompletionHandler } from "./chatCompletionHandler";
|
||||
import {
|
||||
handleDeviceCode,
|
||||
handleAccessToken,
|
||||
handleUser,
|
||||
handleUserEmails,
|
||||
handleUserRepos,
|
||||
handleRepo,
|
||||
handleRepoBranches,
|
||||
handleOrgRepos,
|
||||
handleGitPush,
|
||||
handleGetPushEvents,
|
||||
handleClearPushEvents,
|
||||
} from "./githubHandler";
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
|
||||
|
||||
const PORT = 3500;
|
||||
|
||||
// Helper function to create OpenAI-like streaming response chunks
|
||||
export function createStreamChunk(
|
||||
content: string,
|
||||
role: string = "assistant",
|
||||
isLast: boolean = false,
|
||||
) {
|
||||
const chunk = {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: "fake-model",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: isLast ? {} : { content, role },
|
||||
finish_reason: isLast ? "stop" : null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return `data: ${JSON.stringify(chunk)}\n\n${isLast ? "data: [DONE]\n\n" : ""}`;
|
||||
}
|
||||
|
||||
export const CANNED_MESSAGE = `
|
||||
<dyad-write path="file1.txt">
|
||||
A file (2)
|
||||
</dyad-write>
|
||||
More
|
||||
EOM`;
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.send("OK");
|
||||
});
|
||||
|
||||
// Ollama-specific endpoints
|
||||
app.get("/ollama/api/tags", (req, res) => {
|
||||
const ollamaModels = {
|
||||
models: [
|
||||
{
|
||||
name: "testollama",
|
||||
modified_at: "2024-05-01T10:00:00.000Z",
|
||||
size: 4700000000,
|
||||
digest: "abcdef123456",
|
||||
details: {
|
||||
format: "gguf",
|
||||
family: "llama",
|
||||
families: ["llama"],
|
||||
parameter_size: "8B",
|
||||
quantization_level: "Q4_0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "codellama:7b",
|
||||
modified_at: "2024-04-25T12:30:00.000Z",
|
||||
size: 3800000000,
|
||||
digest: "fedcba654321",
|
||||
details: {
|
||||
format: "gguf",
|
||||
family: "llama",
|
||||
families: ["llama", "codellama"],
|
||||
parameter_size: "7B",
|
||||
quantization_level: "Q5_K_M",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log("* Sending fake Ollama models");
|
||||
res.json(ollamaModels);
|
||||
});
|
||||
|
||||
// LM Studio specific endpoints
|
||||
app.get("/lmstudio/api/v0/models", (req, res) => {
|
||||
const lmStudioModels = {
|
||||
data: [
|
||||
{
|
||||
type: "llm",
|
||||
id: "lmstudio-model-1",
|
||||
object: "model",
|
||||
publisher: "lmstudio",
|
||||
state: "loaded",
|
||||
max_context_length: 4096,
|
||||
quantization: "Q4_0",
|
||||
compatibility_type: "gguf",
|
||||
arch: "llama",
|
||||
},
|
||||
{
|
||||
type: "llm",
|
||||
id: "lmstudio-model-2-chat",
|
||||
object: "model",
|
||||
publisher: "lmstudio",
|
||||
state: "not-loaded",
|
||||
max_context_length: 8192,
|
||||
quantization: "Q5_K_M",
|
||||
compatibility_type: "gguf",
|
||||
arch: "mixtral",
|
||||
},
|
||||
{
|
||||
type: "embedding", // Should be filtered out by client
|
||||
id: "lmstudio-embedding-model",
|
||||
object: "model",
|
||||
publisher: "lmstudio",
|
||||
state: "loaded",
|
||||
max_context_length: 2048,
|
||||
quantization: "F16",
|
||||
compatibility_type: "gguf",
|
||||
arch: "bert",
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log("* Sending fake LM Studio models");
|
||||
res.json(lmStudioModels);
|
||||
});
|
||||
|
||||
["lmstudio", "gateway", "engine", "ollama"].forEach((provider) => {
|
||||
app.post(
|
||||
`/${provider}/v1/chat/completions`,
|
||||
createChatCompletionHandler(provider),
|
||||
);
|
||||
});
|
||||
|
||||
// Default test provider handler:
|
||||
app.post("/v1/chat/completions", createChatCompletionHandler("."));
|
||||
|
||||
// GitHub API Mock Endpoints
|
||||
console.log("Setting up GitHub mock endpoints");
|
||||
|
||||
// GitHub OAuth Device Flow
|
||||
app.post("/github/login/device/code", handleDeviceCode);
|
||||
app.post("/github/login/oauth/access_token", handleAccessToken);
|
||||
|
||||
// GitHub API endpoints
|
||||
app.get("/github/api/user", handleUser);
|
||||
app.get("/github/api/user/emails", handleUserEmails);
|
||||
app.get("/github/api/user/repos", handleUserRepos);
|
||||
app.post("/github/api/user/repos", handleUserRepos);
|
||||
app.get("/github/api/repos/:owner/:repo", handleRepo);
|
||||
app.get("/github/api/repos/:owner/:repo/branches", handleRepoBranches);
|
||||
app.post("/github/api/orgs/:org/repos", handleOrgRepos);
|
||||
|
||||
// GitHub test endpoints for verifying push operations
|
||||
app.get("/github/api/test/push-events", handleGetPushEvents);
|
||||
app.post("/github/api/test/clear-push-events", handleClearPushEvents);
|
||||
|
||||
// GitHub Git endpoints - intercept all paths with /github/git prefix
|
||||
app.all("/github/git/*", handleGitPush);
|
||||
|
||||
// Start the server
|
||||
const server = createServer(app);
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Fake LLM server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
process.on("SIGINT", () => {
|
||||
console.log("Shutting down fake LLM server");
|
||||
server.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
1628
testing/fake-llm-server/package-lock.json
generated
Normal file
1628
testing/fake-llm-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
testing/fake-llm-server/package.json
Normal file
28
testing/fake-llm-server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "fake-llm-server",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node index.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "Fake OpenAI API server for testing",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"stream": "0.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.17.46",
|
||||
"git-http-mock-server": "^2.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
13
testing/fake-llm-server/tsconfig.json
Normal file
13
testing/fake-llm-server/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user