UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

306 lines 9.81 kB
/** * GitHubAPIStub provides mock GitHub API functionality for testing. * * Simulates GitHub API interactions without making real network requests. * Supports configurable responses, error injection, rate limiting, and request tracking. * * @example * ```typescript * const github = new GitHubAPIStub(); * * // Create a mock issue * const issue = await github.createIssue('Bug: Fix login', 'Login not working'); * * // Configure custom response * github.setResponse('/repos/owner/repo/issues', 'GET', { issues: [] }); * * // Inject error for testing * github.injectError('/repos/owner/repo/pulls', new Error('API Error')); * * // Check request history * const requests = github.getRequestHistory(); * ``` */ export class GitHubAPIStub { responses = new Map(); requestHistory = []; issues = new Map(); pullRequests = new Map(); issueCounter = 1; prCounter = 1; rateLimitRemaining = 5000; rateLimitReset = Date.now() + 3600000; // 1 hour from now injectedErrors = []; /** * Set a custom response for a specific endpoint and method. * * @param endpoint - API endpoint path * @param method - HTTP method (GET, POST, PUT, DELETE, etc.) * @param response - Response data * @param statusCode - HTTP status code (default: 200) */ setResponse(endpoint, method, response, statusCode = 200) { const key = this.getResponseKey(endpoint, method); this.responses.set(key, { data: response, statusCode }); } /** * Configure rate limit for testing rate limit handling. * * @param remaining - Number of requests remaining * @param resetTime - Unix timestamp when rate limit resets (default: 1 hour from now) */ setRateLimit(remaining, resetTime) { this.rateLimitRemaining = remaining; this.rateLimitReset = resetTime ?? (Date.now() + 3600000); } /** * Make a simulated API request. * * @param endpoint - API endpoint path * @param method - HTTP method * @param body - Request body (optional) * @returns GitHub API response */ async request(endpoint, method, body) { // Record request this.requestHistory.push({ endpoint, method, body, timestamp: Date.now() }); // Check for injected errors const injectedError = this.injectedErrors.find(e => e.endpoint === endpoint); if (injectedError) { throw injectedError.error; } // Check rate limit if (this.rateLimitRemaining <= 0) { return { data: { message: 'API rate limit exceeded', documentation_url: 'https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting' }, status: 403, headers: this.getRateLimitHeaders() }; } // Decrement rate limit this.rateLimitRemaining--; // Check for custom response const key = this.getResponseKey(endpoint, method); const mockResponse = this.responses.get(key); if (mockResponse) { return { data: mockResponse.data, status: mockResponse.statusCode, headers: this.getRateLimitHeaders() }; } // Default response return { data: { message: 'Not Found' }, status: 404, headers: this.getRateLimitHeaders() }; } /** * Create a new issue. * * @param title - Issue title * @param body - Issue body (optional) * @param labels - Label names to apply (optional) * @returns Created issue */ async createIssue(title, body, labels) { const issueNumber = this.issueCounter++; const now = new Date().toISOString(); const issue = { number: issueNumber, title, body: body ?? '', state: 'open', labels: (labels ?? []).map(name => ({ name, color: '0366d6' })), createdAt: now, updatedAt: now }; this.issues.set(issueNumber, issue); // Record request this.requestHistory.push({ endpoint: '/repos/owner/repo/issues', method: 'POST', body: { title, body, labels }, timestamp: Date.now() }); return issue; } /** * Get an issue by number. * * @param number - Issue number * @returns Issue data * @throws Error if issue not found */ async getIssue(number) { // Record request this.requestHistory.push({ endpoint: `/repos/owner/repo/issues/${number}`, method: 'GET', timestamp: Date.now() }); const issue = this.issues.get(number); if (!issue) { throw new Error(`Issue #${number} not found`); } return issue; } /** * List issues with optional filtering. * * @param options - Filter options * @returns Array of issues */ async listIssues(options = {}) { // Record request this.requestHistory.push({ endpoint: '/repos/owner/repo/issues', method: 'GET', body: options, timestamp: Date.now() }); let issueList = Array.from(this.issues.values()); // Filter by state if (options.state && options.state !== 'all') { issueList = issueList.filter(issue => issue.state === options.state); } // Filter by labels if (options.labels && options.labels.length > 0) { issueList = issueList.filter(issue => { const issueLabels = issue.labels.map(l => l.name); return options.labels.every(label => issueLabels.includes(label)); }); } // Sort by number (descending) issueList.sort((a, b) => b.number - a.number); // Pagination const page = options.page ?? 1; const perPage = options.per_page ?? 30; const startIndex = (page - 1) * perPage; const endIndex = startIndex + perPage; return issueList.slice(startIndex, endIndex); } /** * Create a new pull request. * * @param title - PR title * @param head - Head branch name * @param base - Base branch name * @returns Created pull request */ async createPullRequest(title, head, base) { const prNumber = this.prCounter++; const now = new Date().toISOString(); const pr = { number: prNumber, title, head, base, state: 'open', createdAt: now }; this.pullRequests.set(prNumber, pr); // Record request this.requestHistory.push({ endpoint: '/repos/owner/repo/pulls', method: 'POST', body: { title, head, base }, timestamp: Date.now() }); return pr; } /** * Add labels to an issue. * * @param issueNumber - Issue number * @param labels - Label names to add * @throws Error if issue not found */ async addLabel(issueNumber, labels) { // Record request this.requestHistory.push({ endpoint: `/repos/owner/repo/issues/${issueNumber}/labels`, method: 'POST', body: { labels }, timestamp: Date.now() }); const issue = this.issues.get(issueNumber); if (!issue) { throw new Error(`Issue #${issueNumber} not found`); } // Add new labels (avoid duplicates) const existingLabels = new Set(issue.labels.map(l => l.name)); for (const labelName of labels) { if (!existingLabels.has(labelName)) { issue.labels.push({ name: labelName, color: '0366d6' }); } } issue.updatedAt = new Date().toISOString(); } /** * Reset the stub to initial state. * Clears all issues, pull requests, responses, and request history. */ reset() { this.responses.clear(); this.requestHistory = []; this.issues.clear(); this.pullRequests.clear(); this.issueCounter = 1; this.prCounter = 1; this.rateLimitRemaining = 5000; this.rateLimitReset = Date.now() + 3600000; this.injectedErrors = []; } /** * Get the history of all requests made to the stub. * * @returns Array of requests in chronological order */ getRequestHistory() { return [...this.requestHistory]; } /** * Inject an error for a specific endpoint. * The next request to this endpoint will throw the specified error. * * @param endpoint - API endpoint path * @param error - Error to throw */ injectError(endpoint, error) { this.injectedErrors.push({ endpoint, error }); } /** * Inject a rate limit error. * Sets rate limit to 0, causing subsequent requests to return 403. */ injectRateLimitError() { this.rateLimitRemaining = 0; } /** * Generate a unique key for response mapping. */ getResponseKey(endpoint, method) { return `${method.toUpperCase()}:${endpoint}`; } /** * Generate rate limit headers. */ getRateLimitHeaders() { return { 'x-ratelimit-limit': '5000', 'x-ratelimit-remaining': this.rateLimitRemaining.toString(), 'x-ratelimit-reset': Math.floor(this.rateLimitReset / 1000).toString() }; } } //# sourceMappingURL=github-stub.js.map